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.获取粉丝idLong 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.返回idreturn 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 countString 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(时间戳)、offsetList<Long> ids = new ArrayList<>(typedTuples.size());long minTime = 0; // 2int os = 1; // 2for (ZSetOperations.TypedTuple<String> tuple : typedTuples) { // 5 4 4 2 2// 4.1.获取idids.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); }