使用Redis实现点赞功能
以上是参考文章,以下是个人总结,可能没有以上总结的好,仅做自我复盘。
点赞操作比较频繁,而且比较随意,所以数据变更很快,如果用mysql,会对mysql产生很大的压力,于是决定使用Redis,防止数据丢失,所以会定期将数据持久化同步到mysql中。
然后开始进行分析,点赞功能需要保存的数据都有哪些,比如给某个新闻点赞,那么需要保存的数据需要有该新闻的id(topicId)、点赞的用户id(fromUid)、点赞的状态(status)。
通过研究Redis的基本数据类型,最终觉得使用hash进行保存,定义一个点赞key(AGREE),所以最后的数据结构如下
127.0.0.1:6379> hset AGREE 1::2 0
将被点赞id和点赞用户进行拼接作为一个字段,最后一个0代表状态,0代表未点赞,1代表一点赞,在存入之前进行判断,进行相应的保存。
大概的流程图如下:
具体实现代码:
RedisKeyUtils
package org.jeecg.modules.common.util; /** * @Author: qiaochengqiang * @Date: 2022/2/17 * @Description: 根据一定规则生成key **/ public class RedisKeyUtils { //保存用户点赞数据的key public static final String MAP_KEY_USER_LIKED = "MAP_USER_LIKED"; //保存用户被点赞数量的key public static final String MAP_KEY_USER_LIKED_COUNT = "MAP_USER_LIKED_COUNT"; /** * 拼接被点赞的用户id和点赞的人的id作为key。格式 222222::333333::主题id * @param topicId likedUserId 被点赞的人id == topicId * @param fromUid likedPostId 点赞的人的id == fromUid * @return */ //* @param likedUserId 被点赞的人id == topicId //* @param likedPostId 点赞的人的id == fromUid public static String getLikedKey(String topicId, String fromUid){ StringBuilder builder = new StringBuilder(); builder.append(topicId); builder.append("::"); builder.append(fromUid); return builder.toString(); } }
LikedStatusEnum
package org.jeecg.modules.common.util; import lombok.Getter; /** * @Author: qiaochengqiang * @Date: 2022/2/17 * @Description: 用户点赞状态 **/ @Getter public enum LikedStatusEnum { LIKE(1, "点赞"), UNLIKE(0, "取消点赞/未点赞"), ; private Integer code; private String msg; LikedStatusEnum(Integer code, String msg) { this.code = code; this.msg = msg; } }
CommonAgree
package org.jeecg.modules.agree.entity; import java.io.Serializable; import java.io.UnsupportedEncodingException; import java.util.Date; import java.math.BigDecimal; import java.util.Map; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import com.fasterxml.jackson.annotation.JsonFormat; import org.springframework.format.annotation.DateTimeFormat; import org.jeecgframework.poi.excel.annotation.Excel; import org.jeecg.common.aspect.annotation.Dict; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.EqualsAndHashCode; import lombok.experimental.Accessors; /** * @Description: 点赞表 * @Author: jeecg-boot * @Date: 2022-02-15 * @Version: V1.0 */ @Data @TableName("common_agree") @Accessors(chain = true) @EqualsAndHashCode(callSuper = false) @ApiModel(value="common_agree对象", description="点赞表") public class CommonAgree implements Serializable { private static final long serialVersionUID = 1L; /**主键*/ @TableId(type = IdType.ASSIGN_UUID) @ApiModelProperty(value = "主键") private java.lang.String id; /**创建人*/ @ApiModelProperty(value = "创建人") private java.lang.String createBy; /**创建日期*/ @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") @ApiModelProperty(value = "创建日期") private java.util.Date createTime; /**更新人*/ @ApiModelProperty(value = "更新人") private java.lang.String updateBy; /**更新日期*/ @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") @ApiModelProperty(value = "更新日期") private java.util.Date updateTime; /**所属部门*/ @ApiModelProperty(value = "所属部门") private java.lang.String sysOrgCode; /**系统id*/ @Excel(name = "系统id", width = 15) @ApiModelProperty(value = "系统id") private java.lang.String sysCode; /**租户id*/ @Excel(name = "租户id", width = 15) @ApiModelProperty(value = "租户id") private java.lang.String tenantId; /**基础flag*/ @Excel(name = "基础flag", width = 15) @ApiModelProperty(value = "基础flag") private java.lang.String baseFlag; /**点赞用户*/ @Excel(name = "点赞用户", width = 15) @ApiModelProperty(value = "点赞用户") private java.lang.String fromUid; /**被点赞的评论或回复id*/ @Excel(name = "被点赞的评论或回复id", width = 15) @ApiModelProperty(value = "被点赞的评论或回复id") private java.lang.String topicId; /**被点赞类型*/ @Excel(name = "被点赞类型", width = 15) @ApiModelProperty(value = "被点赞类型") private java.lang.String topicType; /**被点赞用户*/ @Excel(name = "被点赞用户", width = 15) @ApiModelProperty(value = "被点赞用户") private java.lang.String toUid; /**点赞状态*/ @Excel(name = "点赞状态", width = 15) @ApiModelProperty(value = "点赞状态") private java.lang.Integer status; @TableField(exist = false) public Map<String,Integer> map; public CommonAgree() { } public CommonAgree(String topicId, String toUid, Integer status) { this.topicId = topicId; this.toUid = toUid; this.status = status; } }
RedisService
package org.jeecg.modules.common.service; import org.jeecg.modules.agree.entity.CommonAgree; import java.util.List; public interface RedisService { /** * 点赞。状态为1 * @param topicId * @param fromUserId */ void saveLiked2Redis(String topicId, String fromUserId); /** * 取消点赞。将状态改变为0 * @param topicId * @param fromUserId */ void unlikeFromRedis(String topicId, String fromUserId); /** * 从Redis中删除一条点赞数据 * @param topicId * @param fromUserId */ void deleteLikedFromRedis(String topicId, String fromUserId); /** * 该用户的点赞数加1 * @param toUserId */ void incrementLikedCount(String toUserId); /** * 该用户的点赞数减1 * @param toUserId */ void decrementLikedCount(String toUserId); /** * 获取Redis中存储的所有点赞数据 * @return */ List<CommonAgree> getLikedDataFromRedis(); /** * 获取redis中的存储判断是否已经点赞 * @return 可以根据key 字段 判断是否已经点赞 而不用拿到所有 然后进行遍历 */ CommonAgree checkIsAgreeFromRedis(String topidId,String fromUserId); /** * 根据主题id获取所有的点赞数 * @return */ List<CommonAgree> getAgreeFromRedisByTopicId(String topidId); /** * 根据用户id获取所有的点赞数 * @return */ List<CommonAgree> getAgreeFromRedisByFromUserId(String fromUserId); }
RedisServiceImpl
package org.jeecg.modules.common.service.impl; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.jeecg.modules.agree.entity.CommonAgree; import org.jeecg.modules.common.service.RedisService; import org.jeecg.modules.common.util.LikedStatusEnum; import org.jeecg.modules.common.util.RedisKeyUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ScanOptions; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @Service @Slf4j public class RedisServiceImpl implements RedisService { @Autowired RedisTemplate redisTemplate; @Override public void saveLiked2Redis(String topicId, String fromUserId) { String key = RedisKeyUtils.getLikedKey(topicId, fromUserId); //用户点赞,存储的键为:topicId::fromUId,对应的值为 1 redisTemplate.opsForHash().put(RedisKeyUtils.MAP_KEY_USER_LIKED, key, LikedStatusEnum.LIKE.getCode()); } @Override public void unlikeFromRedis(String topicId, String fromUserId) { String key = RedisKeyUtils.getLikedKey(topicId, fromUserId); //用户点赞,存储的键为:topicId::fromUId,对应的值为 0 redisTemplate.opsForHash().put(RedisKeyUtils.MAP_KEY_USER_LIKED, key, LikedStatusEnum.UNLIKE.getCode()); } @Override public void deleteLikedFromRedis(String topicId, String fromUserId) { String key = RedisKeyUtils.getLikedKey(topicId, fromUserId); redisTemplate.opsForHash().delete(RedisKeyUtils.MAP_KEY_USER_LIKED, key); } @Override public void incrementLikedCount(String toUserId) { redisTemplate.opsForHash().increment(RedisKeyUtils.MAP_KEY_USER_LIKED_COUNT, toUserId, 1); } @Override public void decrementLikedCount(String toUserId) { redisTemplate.opsForHash().increment(RedisKeyUtils.MAP_KEY_USER_LIKED_COUNT, toUserId, -1); } @Override public List<CommonAgree> getLikedDataFromRedis() { Cursor<Map.Entry<Object, Object>> cursor = redisTemplate.opsForHash().scan(RedisKeyUtils.MAP_KEY_USER_LIKED, ScanOptions.NONE); List<CommonAgree> list = new ArrayList<>(); while (cursor.hasNext()){ Map.Entry<Object, Object> entry = cursor.next(); String key = (String) entry.getKey(); //分离出 toUserId,fromUserId String[] split = key.split("::"); //被点赞用户 String topicId = split[0]; String fromUserId = split[1]; Integer value = (Integer) entry.getValue(); //组装成 CommonAgree 对象 CommonAgree userLike = new CommonAgree(topicId, fromUserId, value); list.add(userLike); //存到 list 后从 Redis 中删除 redisTemplate.opsForHash().delete(RedisKeyUtils.MAP_KEY_USER_LIKED, key); } return list; } @Override public CommonAgree checkIsAgreeFromRedis(String topicId, String fromUserId) { //可以根据key 字段 判断是否已经点赞 而不用拿到所有 然后进行遍历 String smallKey = topicId+"::"+fromUserId; CommonAgree commonAgree = new CommonAgree(); Integer value = (Integer) redisTemplate.opsForHash().get(RedisKeyUtils.MAP_KEY_USER_LIKED,smallKey); if (value != null){ //如果能够查询到 则将查询到的数据直接进行赋值即可 commonAgree.setStatus(value); }else{ //redis 如果没有 则认为是未点赞 commonAgree.setStatus(LikedStatusEnum.UNLIKE.getCode()); } return commonAgree; } @Override public List<CommonAgree> getAgreeFromRedisByTopicId(String topicId) { List<CommonAgree> commonAgrees = new ArrayList<>(); Cursor<Map.Entry<Object, Object>> cursor = redisTemplate.opsForHash().scan(RedisKeyUtils.MAP_KEY_USER_LIKED, ScanOptions.NONE); while (cursor.hasNext()){ CommonAgree commonAgree = new CommonAgree(); Map.Entry<Object, Object> entry = cursor.next(); String key = (String) entry.getKey(); //分离出 topicId,toUserId String[] split = key.split("::"); //被点赞主题 String topicIdRedis = split[0]; String fromUserIdRedis = split[1]; Integer value = (Integer) entry.getValue(); //如果主题id 点赞用户id 以及 状态为1 则代表已经点赞 if (StringUtils.equals(topicId,topicIdRedis)){ commonAgree.setTopicId(topicIdRedis); commonAgree.setFromUid(fromUserIdRedis); commonAgree.setStatus(value); commonAgrees.add(commonAgree); } } return commonAgrees; } @Override public List<CommonAgree> getAgreeFromRedisByFromUserId(String fromUserId) { List<CommonAgree> commonAgrees = new ArrayList<>(); Cursor<Map.Entry<Object, Object>> cursor = redisTemplate.opsForHash().scan(RedisKeyUtils.MAP_KEY_USER_LIKED, ScanOptions.NONE); while (cursor.hasNext()){ CommonAgree commonAgree = new CommonAgree(); Map.Entry<Object, Object> entry = cursor.next(); String key = (String) entry.getKey(); //分离出 topicId,toUserId String[] split = key.split("::"); //被点赞主题 String topicIdRedis = split[0]; String fromUserIdRedis = split[1]; Integer value = (Integer) entry.getValue(); //如果主题id 点赞用户id 以及 状态为1 则代表已经点赞 if (StringUtils.equals(fromUserId,fromUserIdRedis)){ commonAgree.setTopicId(topicIdRedis); commonAgree.setFromUid(fromUserIdRedis); commonAgree.setStatus(value); commonAgrees.add(commonAgree); } } return commonAgrees; } }
ICommonAgreeService
package org.jeecg.modules.agree.service; import org.jeecg.modules.agree.entity.CommonAgree; import com.baomidou.mybatisplus.extension.service.IService; import java.util.List; /** * @Description: 点赞表 * @Author: jeecg-boot * @Date: 2022-02-15 * @Version: V1.0 */ public interface ICommonAgreeService extends IService<CommonAgree> { /** * @Author: qiaochengqiang * @Date: 2022/2/17 * @Description: 通过点赞人和被点赞人查询是否存在点赞记录 **/ CommonAgree getByTopicIdAndFromUserId(String topicId, String fromUserId); /** * @Author: qiaochengqiang * @Date: 2022/2/17 * @Description: 通过点赞人和被点赞人查询是否存在点赞记录 **/ List<CommonAgree> getAllByTopicIdOrFromUserId(String topicId, String fromUserId); /** * @Author: qiaochengqiang * @Date: 2022/2/17 * @Description: 定时任务 将Redis里的点赞数据存入数据库中 **/ void transLikedFromRedis2DB(); }
CommonAgreeServiceImpl
package org.jeecg.modules.agree.service.impl; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import org.apache.commons.lang3.StringUtils; import org.jeecg.modules.agree.entity.CommonAgree; import org.jeecg.modules.agree.mapper.CommonAgreeMapper; import org.jeecg.modules.agree.service.ICommonAgreeService; import org.jeecg.modules.common.service.RedisService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import java.util.List; /** * @Description: 点赞表 * @Author: jeecg-boot * @Date: 2022-02-15 * @Version: V1.0 */ @Service public class CommonAgreeServiceImpl extends ServiceImpl<CommonAgreeMapper, CommonAgree> implements ICommonAgreeService { @Autowired RedisService redisService; @Override public CommonAgree getByTopicIdAndFromUserId(String topicId, String fromUserId) { QueryWrapper<CommonAgree> queryWrapper = new QueryWrapper<>(); queryWrapper.lambda().eq(CommonAgree::getFromUid,fromUserId); queryWrapper.lambda().eq(CommonAgree::getTopicId,topicId); return getOne(queryWrapper); } @Override public List<CommonAgree> getAllByTopicIdOrFromUserId(String topicId, String fromUserId) { QueryWrapper<CommonAgree> queryWrapper = new QueryWrapper<>(); if (topicId != null && StringUtils.isNotEmpty(topicId)){ queryWrapper.lambda().eq(CommonAgree::getTopicId,topicId); } if (fromUserId != null && StringUtils.isNotEmpty(fromUserId)){ queryWrapper.lambda().eq(CommonAgree::getFromUid,fromUserId); } return list(queryWrapper); } @Override public void transLikedFromRedis2DB() { //数据同步的时候是否需要查询所有数据 只同步当天的是否可以 List<CommonAgree> list = redisService.getLikedDataFromRedis(); for (CommonAgree commonAgree : list) { CommonAgree cg = getByTopicIdAndFromUserId(commonAgree.getTopicId(), commonAgree.getFromUid()); if (cg == null){ //没有记录,直接存入 save(commonAgree); }else{ //有记录,需要更新 cg.setStatus(commonAgree.getStatus()); save(cg); } } } }
CommonAgreeController
package org.jeecg.modules.agree.controller; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang3.StringUtils; import org.jeecg.common.api.vo.Result; import org.jeecg.common.system.query.QueryGenerator; import org.jeecg.common.util.oConvertUtils; import org.jeecg.modules.agree.entity.CommonAgree; import org.jeecg.modules.agree.service.ICommonAgreeService; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import lombok.extern.slf4j.Slf4j; import org.jeecg.modules.common.service.RedisService; import org.jeecg.modules.common.util.LikedStatusEnum; import org.jeecgframework.poi.excel.ExcelImportUtil; import org.jeecgframework.poi.excel.def.NormalExcelConstants; import org.jeecgframework.poi.excel.entity.ExportParams; import org.jeecgframework.poi.excel.entity.ImportParams; import org.jeecgframework.poi.excel.view.JeecgEntityExcelView; import org.jeecg.common.system.base.controller.JeecgController; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartHttpServletRequest; import org.springframework.web.servlet.ModelAndView; import com.alibaba.fastjson.JSON; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.jeecg.common.aspect.annotation.AutoLog; /** * @Description: 点赞表 * @Author: jeecg-boot * @Date: 2022-02-15 * @Version: V1.0 */ @Api(tags="点赞表") @RestController @RequestMapping("/common/commonAgree") @Slf4j public class CommonAgreeController extends JeecgController<CommonAgree, ICommonAgreeService> { @Autowired private ICommonAgreeService commonAgreeService; @Autowired private RedisService redisService; /** * 添加 * * @param commonAgree * @return */ @AutoLog(value = "点赞表-添加") @ApiOperation(value="点赞表-添加", notes="点赞表-添加") @PostMapping(value = "/add") public Result<?> add(@RequestBody CommonAgree commonAgree) { //点赞分两个步骤 首先保存到redis 定时计划保存到数据库中 //将数据保存到 redis 保存是会根据状态判断是点赞还是取消点赞 Integer status = commonAgree.getStatus(); //如果状态为null 或者状态为0 则属于点赞 if (status == null || LikedStatusEnum.UNLIKE.getCode() == status){ redisService.saveLiked2Redis(commonAgree.getTopicId(), commonAgree.getFromUid()); }else{ //取消点赞 redisService.unlikeFromRedis(commonAgree.getTopicId(), commonAgree.getFromUid()); } return Result.OK("点赞成功!"); } /** * @Author: qiaochengqiang * @Date: 2022/2/17 * @Description: 根据主题id和登录用户id查询是否已经点赞 **/ @AutoLog(value = "点赞表-通过id查询") @ApiOperation(value="点赞表-通过用户id和主题id查询", notes="点赞表-通过用户id和主题id查询") @GetMapping(value = "/queryAgree") public Result<?> queryAgree(@RequestParam(name="fromUid",required=true) String fromUid, @RequestParam(name="topicId",required=true) String topicId) { CommonAgree commonAgree = new CommonAgree(); commonAgree = redisService.checkIsAgreeFromRedis(topicId, fromUid); return Result.OK(commonAgree); } /** * @Author: qiaochengqiang * @Date: 2022/2/17 * @Description: 通过点赞id 被点赞主体id 以及被点赞用户id 进行查询 * 统计一共被点了多少赞 * 统计一共点了多少赞 **/ @AutoLog(value = "点赞表-通过id查询") @ApiOperation(value="点赞表-通过id查询", notes="点赞表-通过id查询") @GetMapping(value = "/queryAgreeByTopicIdOrFromUserId") public Result<?> queryAgreeByTopicIdOrFromUserId(@RequestParam(name="topicId",required=false) String topicId, @RequestParam(name="fromUserId",required=false) String fromUserId) { //按照主题id 查询统计总数 if (StringUtils.isEmpty(topicId) && StringUtils.isEmpty(fromUserId)){ return Result.error("参数不能都为空!"); } if (StringUtils.isNotEmpty(topicId) && StringUtils.isNotEmpty(fromUserId)){ return Result.error("参数不能都存在!"); } List<CommonAgree> commonAgrees = new ArrayList<>(); if (StringUtils.isNotEmpty(topicId)){ //查询redis List<CommonAgree> agreeFromRedisByTopicId = redisService.getAgreeFromRedisByTopicId(topicId); commonAgrees.addAll(agreeFromRedisByTopicId); } //按照用户 查询一共点赞多少 给谁点赞 if (StringUtils.isNotEmpty(fromUserId)){ List<CommonAgree> agreeFromRedisByFromUserId = redisService.getAgreeFromRedisByFromUserId(fromUserId); commonAgrees.addAll(agreeFromRedisByFromUserId); } return Result.OK(commonAgrees); }
}
然后定时保存数据库中即可。