Java社区项目实现发帖、评论的方法见 详解Java社区项目实现发帖、评论的方法(仿百度贴吧)Java社区项目,用户可以发帖,评论,仿百度贴吧,评论共二级评论, - 掘金
场景描述:每个用户都可以对帖子和评论进行点赞,设置点赞表,记录点赞用户id,点赞目标id,点赞目标类型;帖子表和评论表中有点赞数。后端提供接口:“点赞或取消点赞”,判断点赞表中有无当前点赞记录,如果没有就是点赞操作,插入新数据,并根据点赞目标类型在Redis中插入今日帖子/评论点赞数差值,使用Hash存储,key是前缀加帖子/评论标识加年月日,field是帖子/评论id,value加1。如果有就是取消点赞操作,删除数据库对应数据并把Redis中对应value减1即可。点赞数查询时需要查MySQL与Redis中今日点赞数差值之和。点赞状态的判断就是点赞表中有没有对应数据。使用XXL-JOB设置定时任务,每日凌晨将点赞数差值同步到数据库中,然后删除Redis中对应的key。
点赞功能实现方案详解
下面是完整的点赞系统实现,结合数据库、Redis和定时任务,确保高并发下的性能和数据一致性。
系统架构设计
graph TD
A[客户端] --> B[API接口]
B --> C[点赞服务]
C --> D[MySQL数据库]
C --> E[Redis缓存]
F[定时任务] --> E
F --> D
数据库设计
点赞记录表
CREATE TABLE `like_record` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`user_id` BIGINT NOT NULL COMMENT '用户ID',
`target_id` BIGINT NOT NULL COMMENT '点赞目标ID',
`target_type` TINYINT NOT NULL COMMENT '目标类型:0-帖子, 1-评论',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_target` (`user_id`, `target_id`, `target_type`),
KEY `idx_target` (`target_id`, `target_type`)
) COMMENT='点赞记录表';
更新帖子表和评论表
-- 帖子表添加点赞数字段
ALTER TABLE `post` ADD COLUMN `like_count` INT NOT NULL DEFAULT 0 COMMENT '点赞数';
-- 评论表添加点赞数字段
ALTER TABLE `comment` ADD COLUMN `like_count` INT NOT NULL DEFAULT 0 COMMENT '点赞数';
Redis数据结构设计
点赞数差值存储
使用Hash结构存储每日点赞数差值:
- Key格式:
like:daily:{targetType}:{date}targetType: 0(帖子)或1(评论)date: 当前日期(格式: yyyyMMdd)
- Field: 目标ID(帖子ID或评论ID)
- Value: 当日点赞数差值(可正可负)
示例:
Key: like:daily:0:20230615
Field: 12345 (帖子ID)
Value: +5 (当日新增5个点赞)
Key: like:daily:1:20230615
Field: 67890 (评论ID)
Value: -2 (当日取消2个点赞)
核心服务实现
点赞/取消点赞服务
@Service
@RequiredArgsConstructor
public class LikeService {
private final LikeRecordMapper likeRecordMapper;
private final RedisTemplate<String, String> redisTemplate;
private final StringRedisTemplate stringRedisTemplate;
@Transactional
public LikeResult toggleLike(Long userId, Long targetId, Integer targetType) {
// 1. 检查是否已点赞
LikeRecord existing = likeRecordMapper.selectOne(
new QueryWrapper<LikeRecord>()
.eq("user_id", userId)
.eq("target_id", targetId)
.eq("target_type", targetType)
);
String date = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
String redisKey = "like:daily:" + targetType + ":" + date;
if (existing == null) {
// 点赞操作
LikeRecord record = new LikeRecord();
record.setUserId(userId);
record.setTargetId(targetId);
record.setTargetType(targetType);
likeRecordMapper.insert(record);
// Redis操作: 增加差值
incrementRedisDelta(redisKey, targetId, 1);
return new LikeResult(true, getLikeCount(targetId, targetType));
} else {
// 取消点赞操作
likeRecordMapper.deleteById(existing.getId());
// Redis操作: 减少差值
incrementRedisDelta(redisKey, targetId, -1);
return new LikeResult(false, getLikeCount(targetId, targetType));
}
}
private void incrementRedisDelta(String key, Long targetId, int delta) {
stringRedisTemplate.opsForHash().increment(key, targetId.toString(), delta);
// 设置过期时间: 2天(确保定时任务能处理)
stringRedisTemplate.expire(key, 2, TimeUnit.DAYS);
}
public int getLikeCount(Long targetId, Integer targetType) {
// 1. 获取基础点赞数(数据库中的值)
int baseCount = getBaseLikeCount(targetId, targetType);
// 2. 获取Redis中今日的点赞数差值
int todayDelta = getTodayDelta(targetId, targetType);
return baseCount + todayDelta;
}
private int getBaseLikeCount(Long targetId, Integer targetType) {
if (targetType == 0) {
return postMapper.selectLikeCount(targetId);
} else {
return commentMapper.selectLikeCount(targetId);
}
}
private int getTodayDelta(Long targetId, Integer targetType) {
String date = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
String key = "like:daily:" + targetType + ":" + date;
Object value = redisTemplate.opsForHash().get(key, targetId.toString());
return value != null ? Integer.parseInt(value.toString()) : 0;
}
}
定时任务实现(XXL-JOB)
@Component
@RequiredArgsConstructor
public class LikeSyncJob {
private final RedisTemplate<String, String> redisTemplate;
private final PostMapper postMapper;
private final CommentMapper commentMapper;
@XxlJob("likeSyncJobHandler")
public void execute() {
// 1. 获取前一天的日期
LocalDate yesterday = LocalDate.now().minusDays(1);
String dateStr = yesterday.format(DateTimeFormatter.BASIC_ISO_DATE);
// 2. 处理帖子点赞同步
syncForTargetType(0, dateStr);
// 3. 处理评论点赞同步
syncForTargetType(1, dateStr);
}
private void syncForTargetType(int targetType, String date) {
String key = "like:daily:" + targetType + ":" + date;
// 获取所有field-value对
Map<Object, Object> entries = redisTemplate.opsForHash().entries(key);
if (entries.isEmpty()) {
redisTemplate.delete(key);
return;
}
// 按目标ID分组处理
Map<Long, Integer> deltaMap = new HashMap<>();
entries.forEach((field, value) -> {
Long targetId = Long.parseLong(field.toString());
int delta = Integer.parseInt(value.toString());
deltaMap.put(targetId, delta);
});
// 批量更新数据库
if (targetType == 0) {
postMapper.batchUpdateLikeCount(deltaMap);
} else {
commentMapper.batchUpdateLikeCount(deltaMap);
}
// 删除Redis key
redisTemplate.delete(key);
}
}
Mapper批量更新方法
// PostMapper.java
@Update("<script>" +
"UPDATE post SET like_count = like_count + CASE id " +
"<foreach item='item' index='key' collection='deltaMap.entrySet()'>" +
"WHEN #{key} THEN #{item.value} " +
"</foreach>" +
"END " +
"WHERE id IN " +
"<foreach item='key' collection='deltaMap.keySet()' open='(' separator=',' close=')'>" +
"#{key}" +
"</foreach>" +
"</script>")
void batchUpdateLikeCount(@Param("deltaMap") Map<Long, Integer> deltaMap);
// CommentMapper.java (类似实现)
点赞状态查询优化
使用Redis缓存点赞状态
@Service
public class LikeStatusService {
private final LikeRecordMapper likeRecordMapper;
private final RedisTemplate<String, String> redisTemplate;
// 获取用户点赞状态
public boolean isLiked(Long userId, Long targetId, Integer targetType) {
String cacheKey = getCacheKey(userId, targetId, targetType);
// 1. 尝试从缓存获取
Boolean cached = redisTemplate.opsForValue().getBit(cacheKey, 0);
if (cached != null) {
return cached;
}
// 2. 查询数据库
boolean exists = likeRecordMapper.exists(
new QueryWrapper<LikeRecord>()
.eq("user_id", userId)
.eq("target_id", targetId)
.eq("target_type", targetType)
);
// 3. 更新缓存
redisTemplate.opsForValue().setBit(cacheKey, 0, exists);
redisTemplate.expire(cacheKey, 7, TimeUnit.DAYS);
return exists;
}
private String getCacheKey(Long userId, Long targetId, Integer targetType) {
return "like:status:" + userId + ":" + targetType + ":" + targetId;
}
// 当点赞状态变化时清除缓存
public void clearCache(Long userId, Long targetId, Integer targetType) {
String cacheKey = getCacheKey(userId, targetId, targetType);
redisTemplate.delete(cacheKey);
}
}
高并发优化策略
1. Redis管道化操作
private void incrementRedisDeltaPipeline(String key, Long targetId, int delta) {
redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
StringRedisConnection stringRedisConn = (StringRedisConnection) connection;
stringRedisConn.hIncrBy(key, targetId.toString(), delta);
stringRedisConn.expire(key, 172800); // 2天
return null;
});
}
2. 热点数据缓存
@Cacheable(value = "likeCount", key = "#targetType + ':' + #targetId")
public int getLikeCount(Long targetId, Integer targetType) {
// 原始实现
}
3. 延迟双删策略
@CacheEvict(value = "likeCount", key = "#targetType + ':' + #targetId")
public void evictLikeCountCache(Long targetId, Integer targetType) {
// 第一次删除缓存
// 执行数据库更新操作
// 延迟删除(确保数据库主从同步完成)
executor.schedule(() -> {
cacheManager.getCache("likeCount").evict(targetType + ":" + targetId);
}, 500, TimeUnit.MILLISECONDS);
}
前端交互实现
点赞组件(Vue 3)
<template>
<div class="like-container" @click="toggleLike">
<div :class="['like-icon', { 'liked': isLiked }]">
<i class="fas fa-heart"></i>
</div>
<div class="like-count">{{ displayCount }}</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
import { useUserStore } from '@/stores/user';
import { toggleLikeApi } from '@/api/like';
const props = defineProps({
targetId: Number,
targetType: Number, // 0: 帖子, 1: 评论
initialLiked: Boolean,
initialCount: Number
});
const emit = defineEmits(['update:likeStatus']);
const userStore = useUserStore();
const isLiked = ref(props.initialLiked);
const actualCount = ref(props.initialCount);
const isLoading = ref(false);
// 显示计数(优化大数字展示)
const displayCount = computed(() => {
if (actualCount.value > 1000) {
return (actualCount.value / 1000).toFixed(1) + 'k';
}
return actualCount.value;
});
// 点赞/取消点赞操作
const toggleLike = async () => {
if (!userStore.isAuthenticated || isLoading.value) return;
isLoading.value = true;
try {
// 乐观更新
const wasLiked = isLiked.value;
const oldCount = actualCount.value;
isLiked.value = !wasLiked;
actualCount.value = wasLiked ? oldCount - 1 : oldCount + 1;
// 发送API请求
const response = await toggleLikeApi({
targetId: props.targetId,
targetType: props.targetType
});
// 确保UI与服务器一致
isLiked.value = response.liked;
actualCount.value = response.count;
emit('update:likeStatus', response.liked);
} catch (error) {
// 出错时回滚
isLiked.value = props.initialLiked;
actualCount.value = props.initialCount;
console.error('点赞操作失败', error);
} finally {
isLoading.value = false;
}
};
</script>
<style scoped>
.like-container {
display: flex;
align-items: center;
cursor: pointer;
}
.like-icon {
color: #8e8e8e;
font-size: 18px;
transition: all 0.3s ease;
}
.like-icon.liked {
color: #ff2d55;
animation: pulse 0.5s;
}
.like-count {
margin-left: 6px;
font-size: 14px;
color: #8e8e8e;
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.2); }
100% { transform: scale(1); }
}
</style>
安全与防护
1. 频率限制
@PostMapping("/toggle")
@RateLimiter(key = "like:${#userId}", limit = 10, period = "60s")
public Result<LikeResult> toggleLike(@RequestBody LikeRequest request) {
// ...
}
2. 请求验证
@Data
public class LikeRequest {
@NotNull(message = "目标ID不能为空")
@Positive(message = "无效的目标ID")
private Long targetId;
@NotNull(message = "目标类型不能为空")
@Range(min = 0, max = 1, message = "无效的目标类型")
private Integer targetType;
}
3. 权限校验
@PreAuthorize("isAuthenticated()")
@PostMapping("/toggle")
public Result<LikeResult> toggleLike(@RequestBody LikeRequest request) {
// 获取当前用户ID
Long userId = SecurityUtils.getCurrentUserId();
// ...
}
系统监控与告警
监控指标
- 点赞操作QPS
- 点赞/取消点赞比例
- Redis内存使用情况
- 定时任务执行时长
- 数据库更新延迟
异常告警
- Redis连接失败
- 数据库更新异常
- 定时任务执行失败
- 点赞操作成功率下降
graph LR
A[应用服务器] -->|发送指标| B[Prometheus]
B --> C[Grafana仪表盘]
B --> D[Alertmanager]
D --> E[邮件告警]
D --> F[短信告警]
D --> G[Slack通知]
总结与扩展
方案优势
- 高性能:Redis处理高并发点赞请求,减轻数据库压力
- 最终一致性:通过定时任务保证数据最终一致
- 可扩展性:水平扩展Redis集群应对更高并发
- 容错机制:Redis持久化保证数据安全
- 实时反馈:客户端乐观更新提升用户体验
扩展方向
- 点赞排行榜:使用Redis ZSET实现实时点赞排行榜
ZINCRBY post:likes:ranking 1 {postId} - 点赞消息通知:集成消息队列发送实时通知
- 反作弊系统:检测异常点赞行为
- 数据分析:分析用户点赞行为模式
- 多级缓存:增加本地缓存减少Redis访问
此方案完整实现了场景描述中的点赞系统要求,通过MySQL、Redis和定时任务的结合,在保证数据一致性的同时提供了高性能的点赞服务,适合高并发的社区场景。