Feed流
- Feed 流:是提供给用户的内容流,为用户持续的提供 “沉浸式” 的体验,通过无限下拉刷新获取新的信息。比如微博的关注页,抖音的关注页视频都叫Feed流
- Feed:Feed流中的一条信息,比如朋友发布的一条朋友圈
redis实现Feed推送
相关数据结构
- SET:redis中的set可以将元素去重
- ZSET:与set一样将元素去重,但是支持排序,新增数据时要指定一个score,默认按照score排序。
Feed 流产品有两种常见模式
-
Timeline:不做内容筛选,简单的按照内容发布时间排序,常用于好友或关注产生的内容。
例如朋友圈
优点:信息全面,不会有缺失。并且实现也相对简单 缺点:信息噪音较多,用户不一定感兴趣,内容获取效率低
-
智能排序:利用算法推送用户感兴趣信息来吸引用户
例如抖音,快手
优点:投喂用户感兴趣信息,用户粘度很高,容易沉迷 缺点:如果算法不精准,可能起到反作用
Feed流的实现方案
feed流程实现的方案主要有三种模式:推模式、拉模式、推拉结合
-
拉模式:也叫做读扩散 这种模式下只有每次读的时候才获取消息,内存消耗小。缺点是读操作过于频繁,若用户关注了许多博主,一次要读的消息也是十分多,造成延迟较高
-
推模式:也叫做写扩散。 这种模式下在发消息时写入粉丝收件箱,内存占用更高。缺点是写操作频繁,如果有大v,需要给很多粉丝写消息
-
推拉结合模式:也叫做读写混合 这种模式兼具推和拉两种模式的优点。对于不同博主采取不同的策略,缺点是实现复杂 普通博主:可以采用推模式,写操作并不是很繁重 大v:其粉丝分两种粉丝,一种是活跃用户,一种是普通用户;一般而言活跃用户数量少,可以采用推模式;普通用户数量多,但它们查看消息少、频率低,采用拉模式。
在本文中采取写扩散作为案例。redis中实现feed流需要使用zset,当博主发布一条动态时往粉丝的收件箱(redis的zset)写一条数据。数据格式为:
{
score:一般为时间戳,
member:消息内容
}
具体实现代码 将消息写入粉丝收信箱
fans, err := mysql.GetFansByID(h.Context, u)
if err != nil {
return nil, err
}
for _, fan := range fans {
key := constants.FEED_KEY + strconv.FormatInt(fan.ID, 10)
err = redis.RedisClient.ZAdd(h.Context, key, &redis2.Z{
Score: float64(time.Now().Unix()),
Member: req.ID,
}).Err()
}
粉丝读取收信箱
u := utils.GetUser(h.Context).GetID()
key := constants.FEED_KEY + strconv.FormatInt(u, 10)
zSet, err := redis.GetBlogsByKey(h.Context, key, req.LastId, req.Offset)
var bids []string
for _, z := range zSet {
bids = append(bids, z.Member.(string))
}
var blogs []*blog.Blog
err = mysql.DB.Where("id in ?", bids).Find(&blogs).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("没有更多数据")
}
if err != nil {
return nil, err
}
//fmt.Printf("blogs: %v\n", blogs)
var res blog.FollowBlogRresp
res.List = blogs
res.MinTime = "0"
if len(zSet) > 0 {
res.MinTime = strconv.FormatInt(int64(zSet[len(zSet)-1].Score), 10)
}
// 取最小分数的记录数
var offset int64 = 0
minScore := zSet[len(zSet)-1].Score
for _, element := range zSet {
if element.Score == minScore {
offset++
}
}
res.Offset = offset
滚动分页
为什么要用滚动分页
由于Feed流中的数据是随时间变化不断更新的,传统的分页方式为根据每页几条pageSize和当前第几页页Page来计算查询范围,这对于Feed流中的动态列表而言会有重复读的问题:
在T1时刻查询到6、5、4三条数据后,在下一次查询(T3)时刻前有人往数据库写入一条记录7,在T3时刻查询时候根据pageSize和page计算从第四条记录开始查3条,就会得到重复的记录:4。
而滚动分页在查询时采用游标形,会记住上次查询的最后位置,可以避免这种情况
第一次查询后记住lastId为4,即上一次查询的最小值,在第二次查询时从lastId往后查找,即使中间有人插入数据也不会影响查询结果。
滚动分页实现
在redis中读取zset的数据使用zrange、zrevenge命令,一般使用zrevenge将查询结果按照score从大到小反转。读取时要关注4个参数:
-
max:最大的分数,上一次查询中最小的时间戳
-
min:最小分数,默认0
-
offset:偏移量,小于等于max的第几个元素,如写0,则查询结果包含socre=max的
-
count:查询的数据条数
==分页规则==
假设有如下数据:
| member | score |
|---|---|
| item8 | 8 |
| item7-1 | 7 |
| item7-2 | 7 |
| item6 | 6 |
| item5 | 5 |
| item4 | 4 |
使用如下命令查询
zrevrangebyscore key max min withscores limit offset count
第一次查询时,offset应当被设置成为0,例如
zrevrangebyscore key 100 0 withscores limit 0 3
结果是:
item8:8
item7-1:7
item7-2:7
后续查询时,max应当设置成上一次查询的最小分数,再此处设置为7.如果此时将offset设置为1,查询结果会变成
zrevrangebyscore key 100 0 withscores limit 0 3
结果是:
item7-2:7
item6:6
item5:5
==显然:得到一条重复的item7-2记录,这是错误的== 为了解决这个问题,后续查询时候,应当将offset设置成上一次查询的最小分数对应的记录条数。比如在本案例中第二次查询应该为
zrevrangebyscore key 100 0 withscores limit 1 3
结果是:
item6:6
item5:5