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); }