好友关注
要点:
- Feed流的使用
1.1 关注和取关
1.1.1 需求

基于该表结构,需要实现两个接口:
关注和取关接口
判断是否关注的接口
关注是User之间关系,博主和粉丝是多对多的关系,一个博主可以有多个粉丝,一个粉丝也可以关注多个博主,所以抽离出来一张 tb_follow 表来表示:

1.2 共同关注
代码:
@Override
public Result followCommons(Long id) {
// 1. 获取当前登录用户ID
UserDTO dto = UserHolder.getUser();
if (dto == null) {
return Result.fail("请先登录");
}
Long userId = dto.getId();
// 2. 使用 Set 求博主和当前登录用户关注的共同用户
String bloggerKey = "follow:" + id;
String userKey = "follow:" + userId;
Set<String> intersect = stringRedisTemplate.opsForSet().intersect(bloggerKey, userKey);
if (intersect == null || intersect.isEmpty()) {
return Result.ok(Collections.emptyList());
}
List<Long> idList = intersect.stream().map(Long::valueOf).collect(Collectors.toList());
List<UserDTO> dtoList = userService.listByIds(idList)
.stream()
.map(user -> BeanUtil.copyProperties(user, UserDTO.class))
.collect(Collectors.toList());
return Result.ok(dtoList);
}1.3 关注推送
1.3.1 背景
关注推送也叫做Feed流,直译为投喂,为用户持续提供“沉浸式”的体验,通过无限下拉刷新获取新消息。

Feed流模式

Feed流实现方案
拉模式:也叫读扩散,用户拉取Feed流,优点是节省内存空间,缺点是延迟高,时间换空间

推模式:也叫写扩散,将Feed流写到关联用户的收件箱中,优点是延迟低,缺点是重复写占用空间,空间换时间

推拉结合模式:大致原则是优化用户的体验,达到延迟低的效果,对于普通用户发的消息,直接使用推模式,此类用户占用内存较少;对于大V发的消息,对大V的活跃粉丝使用推模式,对大V的普通粉丝使用拉模式。

三种模式对比

本例使用推模式。
1.3.2 代码实现
需求

实现思路
Feed流分页查询 传统分页是指基于角标访问,如 ZSet Range函数,在数据插入时,数据的角标会有变化,导致分页困难

滚动查询实现基本思路
ZREVRANGEBYSCORE
keymaxmin[WITHSCORES][LIMIT offset count]:根据分数范围查询集合,max为分数最大值,min为分数最小值,降序排列
先执行第一条命令,1000改为当前时间戳,查询出时间最靠前的几条记录,然后记录当前查询记录中的最小值,以该条记录为基准,再进行下一次的查询
滚动查询的参数问题

count是与前端约定好了的,offset的值取决于你在“当前这页结果”中,查到了几个与最小值重复的元素,并且要加一个判断逻辑:
如果本次查询的最小分数 == 上次的分数,则
nextOffset = lastOffset + count;如果分数变了,则
nextOffset = count。
(这里的 count 是指本次结果中与最小分数相同的元素个数)
具体实现
- 接口信息:

代码:
javapublic Result queryBlogOfFollow(Long max, Integer offset) { // 1. 获取当前用户 UserDTO dto = UserHolder.getUser(); if (dto == null) { return Result.fail("请先登录!"); } Long userId = dto.getId(); // 2. 查询收件箱 String key = FEED_KEY + userId; // 3. ZREVRANGEBYSCORE key max 0 WITHSCORES LIMIT offset 3 Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet().reverseRangeByScoreWithScores(key, 0, max, offset, 2); if (typedTuples == null || typedTuples.isEmpty()) { return Result.ok(); } // 4. 解析数据 blogId 时间戳 List<Long> blogs = new ArrayList<>(typedTuples.size()); long minTime = 0L; int offsetIndex = 1; for (ZSetOperations.TypedTuple<String> typedTuple : typedTuples) { blogs.add(Long.valueOf(typedTuple.getValue())); // 时间戳 long time = typedTuple.getScore().longValue(); if (minTime == time) { // 如果和最小值时间戳重复,偏移量 + 1 offsetIndex++; } else { // 不一样说明当前时间戳不是最小值,偏移量重新数 minTime = time; offsetIndex = 1; } } if (minTime == max){ offsetIndex += offset; } // 5. 根据获得的 blogId 查询博客 String idStr = StrUtil.join(",", blogs); List<Blog> blogList = query().in("id", blogs).last("ORDER BY FIELD(id," + idStr + ")").list(); blogList.forEach(blog -> { // 5.1 查询博客用户信息 queryBlogUser(blog); // 5.2 查询博客是否被点赞 isBlogLiked(blog); }); // 6. 封装结果返回 ScrollResult scrollResult = new ScrollResult(); scrollResult.setOffset(offsetIndex); scrollResult.setList(blogList); scrollResult.setMinTime(minTime); return Result.ok(scrollResult); }