之前看了一篇文章,讲redis的应用场景,其中一个应用场景就是实现点赞功能。
功能点设计
比如发一篇知乎文章就有点赞功能,比如统计某篇文章的点赞数、用户所有文章的点赞总数,因此设计的点赞功能模块有以下功能点:
- 某篇文章的点赞数;
- 用户所有文章的点赞数;
- 用户点赞的文章;
- 持久化到MySQL数据库;
数据库设计
- Redis数据库设计 redis是一个K-V数据库,没有统一的数据结构,针对不同的功能点,设计不同的数据结构类型
- 用户某篇文章的点赞数 使用HashMap>数据结构,HashMap中的key为articleId,value为Set,Set中的值为userId;
- 用户总的点赞数,使用HashMap数据结构,key为userId,value为记录总的点赞数;
- 用户点赞的文章,使用HashMap数据结构,key为userId,value为Set,Set中的值为articleId;
MySQL数据库设计,最主要的两张表,article表和user_like_article表
article表结构
字段值 | 字段类型 | 说明 |
---|
id | varchar2(24) | 主键 |
article_name | varchar2(100) | 文章内容 |
content | blob | 文章内容 |
total_like_count | bigint | 文章总点赞数 |
文章总的点赞数需要和Redis中的点赞数进行同步。
user_like_article表结构
字段值 | 字段类型 | 说明 |
---|
id | varchar(24) | 主键 |
user_id | bigint | 用户ID |
article_id | bigint | 文章ID |
记录用户点赞文章的信息,是一张中间表
说明:表结构设计省略了delete_state、create_time、update_time等字段。
流程图
流程图比较简单,点赞和取消点赞基本实现步骤相同
- 参数校验 对入参进行非空校验
- 逻辑校验 对于用户点赞,用户不能重复点赞相同的文章,对于取消点赞,用户不能取消为点赞的文章。
- 存入Redis 存入的数据主要是文章的点赞数,某篇文章的点赞数,用户点赞的文章
- 定时任务 通过定时【1小时执行一次】,从Redis读取数据持久化到MySQL中。
代码功能实现
public void likeArticle(Long articleId, Long likedUserId, Long likedPostId) {validateParam(articleId, likedUserId, likedPostId); //参数验证logger.info("点赞数据存入redis开始&#xff0c;articleId:{}&#xff0c;likedUserId:{}&#xff0c;likedPostId:{}", articleId, likedUserId, likedPostId);synchronized (this) {//只有未点赞的用户才可以进行点赞likeArticleLogicValidate(articleId, likedUserId, likedPostId);//1.用户总点赞数&#43;1redisTemplate.opsForHash().increment(TOTAL_LIKE_COUNT_KEY, String.valueOf(likedUserId), 1);//2.用户喜欢的文章&#43;1String userLikeResult &#61; (String) redisTemplate.opsForHash().get(USER_LIKE_ARTICLE_KEY, String.valueOf(likedPostId));Set articleIdSet &#61; userLikeResult &#61;&#61; null ? new HashSet<>() : FastjsonUtil.deserializeToSet(userLikeResult, Long.class);articleIdSet.add(articleId);redisTemplate.opsForHash().put(USER_LIKE_ARTICLE_KEY, String.valueOf(likedPostId), FastjsonUtil.serialize(articleIdSet));//3.文章点赞数&#43;1String articleLikedResult &#61; (String) redisTemplate.opsForHash().get(ARTICLE_LIKED_USER_KEY, String.valueOf(articleId));Set likePostIdSet &#61; articleLikedResult &#61;&#61; null ? new HashSet<>() : FastjsonUtil.deserializeToSet(articleLikedResult, Long.class);likePostIdSet.add(likedPostId);redisTemplate.opsForHash().put(ARTICLE_LIKED_USER_KEY, String.valueOf(articleId), FastjsonUtil.serialize(likePostIdSet));logger.info("取消点赞数据存入redis结束&#xff0c;articleId:{}&#xff0c;likedUserId:{}&#xff0c;likedPostId:{}", articleId, likedUserId, likedPostId);}
}
public void unlikeArticle(Long articleId, Long likedUserId, Long likedPostId) {validateParam(articleId, likedUserId, likedPostId); //参数校验logger.info("取消点赞数据存入redis开始&#xff0c;articleId:{}&#xff0c;likedUserId:{}&#xff0c;likedPostId:{}", articleId, likedUserId, likedPostId);//1.用户总点赞数-1synchronized (this) {//只有点赞的用户才可以取消点赞unlikeArticleLogicValidate(articleId, likedUserId, likedPostId);Long totalLikeCount &#61; Long.parseLong((String)redisTemplate.opsForHash().get(TOTAL_LIKE_COUNT_KEY, String.valueOf(likedUserId)));redisTemplate.opsForHash().put(TOTAL_LIKE_COUNT_KEY, String.valueOf(likedUserId), String.valueOf(--totalLikeCount));//2.用户喜欢的文章-1String userLikeResult &#61; (String) redisTemplate.opsForHash().get(USER_LIKE_ARTICLE_KEY, String.valueOf(likedPostId));Set articleIdSet &#61; FastjsonUtil.deserializeToSet(userLikeResult, Long.class);articleIdSet.remove(articleId);redisTemplate.opsForHash().put(USER_LIKE_ARTICLE_KEY, String.valueOf(likedPostId), FastjsonUtil.serialize(articleIdSet));//3.取消用户某篇文章的点赞数String articleLikedResult &#61; (String) redisTemplate.opsForHash().get(ARTICLE_LIKED_USER_KEY, String.valueOf(articleId));Set likePostIdSet &#61; FastjsonUtil.deserializeToSet(articleLikedResult, Long.class);likePostIdSet.remove(likedPostId);redisTemplate.opsForHash().put(ARTICLE_LIKED_USER_KEY, String.valueOf(articleId), FastjsonUtil.serialize(likePostIdSet));}logger.info("取消点赞数据存入redis结束&#xff0c;articleId:{}&#xff0c;likedUserId:{}&#xff0c;likedPostId:{}", articleId, likedUserId, likedPostId);
}
&#64;Scheduled(cron &#61; "0 0 0/1 * * ? ")
public void redisDataToMySQL() {logger.info("time:{}&#xff0c;开始执行Redis数据持久化到MySQL任务", LocalDateTime.now().format(formatter));//1.更新文章总的点赞数Map articleCountMap &#61; redisTemplate.opsForHash().entries(ARTICLE_LIKED_USER_KEY);for (Map.Entry entry : articleCountMap.entrySet()) {String articleId &#61; entry.getKey();Set userIdSet &#61; FastjsonUtil.deserializeToSet(entry.getValue(), Long.class);//1.同步某篇文章总的点赞数到MySQLsynchronizeTotalLikeCount(articleId, userIdSet);//2.同步用户喜欢的文章synchronizeUserLikeArticle(articleId, userIdSet);}logger.info("time:{}&#xff0c;结束执行Redis数据持久化到MySQL任务", LocalDateTime.now().format(formatter));
}
说明&#xff1a;
- 针对存在并发的问题&#xff0c;通过添加synchronize关键字实现
- 另外还有获取某篇文章的点赞数、用户所有文章的点赞数、用户点赞的文章方法实现&#xff0c;方法实现比较简单不说明&#xff0c;可以在完整代码中找到
目前存在的不足
- 用户点赞取消点赞方法中&#xff0c;Redis事物没有保证
- 该应用只适合单机环境&#xff0c;分布式环境下存在并发操作&#xff0c;分布式锁待完成
最后附&#xff1a;欢迎fork与start&#xff0c;如有纰漏欢迎指正
alan-cxh/like_articlegithub.com