JAVA实现点赞功能

JAVA实现点赞功能
最新回答
虚伪无情

2026-05-21 05:23:33

点赞功能是社交类应用常见的交互设计,下面我将基于Redis+MySQL的架构实现一个高效且防重复提交的点赞系统,并提供完整的Java代码实现。

一、系统架构设计

核心设计要点:

  1. Redis缓存层:存储实时点赞数据(Hash结构)和用户操作记录(Set结构)
  2. MySQL持久层:定时同步最终数据
  3. 双重防重机制:

    前端Token防重放

    后端Redis Set去重

  4. 数据一致性保障:通过定时任务同步数据
二、核心代码实现1. 点赞状态枚举类public enum LikeStatus { CANCEL(0, "取消点赞"), LIKE(1, "点赞"); private final int code; private final String desc; // 构造方法、getter方法省略...}2. Redis工具类(含防重逻辑)@Componentpublic class RedisLikeUtil { @Autowired private StringRedisTemplate redisTemplate; private static final String LIKE_COUNT_KEY = "article:like:count:%s"; private static final String USER_LIKE_KEY = "article:user:like:%s"; private static final String SYNC_QUEUE = "like:sync:queue"; / * 执行点赞操作(带防重判断) */ public boolean doLike(Long articleId, Long userId, LikeStatus status) { String countKey = String.format(LIKE_COUNT_KEY, articleId); String userKey = String.format(USER_LIKE_KEY, articleId); String userLikeRecord = userId + ":" + status.getCode(); // 使用Lua脚本保证原子性 String luaScript = "local exists = redis.call('sIsMember', KEYS[2], ARGV[1]) " + "if (exists == 1 and tonumber(ARGV[2]) == 0) or (exists == 0 and tonumber(ARGV[2]) == 1) then " + " if tonumber(ARGV[2]) == 1 then " + " redis.call('hIncrBy', KEYS[1], 'count', 1) " + " redis.call('sAdd', KEYS[2], ARGV[1]) " + " else " + " redis.call('hIncrBy', KEYS[1], 'count', -1) " + " redis.call('sRem', KEYS[2], ARGV[1]) " + " end " + " redis.call('sAdd', KEYS[3], ARGV[3]) " + " return 1 " + "else " + " return 0 " + "end"; DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript, Long.class); Long result = redisTemplate.execute(redisScript, Arrays.asList(countKey, userKey, SYNC_QUEUE), userLikeRecord, String.valueOf(status.getCode()), articleId + ":" + System.currentTimeMillis()); return result != null && result == 1; } / * 获取点赞数 */ public Long getLikeCount(Long articleId) { String key = String.format(LIKE_COUNT_KEY, articleId); String count = redisTemplate.opsForHash().get(key, "count"); return count == null ? 0L : Long.parseLong(count); }}3. 点赞接口实现(Spring Boot)@RestController@RequestMapping("/api/like")public class LikeController { @Autowired private RedisLikeUtil redisLikeUtil; @PostMapping("/{articleId}") public ResponseEntity<?> toggleLike( @PathVariable Long articleId, @RequestParam Long userId, @RequestParam LikeStatus status) { // 1. 参数校验 if (articleId == null || userId == null) { return ResponseEntity.badRequest().body("参数错误"); } // 2. 执行点赞操作 boolean success = redisLikeUtil.doLike(articleId, userId, status); if (!success) { return ResponseEntity.ok().body(Map.of( "code", 400, "msg", "请勿重复操作" )); } // 3. 返回结果 return ResponseEntity.ok().body(Map.of( "code", 200, "msg", "操作成功", "data", Map.of( "likeCount", redisLikeUtil.getLikeCount(articleId), "status", status.getCode() ) )); }}4. 定时任务同步到MySQL@Componentpublic class LikeSyncTask { @Autowired private StringRedisTemplate redisTemplate; @Autowired private ArticleLikeMapper likeMapper; // 每5分钟执行一次 @Scheduled(fixedRate = 300000) public void syncLikeData() { String queueKey = "like:sync:queue"; // 1. 获取待同步的文章ID集合 Set<String> articleIds = redisTemplate.opsForSet().members(queueKey); if (CollectionUtils.isEmpty(articleIds)) { return; } // 2. 批量处理 articleIds.forEach(articleIdStr -> { Long articleId = Long.parseLong(articleIdStr.split(":")[0]); // 2.1 获取点赞数 String countKey = String.format("article:like:count:%s", articleId); String count = redisTemplate.opsForHash().get(countKey, "count"); long likeCount = count == null ? 0 : Long.parseLong(count); // 2.2 获取用户点赞记录 String userKey = String.format("article:user:like:%s", articleId); Set<String> userRecords = redisTemplate.opsForSet().members(userKey); // 2.3 解析用户记录(格式:userId:status) List<ArticleLikeRecord> records = new ArrayList<>(); if (!CollectionUtils.isEmpty(userRecords)) { userRecords.forEach(record -> { String[] parts = record.split(":"); records.add(new ArticleLikeRecord( articleId, Long.parseLong(parts[0]), Integer.parseInt(parts[1]) )); }); } // 2.4 持久化到数据库 likeMapper.batchUpdate( new ArticleLikeCount(articleId, likeCount), records ); // 2.5 从同步队列中移除 redisTemplate.opsForSet().remove(queueKey, articleIdStr); }); }}5. MySQL数据访问层@Mapperpublic interface ArticleLikeMapper { // 批量更新点赞数和用户记录 void batchUpdate( @Param("count") ArticleLikeCount count, @Param("records") List<ArticleLikeRecord> records );}

对应的XML映射文件:

<update id="batchUpdate"> <!-- 更新点赞总数 --> INSERT INTO article_like_count (article_id, like_count, update_time) VALUES (#{count.articleId}, #{count.likeCount}, NOW()) ON DUPLICATE KEY UPDATE like_count = VALUES(like_count), update_time = VALUES(update_time); <!-- 批量更新用户点赞记录 --> INSERT INTO user_like_record (article_id, user_id, status, create_time) VALUES <foreach collection="records" item="record" separator=","> (#{record.articleId}, #{record.userId}, #{record.status}, NOW()) </foreach> ON DUPLICATE KEY UPDATE status = VALUES(status), update_time = VALUES(create_time);</update>三、关键优化点
  1. 原子性操作:

    使用Lua脚本保证"计数变更+记录操作+同步队列添加"的原子性

    避免多步操作导致的竞态条件

  2. 防重复提交:

    Redis Set存储用户操作记录(articleId:userId:status)

    每次操作前检查是否已存在相同记录

  3. 性能优化:

    Redis Hash存储点赞数(O(1)时间复杂度)

    批量同步机制减少数据库压力

    定时任务错峰执行

  4. 扩展性设计:

    支持多种业务实体点赞(通过articleId抽象)

    易于扩展为点赞排行榜等功能

四、完整流程示意图

  1. 用户发起点赞请求
  2. 服务端校验并执行Redis操作
  3. 记录同步任务到队列
  4. 定时任务批量持久化到MySQL
  5. 前端轮询获取最新点赞数

这个实现方案在保证数据一致性的前提下,通过Redis承担了95%以上的读写压力,MySQL仅作为最终数据存储,适合高并发场景下的点赞功能实现。