Feed流实现信息推送

2.8k 词

什么是Feed流?

关注推送也叫做Feed流,直译为投喂。为用户持续的提供“沉浸式”的体验,通过无限下拉刷新获取新的信息。

Feed流和传统模式的区别如下
Feed流对比传统模式
Feed流主要有三种实现方案

  • 拉模式
  • 推模式
  • 推拉结合

拉模式

又叫做读扩散,在用户需要读取消息时,主动从发件箱拉取消息,然后把获取到的消息进行排序,并呈现给用户。流程如图:
拉模式
优点:消息只用保存一份,节约内存
缺点:大量消息被拉去的话,用户的性能消耗较大

推模式

又叫做写扩散,用户发布消息后,就直接推送到粉丝的收件箱里。流程如图:
推模式
优点:延时低,用户可以很快以较低成本获取到信息
缺点:对于内存的负担大,因为一份消息需要被存储很多份

推拉结合模式

又叫做读写混合,兼具两种模式的优点。对于普通粉丝,采用拉模式;对于活跃粉丝,采用推模式。如图所示:
推拉结合

三种模式对比

拉模式 推模式 推拉结合
写比例
读比例
用户读取延迟
实现难度 复杂 简单 很复杂
使用场景 很少使用 用户量少、没有大V 过千万的用户量,有大V

基于Redis实现推模式

需求

  • 在保存blog到数据库的同时,推送到粉丝的收件箱
  • 收件箱满足可以根据时间戳排序
  • 查询收件箱数据时,可以分页查询

推送的具体实现

用户发布博客后,在保存到数据库的同时,保存到Redis中的ZSET中,key是每个粉丝的id。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Override
@Transactional
public Long saveBlog(Blog blog) {
// 获取登录用户
UserDTO user = UserHolder.getUser();
blog.setUserId(user.getId());
// 保存探店博文
boolean success = save(blog);
if (!success) {
throw new MySqlException(MessageConstant.BLOG_FAILED);
}
// 推送给粉丝
// 获取粉丝
List<Follow> follows = followService.lambdaQuery().eq(Follow::getFollowUserId, user.getId()).list();
// 推送
follows.forEach(follow -> {
String key = RedisConstants.FEED_KEY + follow.getUserId();
redisTemplate.opsForZSet().add(key, blog.getId().toString(), System.currentTimeMillis());
});
// 返回id
return blog.getId();
}

Feed流的分页实现

Feed流中的数据会不断更新,所以数据的角标也在变化,因此不能采用传统的分页模式。所以我们在这里采用滚动分页来实现分页查询。
滚动分页实现
滚动分页查询需要的Redis命令

1
ZREVRANGEBYSCORE key max min WITHSCORES LIMIT offset count

参数含义:

  • key:ZSET的键名
  • max:第一页为当前时间戳,后面的查询为上次查询的最小时间戳
  • min:0
  • offset:第一页为0,后面是上次查询时与最小值一样的元素个数
  • count:每一页的数据条数

基于这条指令,实现业务代码。
先创建一个返回数据的DTO

1
2
3
4
5
public class ScrollResult {
private List<?> list;
private Long minTime;
private Integer offset;
}

然后按照以下步骤实现业务逻辑:

  1. 获取当前用户
  2. 查询收件箱
  3. 解析数据:blogId,minTime,offset
  4. 根据blogId查询博客
  5. 封装并返回

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@Override
public ScrollResult queryBlogOfFollow(Long max, Integer offset) {
// 获取当前用户
Long userId = UserHolder.getUser().getId();
// 查询收件箱
String key = RedisConstants.FEED_KEY + userId;
Set<ZSetOperations.TypedTuple<String>> typedTuples =
redisTemplate.opsForZSet().reverseRangeByScoreWithScores(
key, 0, max, offset, RedisConstants.BLOG_COUNT);
// 解析数据
if (typedTuples == null || typedTuples.isEmpty()) {
return null;
}
long minTime = System.currentTimeMillis();
int nextOffset = 0;
List<Long> ids = new ArrayList<>(typedTuples.size());
for (ZSetOperations.TypedTuple<String> tuple : typedTuples){
ids.add(Long.valueOf(tuple.getValue()));
long time = tuple.getScore().longValue();
if (minTime == time) {
nextOffset++;
}else {
minTime = time;
nextOffset = 0;
}
}
// 根据id查询blog
String idStr = StrUtil.join(",", ids);
List<Blog> blogs = lambdaQuery()
.in(Blog::getId, ids)
.last("order by field(id, " + idStr + ")")
.list();
blogs.forEach(blog -> {
setIsLiked(blog);
queryBlogUser(blog);
});
// 封装并返回
ScrollResult scrollResult = ScrollResult.builder()
.minTime(minTime)
.offset(nextOffset)
.list(blogs)
.build();
return scrollResult;
}

以上,就是Feed流的推模式实现。以后可能会改进为推拉结合。