Redis 实现文章点赞功能(附带前后端代码、数据库)

您所在的位置:网站首页 redis有数据库的概念吗为什么 Redis 实现文章点赞功能(附带前后端代码、数据库)

Redis 实现文章点赞功能(附带前后端代码、数据库)

2024-05-24 20:25| 来源: 网络整理| 查看: 265

# Redis 实现文章点赞功能(附带前后端代码、数据库)

作者:南侠 (opens new window),编程导航星球 (opens new window) 编号 29240

使用redis与mysql定期同步的方案实现点赞功能的相关逻辑设计和代码编写

# (1)前言及问题分析

点赞功能是很多社交平台和在线应用中常见的一个交互特性,它可以增强用户参与感、社交体验,并且有助于内容的推广。

# 关键特性: 唯一性: 每个用户对同一条内容只能点赞一次,确保用户不能多次重复点赞。 即时性: 点赞的反馈应该是即时的,用户点击点赞按钮后,系统应该迅速响应,不应有明显的延迟。 可见性: 点赞状态应该及时地反映在用户界面上,以便其他用户能够看到谁给某个内容点赞了。 可撤销性: 用户应该能够取消点赞,确保用户可以更改他们的喜好。 计数和排名: 点赞数量通常会被用于衡量内容受欢迎程度,所以需要对点赞数量进行实时的计数和排名。 # (2)功能方案图

# (3)功能点详情 # 1. 前端

在此方案中,前端涉及主要有以下几点:

页面开发:每个组件库都有对应的组件可以使用,比如笔者使用的是Arco Design的卡片插槽,使用vue3集成,代码如下: {{ parseInt(solution.solutionLikes as any, 10) + 1 }} 123456789101112131415进入文章页面时,调用两个接口:1. 文章信息接口;2. 用户是否点赞状态查询接口 用户点赞或取消点赞成功后:手动更新页面文章点赞数(因为redis和mysql同步并不实时,且点赞数是从相对滞后的mysql中查,所以,需要前端手动运算一下,确保给予用户正确的结果反馈,至于退出页面重新进入文章点赞数量不变的问题,其实无所谓,因为我们可以这么说:之所以没变,是因为其他人点赞补充了而已) 承接3,同时,变更用户当前页面点赞状态,无需调用2中的点赞状态查询接口,这样可以提高一些效率 # 2. Redis

点赞是一个频繁的操作。

为什么使用Redis,那么首先是其必要性,以下是chatgpt给出的:

快速读写: Redis 是一个基于内存的高性能键值存储数据库,适合用于需要快速读写的场景,如点赞记录的存储和读取。 计数器: Redis 的原子性操作使其非常适合作为点赞计数器的后端存储,避免了并发操作导致的数据不一致问题。 缓存: Redis 可以用作缓存存储,可以缓解数据库负担,提高系统性能。例如,可以将点赞记录存储在 Redis 中,减轻对主数据库的访问压力。 持久化: Redis 支持数据持久化,可以在需要时将数据保存到磁盘,确保数据的可靠性。 集合和排序集合: Redis 的集合和排序集合数据结构非常适合用于存储用户点赞记录和计数。可以方便地进行添加、删除、查找等操作。 分布式: 在分布式系统中,Redis可以作为分布式锁的一部分,确保在高并发情况下点赞操作的一致性。

总的来说,使用 Redis 可以提高点赞功能的性能、可靠性和扩展性,使系统更加稳定和高效。

明确了必要性之后,本文方案主要使用了以下两个数据结构:

articleId-set:key=articleId,value=set(userId) articleLike-hash:key=articleId,val=likesNum;

使用2主要是便于更快的查询当前文章的点赞数,提高效率,使1专注于点赞者的修改。

(相关代码在4(后端)中)

# 3. Mysql

涉及的表结构主要有两个:

文章表(article):articleId、likesNum。。。 文章点赞表(article_like):articleId、userId。。。

点赞信息稳定下来后,也是要持久化的,因此存到数据库是必要的。

参考代码:

/* Navicat Premium Data Transfer Source Server : zzx Source Server Type : MySQL Source Server Version : 80033 (8.0.33) Source Host : localhost:3306 Source Schema : sspuoj_db_dev Target Server Type : MySQL Target Server Version : 80033 (8.0.33) File Encoding : 65001 Date: 16/12/2023 18:24:31 */ SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for question_solution -- ---------------------------- DROP TABLE IF EXISTS `question_solution`; CREATE TABLE `question_solution` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id', `solutionLikes` bigint NULL DEFAULT 0 COMMENT '题解点赞数', `createTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `updateTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `isDelete` tinyint NOT NULL DEFAULT 0 COMMENT '是否删除', PRIMARY KEY (`id`) USING BTREE, INDEX `idx_id`(`id` ASC) USING BTREE, INDEX `idx_userId`(`userId` ASC) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 25 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; SET FOREIGN_KEY_CHECKS = 1; -- ---------------------------- -- Table structure for article_likes -- ---------------------------- DROP TABLE IF EXISTS `article_likes`; CREATE TABLE `article_likes` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id', `articleId` bigint NULL DEFAULT NULL COMMENT '文章id', `userId` bigint NULL DEFAULT NULL COMMENT '点赞人id', `createTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `updateTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `isDelete` tinyint NOT NULL DEFAULT 0 COMMENT '是否删除', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 9 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; SET FOREIGN_KEY_CHECKS = 1; 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051# 4. 后端

主要服务有3个:

redis-mysql同步的定时任务 查询用户是否点过赞 用户点赞/取消点赞

下面我们结合代码来细说。

首先是2、3的service代码:

查询用户是否点过赞,只需根据articleId查到对应的set看里面有没有该用户 点赞和取消点赞,只需插入或删除set,增加或修改hash package sspu.zzx.sspuoj.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.HashOperations; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.SetOperations; import org.springframework.stereotype.Service; import sspu.zzx.sspuoj.mapper.ArticleLikesMapper; import sspu.zzx.sspuoj.model.entity.ArticleLikes; import sspu.zzx.sspuoj.service.ArticleLikesService; import java.util.Collections; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; /** * @author ZZX * @description 针对表【article_likes】的数据库操作Service实现 * @createDate 2023-12-12 15:14:24 */ @Service public class ArticleLikesServiceImpl extends ServiceImpl implements ArticleLikesService { private final RedisTemplate redisTemplate; private final SetOperations setOperations; private final HashOperations hashOperations; @Autowired public ArticleLikesServiceImpl(RedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; this.setOperations = redisTemplate.opsForSet(); this.hashOperations = redisTemplate.opsForHash(); } // 添加用户到文章的点赞集合中,同时设置集合键永不过期 public void addUserToLikeSet(Long articleId, Long userId) { Long add = setOperations.add(getArticleLikeSetKey(articleId), userId); // 设置集合键150年过期 redisTemplate.expire(getArticleLikeSetKey(articleId), 365 * 150, TimeUnit.DAYS); } // 检查用户是否已经点赞 public boolean isUserLiked(Long articleId, Long userId) { return Boolean.TRUE.equals(setOperations.isMember(getArticleLikeSetKey(articleId), userId)); } // 设置文章的点赞数 public void setArticleLikes(Long articleId, long likes) { hashOperations.put(getArticleLikesHashKey(), articleId, likes); } // 获取文章的点赞数 public Long getArticleLikes(Long articleId) { Object likes = hashOperations.get(getArticleLikesHashKey(), articleId); return likes != null ? Long.parseLong(likes.toString()) : 0L; } // 获取文章点赞的用户ID集合 public Set getArticleLikedUsers(Long articleId) { Set likedUsers = setOperations.members(getArticleLikeSetKey(articleId)); return likedUsers != null ? likedUsers : Collections.emptySet(); } // 移除用户从文章的点赞集合中 public void removeUserFromLikeSet(Long articleId, Long userId) { setOperations.remove(getArticleLikeSetKey(articleId), userId); } // 获取文章的点赞集合的键 private String getArticleLikeSetKey(Long articleId) { return "article:" + articleId + ":likes"; } // 获取文章点赞数的哈希表键 private String getArticleLikesHashKey() { return "article:likes"; } // 点赞 public void like(Long articleId, Long userId) { addUserToLikeSet(articleId, userId); Long likes = getArticleLikes(articleId); if (likes >= 0) { setArticleLikes(articleId, likes + 1); } } // 取消点赞 public void cancelLike(Long articleId, Long userId) { if (isUserLiked(articleId, userId)) { removeUserFromLikeSet(articleId, userId); Long likes = getArticleLikes(articleId); if (likes > 0) { setArticleLikes(articleId, likes - 1); } } } @Override public Boolean likeArticleOrNot(Long articleId, Long userId) { // 获得当前点赞文章的用户集合 Set likeUsers = getArticleLikedUsers(articleId); // 如果存在该用户,就取消点赞 if (likeUsers.size() > 0 && likeUsers.contains(userId)) { cancelLike(articleId, userId); return false; } // 反之,点赞 else { like(articleId, userId); return true; } } @Override public Boolean ifLiked(Long articleId, Long userId) { // 首先从redis检查,如果有,那么数据库里面最终也一定会有 Set articleLikedUsers = getArticleLikedUsers(articleId).stream().map(e -> (Long) e).collect(Collectors.toSet()); if (articleLikedUsers.contains(userId)) return true; /* 这块感觉不用,保证实时性比较好,redis宕机后再同步就好了 // 如果redis中没有,则从数据库中查,有则有,否则那确实是没有 QueryWrapper queryWrapper = new QueryWrapper(); queryWrapper.eq("article_id", articleId); queryWrapper.eq("user_id", userId); List list = this.list(queryWrapper); return !list.isEmpty(); */ return false; } } 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154

最后是,redis与mysql的同步代码(初版代码不是很优雅,但逻辑基本如此,仅供参考)

package sspu.zzx.sspuoj.task; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import sspu.zzx.sspuoj.model.entity.ArticleLikes; import sspu.zzx.sspuoj.model.entity.QuestionSolution; import sspu.zzx.sspuoj.service.QuestionSolutionService; import sspu.zzx.sspuoj.service.impl.ArticleLikesServiceImpl; import java.util.*; import java.util.stream.Collectors; /** * @version 1.0 * @Author ZZX * @Date 2023/12/12 16:54 */ @Component @Slf4j public class ArticleLikesSynTask { @Autowired private QuestionSolutionService questionSolutionService; @Autowired private ArticleLikesServiceImpl articleLikesService; /** * 定时同步文章点赞信息 */ @Scheduled(cron = "0 0 12 */1 * *") // 每1天 // @Scheduled(cron = "0 */1 * * * *") // 每一分钟执行一次 public void synArticleLikes() { log.info("定时同步文章点赞信息 - " + new Date()); // 获取所有title不是【外部图文】的文章 QueryWrapper queryWrapper = new QueryWrapper(); queryWrapper.ne("title", "外部图文"); List articles = questionSolutionService.list(queryWrapper); // 获取所有文章点赞集合 List articleLikes = articleLikesService.list(); // 按文章id分组,Map的值为List Map idToArticleLikesMap = articleLikes.stream().collect(Collectors.groupingBy(ArticleLikes::getArticleId)); // 定义要更新的question_solution List toUpdateSolution = new ArrayList(); // 定义最终要删除和添加的点赞记录 List toDeleteArticleLikes = new ArrayList(); List toAddArticleLikes = new ArrayList(); for (QuestionSolution article : articles) { // 从redis中文章id对应的点赞数 Long articleLikesFromRedis = articleLikesService.getArticleLikes(article.getId()); // 从redis中文章id对应的具体点赞用户集合 List articleLikedUserIds = articleLikesService.getArticleLikedUsers(article.getId()).stream().map(Object::toString) // 假设返回的元素是字符串类型,如果不是,可以根据实际情况调整 .map(Long::parseLong).collect(Collectors.toList()); // 获得要删除的文章点赞记录 List articleLikesFromDB = idToArticleLikesMap.get(article.getId()); if (articleLikesFromDB == null) { articleLikesFromDB = new ArrayList(); } /*如果redis的点赞用户集合为空,则不执行删除和添加, 这种情况我们认为redis宕机然后刚刚重启 并将数据库中的对应数据同步至redis中 */ if (articleLikedUserIds.isEmpty()) { for (ArticleLikes likes : articleLikesFromDB) { articleLikesService.addUserToLikeSet(article.getId(), likes.getUserId()); } articleLikesService.setArticleLikes(article.getId(), articleLikesFromDB.size()); continue; } // 比较数目和结合的size,使其一致,以集合size为准,并更新article对应记录的点赞数 long articleLikeListSize = Long.parseLong(articleLikesFromRedis.toString()); if (articleLikesFromRedis.equals(articleLikeListSize)) { articleLikesService.setArticleLikes(article.getId(), articleLikeListSize); } if (!article.getSolutionLikes().equals(articleLikeListSize)) { article.setSolutionLikes(articleLikeListSize); toUpdateSolution.add(article); } Iterator iterator = articleLikesFromDB.iterator(); while (iterator.hasNext()) { ArticleLikes likes = iterator.next(); if (!articleLikedUserIds.contains(likes.getUserId())) { toDeleteArticleLikes.add(likes); } } // 获得要添加的文章点赞记录 List collectUserIdFromDB = articleLikesFromDB.stream().map(ArticleLikes::getUserId).collect(Collectors.toList()); for (Long articleLikedUserId : articleLikedUserIds) { if (!collectUserIdFromDB.contains(articleLikedUserId)) { ArticleLikes articleLikes1 = new ArticleLikes(); articleLikes1.setArticleId(article.getId()); articleLikes1.setUserId(articleLikedUserId); toAddArticleLikes.add(articleLikes1); } } } // 更新question_solution表 if (toUpdateSolution.size() > 0) { questionSolutionService.updateBatchById(toUpdateSolution); } // 更新article_likes表中的字段(删除和添加) if (toDeleteArticleLikes.size() > 0) { articleLikesService.removeByIds(toDeleteArticleLikes.stream().map(ArticleLikes::getId).collect(Collectors.toList())); } if (toAddArticleLikes.size() > 0) { articleLikesService.saveBatch(toAddArticleLikes); } } } 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125

其中,考虑到redis有可能宕机(因为资源有限,redis没有集群,而且就算有集群,也有可能都挂)的问题,本方案是将redis中set不存在或为空,作为判别标志。在同步时,如果发现redis的set为空,则mysql向redis同步,否则就是redis向mysql同步。这种方案的好处是,判别方便,缺点就是,处理不了所有人对所有文章都不点赞的情况,但这种情况出现的概率比较少,且就算出现,也能容忍,于是采取该方案。

# (4)总结

下面是更新后的要点总结,包括处理Redis宕机的逻辑:

# 前端部分: 页面开发:使用组件库中的组件,如Arco Design的卡片插槽,展示点赞按钮和点赞数量。 进入文章页面时,调用两个接口:获取文章信息接口和查询用户是否点赞状态接口。 用户点赞或取消点赞成功后,手动更新页面上的点赞数,并变更用户当前页面的点赞状态。 # Redis部分: 使用Redis作为存储点赞信息的后端,考虑了快速读写、计数器、缓存、持久化、集合和排序集合等特性。 使用两个主要的数据结构:set存储点赞用户,hash存储点赞数量。 # MySQL部分: 设计了两张表:文章表(article)和文章点赞表(article_likes)。 点赞信息需要持久化到数据库,确保数据的长久保存。 # 后端部分: 提供了Redis与MySQL同步的定时任务,定期将数据从MySQL同步到Redis。 实现了查询用户是否点过赞的接口、用户点赞/取消点赞的接口等服务。 通过定时任务实现了文章点赞信息的同步,确保Redis中的数据与MySQL中的数据一致。 处理了Redis宕机的情况,在同步任务中进行了检查,如果Redis不可用,将Mysql的数据同步至redis,保证系统的可用性。


【本文地址】


今日新闻


推荐新闻


    CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3