如何使用Redis的Hash和quarzt任务框架来完成一个定时Redis持久化到数据库的点赞功能

655 阅读4分钟

前言

这篇文章是关于使用redis中的hash存储结构来保存点赞数据,并加入了定时任务框架quartz,让redis中的数据写入到mysql,这篇文章是对我上一篇实现点赞功能的优化和拓展,若有不足请大家指出。 (如何高效的使用java实现类似抖音的评论点赞列表(互动消息)以及点赞的基本操作 - 掘金 (juejin.cn))

本文借鉴了Redis如何高效实现点赞、取消点赞功能 - 掘金 (juejin.cn)这篇文章的思路,与自己的数据库和不同的需求结合完成。

为什么要使用redis实现点赞

由于点赞可能是用户频繁点击的操作,有时会发生误触等,就会多次发起请求,会增加mysql的压力。

实现功能

点赞时先把点赞数据存储到redis中,在每隔一段时间后把redis中的数据写入到mysql中,若用户查看自己的点赞列表或被点赞列表则直接执行该定时任务。

由于我认为点赞数据中应该有点赞时间这个字段,方便排序和展示,所以我在value中存了点赞状态和点赞时间两个状态,取出时需要进行字符串拆分,若有什么更好的方法请在评论区写出。

代码实现

因为我在数据库中没有点赞状态这个字段,数据库只会存储点赞的数据,未点赞或取消点赞等数据不会存储在数据库中,节约了资源。

数据库设计

数据库设计和实体类VO对象等在我的上一篇文章可以查看 (如何高效的使用java实现类似抖音的评论点赞列表(互动消息)以及点赞的基本操作 - 掘金 (juejin.cn))

edis实现类

redis中key和value的工具类行key拼接和value拆分)

public class RedisKeyUtils {

    /**
     * 评论缓存key
     */
    public static final String CACHE_COMMENT_LIKE = "COMMENT_LIKE";

    /**
     * 用户被点赞数量key
     */
    //public static final String CACHE_COMMENT_LIKE_COUNT = "COMMENT_LIKE_COUNT";
    public static final String LIKE = "1";

    public static final String UNLIKE = "0";

    /**
     * 拼接被点赞的用户id和点赞的人的id作为key。格式 222222::333333
     *
     * @param userId    用户id
     * @param commentId 评论id
     * @return {@link String}
     */
    public static String getLikedKey(Integer userId, Integer commentId){
        StringBuilder builder = new StringBuilder();
        builder.append(userId);
        builder.append("::");
        builder.append(commentId);
        return builder.toString();
    }

    /**
     * 拼接点赞状态和点赞时间作为value。格式 1::2023-6-24
     *
     * @param status 状态
     * @param time   时间
     * @return {@link String}
     */
    public static String getValue(String status, Timestamp time){
        StringBuilder builder = new StringBuilder();
        builder.append(status);
        builder.append("&");
        builder.append(time);
        return builder.toString();
    }
}

redis点赞

public void likes(Integer userId, Integer commentId) {
    String likedKey = RedisKeyUtils.getLikedKey(userId, commentId);
    Timestamp time = new Timestamp(System.currentTimeMillis());
    String value = RedisKeyUtils.getValue(LIKE,time);
    redisTemplate.opsForHash().put(CACHE_COMMENT_LIKE, likedKey, value);
}

redis取消点赞

public void unLikes(Integer userId, Integer commentId) {
    String likedKey = RedisKeyUtils.getLikedKey(userId, commentId);
    Timestamp time = new Timestamp(System.currentTimeMillis());
    String value = RedisKeyUtils.getValue(UNLIKE,time);
    redisTemplate.opsForHash().put(CACHE_COMMENT_LIKE, likedKey,value);
    commentMapper.updateLikeNum(commentId,-1);
}

存储redis缓存中数据并删除缓存

由于数据库中没有点赞状态这个字段,所以使用了map进行数据传输

public Map<Like,String> getLikedDataFromRedis() {
    Cursor<Map.Entry<Object, Object>> cursor = redisTemplate.opsForHash().scan(CACHE_COMMENT_LIKE, ScanOptions.NONE);
    Map<Like,String> likeStatusMap = new HashMap<>();
    while (cursor.hasNext()){
        Map.Entry<Object, Object> entry = cursor.next();
        String key = (String) entry.getKey();
        //分离出 userId,commentId
        String[] split = key.split("::");
        Integer userId = Integer.parseInt(split[0]);
        Integer commentId = Integer.parseInt(split[1]);
        String value = (String) entry.getValue() ;

        //把点赞数据存入likeStatusMap
        Timestamp time = new Timestamp(System.currentTimeMillis());
        Like like = new Like();
        like.setUserId(userId);
        like.setCommentId(commentId);
        like.setCreateTime(time);
        likeStatusMap.put(like,value);

        //存到 list 后从 Redis 中删除
        redisTemplate.opsForHash().delete(CACHE_COMMENT_LIKE, key);
    }
    return likeStatusMap;
}

mysql实现类

是否点赞

@Override
public boolean isLike(Integer userId, Integer commentId) {
    //生成评论点赞数据的key
    String LikeKey = RedisKeyUtils.getLikedKey(userId,commentId);
    //从redis中查看是否存在
    if(redisTemplate.opsForHash().hasKey(CACHE_COMMENT_LIKE,LikeKey)){
        String value = Objects.requireNonNull(redisTemplate.opsForHash().get(CACHE_COMMENT_LIKE, LikeKey)).toString();
        String status = redisService.getStatus(value)[0];
        return status.equals("1");
    }else {
        LambdaQueryWrapper<Like> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Like::getUserId,userId)
                .eq(Like::getCommentId,commentId);
        Long count = likeMapper.selectCount(queryWrapper);
        if (count>0){
            return true;
        } else{
            redisService.likes(userId,commentId);
            return false;
        }
    }
}

点赞或取消点赞

public boolean like(Integer userId, Integer commentId) {
    //生成评论点赞数据的key
    String LikeKey = RedisKeyUtils.getLikedKey(userId,commentId);
    //从redis中查看是否存在
    if(redisTemplate.opsForHash().hasKey(CACHE_COMMENT_LIKE,LikeKey)){
        String value = Objects.requireNonNull(redisTemplate.opsForHash().get(CACHE_COMMENT_LIKE, LikeKey)).toString();
        String status = redisService.getStatus(value)[0];
        if(status.equals("1")){
            redisService.unLikes(userId,commentId);
            return false;
        }
        if(status.equals("0")){
            redisService.likes(userId,commentId);
            return true;
        }
    }
    //redis中不存在该点赞数据,从数据库中查找
    LambdaQueryWrapper<Like> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(Like::getCommentId,commentId)
            .eq(Like::getUserId,userId);
    Like like = likeMapper.selectOne(queryWrapper);
    //判断点赞数据是否为空,若为空则没有点赞记录,添加点赞数据
    if(like==null){
        redisService.likes(userId,commentId);
        return true;
    }
    //点赞数据不为空,说明已经点赞,则进行取消点赞
    redisService.unLikes(userId,commentId);
    return false;
}

redis写入数据库

这里对不同的状态进行了多次判断,是由于我的数据库中没有点赞状态这个字段,数据库内只会存储是点赞的数据。

public void transLikedFromRedis2DB() {
    Map<Like,String> likedStatusMap = redisService.getLikedDataFromRedis();
    for (Map.Entry<Like,String> entry : likedStatusMap.entrySet()) {
        //获得点赞状态和点赞时间
        String status = redisService.getStatus(entry.getValue())[0];
        String time = redisService.getStatus(entry.getValue())[1];
        //查询数据库中数据
        LambdaQueryWrapper<Like> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Like::getCommentId,entry.getKey().getCommentId())
                .eq(Like::getUserId,entry.getKey().getUserId());
        Like like = likeMapper.selectOne(queryWrapper);
        if(status.equals("1")){
            //判断状态是点赞还是未点赞
            if (like!=null){
                //数据库内有数据
                if(entry.getKey().getCreateTime()!=like.getCreateTime()){
                    like.setCreateTime(Timestamp.valueOf(time));
                    likeMapper.updateById(like);
                }
            }else{
                //数据库内没有数据
                likeMapper.insert(entry.getKey());
            }
        }else {
            if (like!=null){
                //数据库内有数据
                likeMapper.deleteById(like.getLikeId());
            }
        }
    }
}

定时任务实现

quartz配置类

@Configuration
public class QuartzConfig {

    private static final String LIKE_TASK_IDENTITY = "LikeTaskQuartz";

    @Bean
    public JobDetail quartzDetail(){
        return JobBuilder.newJob(LikeTask.class).withIdentity(LIKE_TASK_IDENTITY).storeDurably().build();
    }

    @Bean
    public Trigger quartzTrigger(){
        SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule()
                .withIntervalInMinutes(5) //一分钟执行一次
                .repeatForever();
        return TriggerBuilder.newTrigger().forJob(quartzDetail())
                .withIdentity(LIKE_TASK_IDENTITY)
                .withSchedule(scheduleBuilder)
                .build();
    }
}

task任务

@Slf4j
public class LikeTask extends QuartzJobBean {

    @Resource
    LikeService likeService;

    private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    @Override
    protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {

        log.info("LikeTask-------- {}", sdf.format(new Date()));

        //将 Redis 里的点赞信息同步到数据库里
        likeService.transLikedFromRedis2DB();
    }
}

用户点赞列表和被点赞列表

用户点赞列表和被点赞列表与我的上一篇文章相同,但是在查询列表前加入了redis写入mysql的方法如何高效的使用java实现类似抖音的评论点赞列表(互动消息)以及点赞的基本操作 - 掘金 (juejin.cn)

//也就是在查询先执行该操作
//立即执行redis存入mysql的定时任务
transLikedFromRedis2DB();

总结

由于数据库内没有点赞状态这个字段,这段代码看起来更加复杂,大家有什么见解可以写在评论区。