开发者

Redis如何使用zset处理排行榜和计数问题

开发者 https://www.devze.com 2025-04-01 10:39 出处:网络 作者: 记得开心一点嘛
目录Redis使用zset处理排行榜和计数业务逻辑ZSET 数据结构优化高并发的点赞操作ZSET 结构设计总结Redis使用zset处理排行榜和计数
目录
  • Redis使用zset处理排行榜和计数
    • 业务逻辑
      • ZSET 数据结构
      • 优化高并发的点赞操作
      • ZSET 结构设计
  • 总结

    Redis使用zset处理排行榜和计数

    在处理计数业务时,我们一般会使用一个数据结构,既是集合又可以保证唯一性,所以我们会选择Redis中的set集合:

    业务逻辑

    用户点击点赞按钮,需要再set集合内判断是否已点赞,未点赞则需要将点赞数+1并保存用户信息到集合中,已点赞则需要将数据库点赞数-1并移除set集合中的用户。

    @Service
    public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {
    
        @Autowired
        private IUserService userService;
        @Resource
        private StringRedisTemplate stringRedisTemplate;
    
        @Override
        public Result likeBlog(Long id) {
            // 获取登录用户
            Long userId = UserHolder.getUser().getId();
            // 判断当前登录用户是否已经点赞
            String key = "blog:like:" + id;
            Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());
            if(BooleanUtil.isFalse(isMember)){
                // 未点赞
                // 数据库点赞数+1
                boolean isSuccess = update().setSql("like = like + 1").eq("id",id).update();
                // 保存用户到Redis集合中
                if(isSuccess){
                    stringRedisTemplate.opsForSet().add(key, userId.toString());
                }
            } else {
                // 已点赞,取消点赞
                // 数据库点赞数-1
                boolean isSuccess = update().setSql("like = like - 1").eq("id",id).update();
                // 移除set集合中的用户
                stringRedisTemplate.opsForSet().remove(key, userId.toString());
            }
            return Result.ok();
        }
    }

    那么我们想要实现按照点赞时间的先后顺序排序,返回Top5的用户,这个时候set无法保证数据有序,所以我们需要换一个数据结构满足业务需求:

    Redis如何使用zset处理排行榜和计数问题

    Redis 的 ZSEphpT(有序集合) 是一个非常适合用于处理 排行榜计数问题 的数据结构。

    在高并发的点赞业务中,使用 ZSET 可以帮助我们高效地管理点赞的排名,并且由于 ZSET 的排序特性,我们可以轻松实现根据点赞数实时排序的功能。

    ZSET 数据结构

    Redis 的 ZSET 是一个集合,它的每个元素都会关联一个 分数(score),这个分数决定了元素在集合中的排序。ZSET 保证集合中的元素是按分数排序的,并且可以在 O(log(N)) 的时间复杂度内进行添加、删除和查找操作

    在高并发的点赞业务中,ZSET 可以帮助我们轻松地进行以下几项操作:

    • 记录每个用户对某个内容(如文章、评论等)的点赞数
    • 通过分数进行实时排序,获取点赞数最多的内容

    优化高并发的点赞操作

    高并发情况下,当多个用户同时对某个内容进行点赞时,我们需要高效地python更新该内容的点赞数,并保证数据一致性。ZSET 提供了很好的支持,具体步骤如下:

    • 用户点赞操作:使用 ZINCRBY 命令来对某个元素的分数进行增量操作,表示对该内容的点赞数增加。
    • 查看点赞数:可以通过 ZSCORE 命令获取某个内容的当前点赞数。
    • 查看排行榜:使用 ZRANGEZREVRANGE 命令来获取点赞数排名前 N 的内容,按分数进行排序。

    ZSET 结构设计

    • key:表示某个内容的点赞的 id。
    • value:表示点赞用户的 id。
    • score:根据点赞时间排序。

    下面是修改后的点赞逻辑:

    @Service
    public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {
    
        @Autowired
        private IUserService userService;
        @Resource
        private StringRedisTemplate stringRedisTemplate;
    
        @Override
        public Result likeBlog(Long id) {
            // 获取登录用户
            Long userId = UserHolder.getUser().getId();
            // 判断当前登录用户是否已经点赞
            String key = "blog:likerOrIEcWUa:" +android id;
            Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());
            if(score == null){
                // 未点赞
                // 数据库点赞数+1
                boolean isSuccess = update().setSql("like = like + 1").eq("id",id).update();
                // 保存用户到Redis集合中
                if(isSuccess){
                    stringRedisTemplate.opsForZSet().add(key, userId.toString(), System.currentTimeMillis());
                }
            } else {
                // 已点赞,取消点赞
                // 数据库点赞数-1
                boolean isSuccess = update().setSql("like = like - 1").eq("id",id).update();
                // 移除set集合中的用户
                stringRedisTemplate.opsForZSet().remove(key, userId.toString());
            }
            return Result.ok();
        }
    }

    而点赞排行榜代码如下:

    @Service
    public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {
    
        @Autowired
        private IUserService userService;
        @Resource
        private StringRedisTemplate stringRedisTemplate;
    
        @Override
        public Result queryBlogLikes(Long id) {
            String key = "blog:like:" + id;
            // 查询top5的点赞用户 zrange key 0 4
            Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4);
            if (top5 == null || top5.isEmpty()) {
                return Result.ok(Collections.emptyList());
            }
            // 解析出集合中的用户的id
            List<Long> ids = top5.stream().map(Long::valueOf).collect(Collectors.toList());
            // 根据id查询用户,并将类型由User转为UserDTO,随后转换为List集合
            String idStr = StrUtil.join(",",ids);
    //        List<UserDTO> userDTOs = userService.listByIds(ids).stream()
    //                .map(user -> BeanUtil.copyProperties(user, UserDTO.class))
    //                .collect(Collectors.toList());
            List<UserDTO> userDTOs = userService.query()
                    .in("id",ids).last("order by field(id," + idStr +")").list()
                    .stream()
                    .map(user -> BeanUtil.copyProperties(user, UserDTO.class))
                    .collect(Collectors.toList());
            return Result.ok(userDTOs);
        }
    }

    使用

    userService.query().in("id", ids).last("or编程客栈der by field(id," + idStr + ")") 

    来查询用户信息,并且使用 order by field(id, ...) 语句来保证查询结果的顺序与 top5 中的用户顺序一致。

    这里的 order by field(id, ...) 是关键,它确保了从数据库返回的数据顺序和 Redis 返回的 top5 用户顺序完全匹配。因为 Redis 中的 ZSet 是有顺序的,top5 会按照点赞数量进行排序。

    如果直接使用 listByIds 方法,可能会导致结果顺序不一致。

    总结

    以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程客栈(www.devze.com)。

    0

    精彩评论

    暂无评论...
    验证码 换一张
    取 消

    关注公众号