Redis的zset实现共同关注、懒汉式关注推送

共同关注功能

@Override
    public Result common(Long id) {
        Long userId = UserHolder.getUser().getId();

        String key1="follow:"+id;
        String key2="follow:"+userId;
        //求两个用户关注的交集就是共同关注
        Set<String> intersect = stringRedisTemplate.opsForSet().intersect(key2, key1);
        if (intersect.isEmpty()||intersect==null){
            return Result.ok(Collections.emptyList());
        }
        List<Long> list = intersect.stream().map(Long::valueOf).collect(Collectors.toList());
        List<UserDTO> userdtos = userService.listByIds(list)
                .stream()
                .map(user -> BeanUtil.copyProperties(user, UserDTO.class))
                .collect(Collectors.toList());
        return Result.ok(userdtos);
    }

Feed流推送

关注推送又叫Feed流,直译为投喂,为用户持续的提供沉浸式体验,如抖音、快手这类短视频APP,通过无限下滑获取最新信息。

传统的推送模式是用户寻找内容,而Feed模式是内容匹配用户

  • 拉模式又叫读扩散
    粉丝从博主那里拉取:博主把自己的推文放到自己的发件箱,用户从多个博主那里拉取,但是每个博主发的推文时间是乱序的,那么用户拉取每个博主的推文后还要进行时间排序,比较耗时。
     
  • 推模式又叫写扩散
    博主推给粉丝:博主把写到的推文的id推给粉丝的收件箱,粉丝刷新后会消费到博主新的推文id,进而根据id加载出新推文,但是如果博主粉丝有千百万,那么要把推文id推给千百万个粉丝收件箱,仍然会比较耗时。

推模式实现关注推送功能 

Redis数据结构可以选用list或者zset,list是链表有下标,可以实现排序和分页,zset可以基于score进行排序,排序之后也可以实现分页查询,但是最终选取zset来实现,原因如下:

list要依赖角标,有重复读问题

参考下面的zset,我觉得list也可以利用lastId实现无重复读效果,只不过zset可以根据score排序并利用score的范围查询,就使用zset了

zset不依赖角标,依赖lastId,无重复读问题

zset的score排序后虽然有角标的效果,但我们是利用score值的范围查询来实现分页的,把score按从大到小排列,每次查询记住最小的score作为下次查询的最大的score,即 lastId

zset集合的元素并不是存进去就有序的,而是可以根据需要用score排序!
因此并不是集合最后一个元素就是最小的时间戳!

(详见 黑马点评 实战篇P9、P10)

修改新增Blog的代码

@Override
    public Result saveBlog(Blog blog) {
        UserDTO user = UserHolder.getUser();
        blog.setUserId(user.getId());
        // 保存探店博文
        boolean save = save(blog);
        if (save){
            //保存成功则把博文id发给粉丝,实现消息推送
            
            //查询该用户的粉丝
            List<Follow> followUsers = followService.query().eq("follow_user_id", user.getId()).list();
            
            //把blogId推给每个粉丝的收件箱,用zset集合实现,时间戳作为score,这样就可以根据时间戳进行排序
            for(Follow follow:followUsers){
                Long userId = follow.getUserId();
                String key="feed:"+userId;
                stringRedisTemplate.opsForZSet().add(key,blog.getId().toString(), System.currentTimeMillis());
            }
        }
        return Result.ok(blog.getId());
    }

滚动分页查询

偏移量offset
假设有一个有序集合,分数从高到低排序为 [70, 60, 50, 40, 30, 20, 10],每次查询的偏移量为上次查询结果中最小分数相同的元素的个数。如果上一次查询的结果是 [50, 40, 30],那么下一次查询的偏移量就是 3,即上一次查询结果中最小分数相同的元素的个数,这样可以确保不会重复返回已经获取过的元素。

Controller层接口

@GetMapping("/of/follow")
    public Result queryBlogOfFollow(
            @RequestParam("lastId") Long max
            ,@RequestParam(value = "offset" ,defaultValue = "0") Integer offset) {
        return blogService.queryBlogOfFollow(max, offset);
    }

BlogServiceImpl实现

@Override
    public Result queryBlogOfFollow(Long max, Integer offset) {
        //获取当前登录用户的id
        Long userId = UserHolder.getUser().getId();
        String key="feed:"+userId;
        //根据登录用户id去redis查询其收件箱得到推送的blog的id
        //详见黑马点评 实战篇p10


        Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate
                .opsForZSet()
                .reverseRangeByScoreWithScores(key, Double.valueOf(0), Double.valueOf(max), Long.valueOf(offset), Long.valueOf(2));
        if (typedTuples==null||typedTuples.isEmpty())
        {
            return Result.ok(Collections.emptyList());
        }

        ArrayList<Long> IDlist = new ArrayList<>(typedTuples.size());
        long minTime=0;
        int os=1;
        for (ZSetOperations.TypedTuple<String> tuple:typedTuples){
            //获取id
            String id = tuple.getValue();
            IDlist.add(Long.valueOf(id));
            long time = tuple.getScore().longValue();
            if (time==minTime){
                os++;//偏移量,每次偏移量都会变化,第一次偏移量默认是0,后面每次后端都要把查询后的偏移量传给前端
            }else {
                minTime=time;
                os=1;//重置
            }
        }
        String join = StrUtil.join(",", IDlist);
        List<Blog> blogList = query().in("id", IDlist).last("ORDER BY FIELD(id," + join + ")").list();
        for (Blog blog:blogList){
            //查询blog有关的用户信息
            Long id = blog.getUserId();
            User user = userService.getById(id);
            blog.setName(user.getNickName());
            blog.setIcon(user.getIcon());

            //判断该登录用户是否点赞
            Long loginUserId = UserHolder.getUser().getId();
            String k="blog:liked:"+id;
            Double score = stringRedisTemplate.opsForZSet().score(k, loginUserId.toString());
            blog.setIsLike(score!=null);

        }

        ScrollResult scrollResult = new ScrollResult();
        scrollResult.setList(blogList);
        scrollResult.setOffset(os);
        scrollResult.setMinTime(minTime);

        return Result.ok(scrollResult);
    }

ScrollResult.java 

@Data
public class ScrollResult {
    private List<?> list;
    //时间戳
    private Long minTime;
    //查询偏移量
    private Integer offset;
}

 

  • 32
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值