一,关注和取关
基于该表结构,实现两个接口:
- 关注和取关接口
- 判断是否关注的接口
关注是 User 之间的关系,是博主与粉丝的关系,数据库中有一张 tb_floow 表来标识
| 字段 | 说明 |
|---|---|
| id | 编号 |
| user_id | 用户id |
| follow_user_id | 关联的用户id |
| create_time | 创建时间 |
关注和取关接口:
public Result follow(Long followUserId,Boolean isFollow){
//获取登录用户
Long userId=UserHolder.getUser().getId();
//1.判断到底是关注还是取关
if(isFollow){
//2.关注,新增数据
Follow follow=new Follow();
follow.setUserId(userId);
follow.setFollowUserId(followUserId);
save(follow);
}else{
//3.取关,删除数据
QueryWrapper<Follow> wrapper=new QueryWrapper<Follow>();
wrapper.eq("user_id",userId).eq("follow_user_id",followUserId);
remove(wrapper);
}
return Result.ok();
}
判断是否关注的接口
public Result isFollow(Long followUserId){
//获取登录用户
Long userId=UserHolder.getUser().getId();
//1. 查询是否关注
Integer count=query().eq("user_id",userId).eq("follow_user_id",followUserId).count();
return Result.ok(count>0);
}
二,共同关注
-
改造关注和取关接口,将数据同步到 Redis 中
public Result follow(Long followUserId,Boolean isFollow){ //获取登录用户 Long userId=UserHolder.getUser().getId(); //1.判断到底是关注还是取关 if(isFollow){ //2.关注,新增数据 Follow follow=new Follow(); follow.setUserId(userId); follow.setFollowUserId(followUserId); boolean isSuccess=save(follow); if(isSuccess){ //把关注用户的id,放入redis的set集合 String key="follows:"+userId; stringRedisTemplate.opsForSet().add(key,followUserId.toString()); } }else{ //3.取关,删除数据 QueryWrapper<Follow> wrapper=new QueryWrapper<Follow>(); wrapper.eq("user_id",userId).eq("follow_user_id",followUserId); boolean isSuccess=remove(wrapper); if(isSuccess){ //把取关的用户id删除 String key="follows:"+userId; stringRedisTemplate.opsForSet().remove(key,followUserId.toString()); } } return Result.ok(); } -
使用 Set 数据结构,实现共同关注接口
public Result followCommons(Long id){ //1.获取当前登录用户 Long userId=UserHolder.getUser().getId(); String key1="follows:"+userId; //2.求交集 String key2="follows:"+id; Set<String> userIds=stringRedisTemplate.opsForSet().intersect(key1,key2); if(userIds==null){ //无交集 return Result.ok(Collections.emptyList()); } //3.解析id集合 List<Long> ids=userIds.stream().map(Long::valueOf).collect(Collectors.toList()); //4.查询用户 List<User> users=userService.listByIds(ids); //5.返回 return Result.ok(users); }
三,关注推送
关注推送也叫 Feed 流,直译为投喂。为用户持续的提供沉浸式的体验,通过无限下拉刷新获取新的信息。

3.1 Feed流的模式
Feed 流产品有两种常见模式:
- Timeline:不做内容筛选,简单的按照内容发布时间排序,常用于好友或关注,例如朋友圈
- 优点:信息全面,不会缺失,实现简单
- 缺点:信息噪音较多,用户不一定感兴趣,内容获取效率低
- 智能排序:利用智能算法屏蔽违规的,用户不感兴趣的内容。推送用户感兴趣的信息来吸引用户。
- 优点:投喂用户感兴趣信息,用户粘性很高,容易沉迷
- 缺点:如果算法不准确,可能起到反作用。
3.2 Timeline模式实现方案
-
拉模式:
博主将消息发送到发件箱,并且标注了时间戳,粉丝只有读取消息的时候才会从他关注的博主的发件箱中接受消息到收件箱,根据时间戳排序,当不读取的时候下次接着去拉取消息。实际上收件箱是不存在的,每次都是从所关注的博主的发件箱去查询消息并排序的。

优点:节省内存空间,消息只保存一份(只保存发件箱)
缺点:每次读取都要重新拉取消息,如果消息过多会导致延迟过大。
-
推模式:
当博主要发送消息的时候,这个消息会将消息推送到他的所有粉丝的收件箱里面,粉丝读取消息只需要直接读取自己的收件箱消息即可。

优点:延迟很低
缺点:内存占用较高,每次发消息都要发给所有的粉丝。
-
推拉模式
发信息的人分为:大 V 和普通人,收消息的粉丝分为:普通粉丝和活跃粉丝
- 普通人因为粉丝少,使用推模式,直接将消息推送给每一个粉丝。
- 大V粉丝多,根据粉丝的类型采用不同模式
- 活跃粉丝采用推模式
- 普通粉丝采用拉模式

三者对比:

3.3 基于推模式实现关注推送功能
- Feed流的分页问题

- Feed流的滚动分页

基于 SortedSet 实现关注推送
-
在保存 blog 的时候将消息推送给粉丝
public Result saveBlog(Blog blog){ //获取登录用户 UserDto user=UserHolder.getUser(); blog.setUserId(user.getId()); //保存探店博文 boolean isSuccess=save(blog); if(!isSuccess){ return Result.fail("新增笔记失败"); } //查询笔记作者的所有粉丝select * from tb_follow where follow_user_id=? List<Follow> follows=followService.query().eq("follow_user_id",user.getId()).list(); //推送笔记id给所有粉丝 for(Follow follow:follows){ //获取粉丝id Long userId=follow.getUserId(); //推送(就是推送给每个粉丝的sortedSet里面) String key="feed:"+userId; stringRedisTemplate.opsForSet().add(key,blog.getId().toString(),System.currentTimeMillis()); } //返回id return Result.ok(blog.getId()); } -
滚动分页查询收件箱
滚动查询使用命令:
ZRevRangeByScore key min max WithScore Limit offerset count参数:
- max:
- 如果是第一次查询,那么是当前时间戳
- 如果不是第一次查询,那么是上一次查询的时间戳最小值
- min:0(时间戳最小值为0)
- offset:
- 如果是第一次查询,就是0
- 如果不是第一次查询,值取决于上一次查询的结果,为上一次查询的时间戳的值的相同最小值的个数。
- count:3(一次查3条就是3)
- max:

定义通用滚动分页查询请求对象
@Data
public class ScrollResult {
private List<?> list;
private Long minTime;
private Integer offset;
}
定义接口
@GetMapping("/of/follow")
public Result queryBlogOfFollow(
@RequestParam("lastId")Long max,@RequestParam(value="offset",defaultValue = "0")Integer offset ) {
return blogService.queryBlogOfFollow(max,offset);
}
定义业务层实现
public Result queryBlogOfFollow(Long max,Integer offset){
//1.获取当前用户
Long userId=UserHolder.getUser().getId();
//2.查询收件箱ZRevRangeByScore key min max WithScore Limit offerset count
String key="feed:"+userId;
Set<ZSetOperations.TypeTuple<String>> typeTuples=stringRedisTemplate.opsForSet()
.reverseRangeByScoreWithScores(key,0,max,offset,3);
if(typeTuples == null || typeTuples.isEmpty()){
return Result.ok();
}
//3.解析数据获得:blogId,minTime(最小时间戳),offSet(相同最小时间戳个数)
List<Long> ids=new ArrayList<>(typeTuples.size());
long minTime=0;
int os=1;
for(ZSetOperations.TypedTuple<String> tuple : typedTuples){
//获取id
String idStr=tuple.getValue();
ids.add(Long.valueOf(idStr));
//获取score(时间戳)
minTime=tuple.getScore().longValue();
if(time == minTime){
oss++;
}else{
minTime=time;
os=1;
}
}
//4.根据id查询blog(注意in里面要保证顺序和我们给的顺序一致)
String idStr=StrUtil.join(",",ids);
List<Blog> blogs=query()
.in("id",ids).last("Order by field(id,"+idStr+")").list();
for(Blog blog : blogs ){
//查询blog有关用户
queryBlogUser(blog);
//查询blog是否被点赞
isBlogLiked(blog);
}
//5.封装并返回
ScrollResult r=new ScrollResult();
r.setList(blogs);
r.setOffset(os);
r.setMinTime(minTime);
return Result.ok(r);
}