Redis实现数据推送

ZSet

技术背景:

就是要实现有数据变动的分页查询,比如说,每次查询3个,刚开始查询到7,6,5后又插进来一个数8,那么按以往的分页查询page = page * page_size = 1 * 3 = 3 确定起始位置,下次查询从第4个开始查询,而新插进来一个数,此时的第四个就是旧数据的第三个。会出现重复。

改进办法是记住上一次查询后的最小值,按上面的例子这个最小值就是5,之后查询就查比5小的3个数。这样就解决了插入新数据导致的重复查询的问题。

利用技术:Redis中的有序集合(Sorted Set),结合了集合(Set)和哈希表(Hash)的特性:成员是唯一的,但每个成员都会关联一个双精度浮点数的分数(score),Redis 会根据这些分数对成员进行排序。 这里就用时间作为分数。

对应Spring集成的Redis代码:

//添加笔记到粉丝的收件箱:
// Redis: ZADD key score member [score member ...]
stringRedisTemplate.opsForZSet().add(key, blog.getId().toString(), System.currentTimeMillis());
​
//查询 ->反向查询,因为默认是从小到大查询:
// Redis: ZREVRANGEBYSCORE key max min LIMIT offset count WITHSCORES
Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate
    .opsForZSet()
    .reverseRangeByScoreWithScores(key, 0, max, offset, 2);

Feed流:

1、智能排序(抖音推荐)

2、Timeline实现:

拉模式:也叫做读扩散,当张三和李四发了消息后,都会保存在自己的邮箱中,假设赵六要读取信息,那么他会从读取他自己的收件箱,此时系统会从他关注的人群中,把他关注人的信息全部都进行拉取到自己的收件箱,然后在进行排序

推模式:也叫做写扩散,推模式是没有写邮箱,当张三写了一个内容,此时会主动的把张三写的内容发送到他的每个粉丝收件箱中去,假设此时李四再来读取,就不用再去临时拉取了。时效快,不用临时拉取,数据冗余内存压力大。

推拉结合模式:也叫做读写混合,兼具推和拉两种模式的优点。如果是个普通的人,那么我们采用写扩散的方式,直接把数据写入到他的粉丝中去。

如果是大V,则分情况,如果是活跃粉丝,就采用推模式,直接发送到粉丝收件箱里。而如果是普通的粉丝,上线不是很频繁,所以等他们上线时,再从发件箱里边去拉信息。

建仓:保存传送探店笔记到数据库后,获得到当前笔记作者的所有粉丝,并以每个粉丝的Id建立一个Redis键->key = userId。其值为文章的id; 并以当前时间作为排序的权值。(最新的数据会在最上面 - > 从大到小查询)

@Override
public Result saveBlog(Blog blog) {
    // 1.获取登录用户
    UserDTO user = UserHolder.getUser();
    blog.setUserId(user.getId());
    // 2.保存探店笔记
    boolean isSuccess = save(blog);
    if(!isSuccess){
        return Result.fail("新增笔记失败!");
    }
    // 3.查询笔记作者的所有粉丝 select * from tb_follow where follow_user_id = ?
    List<Follow> follows = followService.query().eq("follow_user_id", user.getId()).list();
    // 4.推送笔记id给所有粉丝
    for (Follow follow : follows) {
        // 4.1.获取粉丝id
        Long userId = follow.getUserId();
        // 4.2.推送 (以每个粉丝的Id建立一个Redis键,key = userId;其值为文章的id,value = blog.id; )
        String key = FEED_KEY + userId;
        stringRedisTemplate.opsForZSet().add(key, blog.getId().toString(), System.currentTimeMillis());
    }
    // 5.返回id
    return Result.ok(blog.getId());
}

在关注列表取值时要完成: 一:每次取值要拿到本次查询数据的最小时间戳作为下一次查询条件;

二: 本次查询出重复元素个数作为偏移量,下次查询时跳过这些数据。 因此设计出实体类:

@Data
public class ScrollResult {
    private List<?> list;
    private Long minTime;
    private Integer offset;
}

取出数据:

@GetMapping("/of/follow")
// 按范围查询:传入上传查询出的最小时间戳作为本次查询的最大值范围。 max
//           传入上次查询出重复元素的个数作为偏移量,就是查询的开始跳过几条数据 offset 查询第一页时默认为 0
public Result queryBlogOfFollow(
    @RequestParam("lastId") Long max, @RequestParam(value = "offset", defaultValue = "0") Integer offset){
    return blogService.queryBlogOfFollow(max, offset);
}
​
@Override
public Result queryBlogOfFollow(Long max, Integer offset) {
    // 1.获取当前用户
    Long userId = UserHolder.getUser().getId();
    // 2.查询收件箱 ZREVRANGEBYSCORE key Max Min LIMIT offset count
    String key = FEED_KEY + userId;
    Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate
        .opsForZSet()
        //reverseRangeByScoreWithScores: 从有序集合里反向查询得分处于特定区间的数据,并且返回带有得分的结果集。
        //key:要查询的有序集合的键名。
        //0:查询的最小得分。
        //max:查询的最大得分。
        //offset:查询结果的偏移量,也就是从第几个元素开始返回。
        //2:要返回的元素数量。
        .reverseRangeByScoreWithScores(key, 0, max, offset, 2);
 
    
    // 3.非空判断
    if (typedTuples == null || typedTuples.isEmpty()) {
        return Result.ok();
    }
    
    // 4.解析数据:blogId、minTime(时间戳)、offset
    List<Long> ids = new ArrayList<>(typedTuples.size());
    long minTime = 0; // 2
    int os = 1; // 2
    for (ZSetOperations.TypedTuple<String> tuple : typedTuples) { // 5 4 4 2 2
        // 4.1.获取id
        ids.add(Long.valueOf(tuple.getValue()));
        // 4.2.获取分数(时间戳)
        long time = tuple.getScore().longValue();
        if(time == minTime){
            os++;
        }else{
            minTime = time;
            os = 1;
        }
    }
    os = minTime == max ? os : os + offset;
    
    // 5.根据id查询blog 保证以上面的id顺序查出结果
    String idStr = StrUtil.join(",", ids);
    List<Blog> blogs = query().in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list();
​
    for (Blog blog : blogs) {
        // 5.1.查询blog有关的用户
        queryBlogUser(blog);
        // 5.2.查询blog是否被点赞
        isBlogLiked(blog);
    }
​
    // 6.封装并返回
    ScrollResult r = new ScrollResult();
    r.setList(blogs);
    r.setOffset(os);
    r.setMinTime(minTime);
​
    return Result.ok(r);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值