目录

  1. 需求沟通
  2. 方案设计
  3. 技术选型
    1. 选型原因
    2. 实现机制
    3. 缓存结构
  4. 设计实现
    1. 用户发帖
    2. 用户关注/取消关注
    3. 动态发布处理队列
    4. 关注取关处理队列
    5. Feed数据清理
  5. 功能扩展
    1. 操作合并
    2. 选择性推送
    3. 动态类型扩展
    4. 时间精度扩展

这些天整理交接文档,打算把自己在这份工作最后一个需求的设计稍微梳理一下。虽然实现只能交给其它人了,而且设计上也比较粗糙,存在不少瑕疵,但这也相当于自己当前技术水平的一个脚印。希望过段时间再回头来看,能发现自己有更大的进步吧!

需求沟通

和产品沟通后,综合考虑开发时限,性能要求和产品逻辑,协商后调整的需求细节总结以下几点:

  1. 添加关注后,需要显示关注者最近的发帖动态(包括关注前)
  2. 取消关注后, 需要从好友动态列表删除
  3. 取消关注后重新关注,同添加关注逻辑处理
  4. 好友动态保留最近300条

方案设计

好友动态的原理同微博的Feed流,常见有以下几种模式:

  • 集中模式。将所有用户的动态汇集到统一的流,不同用户拉取后各自根据关注信息筛选处理。性能要求和复杂度较高,此方案暂不作考虑。
  • 推模式。即写扩散。每当用户发帖,对所有粉丝推送一条该用户的动态消息记录。优点是查看动态的读取场景下效率最高。缺点是对关注列表的变动不符合需求。
  • 拉模式。即读扩散。每当请求好友动态接口,拉取用户所有关注者的最近动态,然后汇总排序。优点是对关注列表的变化能实时体现。缺点是拉取所有关注者的数据,做汇总排序分页的代价较大。
  • 推拉模式。即读+写扩散。是上面两种模式的结合,依据业务需求对推模式做补充,在开销较小的部分使用拉模式。

技术选型

选型原因

最终选择推拉模式,原因如下:

  • 推模式无法满足需求,拉模式开销大于收益。
  • 用户关注数量统计,最大用户关注数量为四位数,平均关注数量也在推模式可接受的开销范围内。
  • 好友动态功能可接受延迟显现,耗时操作可拆分为队列异步处理。
  • 对好友动态的一致性要求较高,关注和取关后,好友动态列表需要保持正确的显示效果。
  • 读需求大于写需求。在可预期的范围内,社区活跃用户的发帖动态数量在可接受范围。
  • 当前版本开发周期较短,推拉模式在满足前期需求和性能的条件上可较为迅速实现,且之后可做功能扩展

实现机制

实现机制为使用Redis的SortedSets实现。主要考虑以下原因:

  • 基于内存操作,性能和吞吐量优于使用MySQL。
  • 项目处于平稳增长期,活跃用户带来的操作性能使用Redis尚有余力。
  • 用户发布的数据已有其他缓存做持久化存储,好友动态仅需展示,不用作其他后续分析,过期后可直接丢弃。
  • 目前线上业务对Redis依赖较多,有现成闲置的缓存服务器可用,搭建适合消息订阅分发的分布式环境需要额外成本。

缓存结构

  • 每个用户一个收Feed有序集用于存放接收到的好友动态ID。有序集key名包含UID,成员为动态记录(动态ID和动态发布者UID的封装),分数为时间戳。
  • 每个用户一个发Feed有序集用于存放该用户自己发的动态ID。有序集key名包含UID,成员为动态ID,分数为时间戳。
  • 一个动态发布处理队列,用于在用户发帖时处理动态推送
  • 一个关注取关处理队列,用于在用户关注/取关时处理动态的增减
  • 一个数据清理脚本,用于定时清理回收过时数据

设计实现

用户发帖

  • 用户发帖时触发事件通知,将帖子ID用户UID时间戳封装为一条消息。
  • 消息放入动态发布处理队列,交给队列进行异步处理。
1
2
3
4
5
6
7
8
9
s=>start: 开始
e=>end: 结束

A=>operation: 用户A
send_queue=>operation: 入发布处理队列
send_data=>subroutine: {id,uid,time}
submit=>inputoutput: 发帖

s->A->submit->send_data->send_queue->e

用户发帖flow

用户关注/取消关注

  • 当用户执行关注或者取消关注动作时,将用户UID关注者UID动作标识封装为一条消息。
  • 动作标识为数字,表示当前操作为关注,取消关注,后续可扩展。
  • 消息放入关注取关处理队列,交给队列进行异步处理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
s=>start: 开始
e=>end: 结束

A=>operation: 用户A
follow_queue=>operation: 入关注处理队列

action=>condition: 关注?

follow=>inputoutput: 关注
unfollow=>inputoutput: 取关

follow_send_data=>subroutine: {uid,feed_uid,1}
unfollow_send_data=>subroutine: {uid,feed_uid,0}

s->A->action
action(yes)->follow->follow_send_data->follow_queue->e
action(no)->unfollow->unfollow_send_data->follow_queue->e

用户关注取关flow

动态发布处理队列

  • 动态发布处理队列发现新消息时,取队首消息出队列。
  • 根据消息中的发布者UID,遍历其粉丝列表(当以后全站粉丝量较大时,可扩展为选择性推送)。
  • 给每个粉丝推送一条动态,将动态ID和时间戳写入粉丝的收Feed有序集中。
  • 消息处理完成,检查队列是否还有消息,无则阻塞。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
s=>start: 发布处理队列
e=>end: 队列阻塞

in_feed=>operation: 写入粉丝收feed
send_feed=>operation: 写入用户自己的发feed

follower_list=>operation: 遍历粉丝列表

is_while=>condition: 遍历完成?
is_queue=>condition: 处理完成?

send_data=>subroutine: {id,uid,time}出队列

s->is_queue
is_queue(yes,right)->e
is_queue(no)->send_data->follower_list->is_while
is_while(no)->in_feed->is_while
is_while(yes)->send_feed->is_queue

动态发布队列flow

关注取关处理队列

  • 关注取关处理队列发现新消息时,取队首消息出队列。
  • 根据动作标识判断是关注还是取关操作。
  • 如果是关注,拉取关注者的发Feed有序集中的动态,将最近的动态ID写入用户自己的收Feed中。
  • 如果是取关,遍历用户自己的收Feed,剔除其中取关UID的动态记录。
  • 消息处理完成,检查队列是否还有消息,无则阻塞。
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
s=>start: 关注取关处理队列
e=>end: 队列阻塞

in_feed=>operation: 获取用户的收feed
in_feed2=>operation: 写入用户自己的收feed
send_feed=>operation: 关注者的发feed

continue=>operation: 消费完成,继续遍历队列

follower_list=>operation: 遍历粉丝列表

is_queue=>condition: 处理完成?
is_follow=>condition: 关注?
recently=>condition: 有近期的动态?

delete=>inputoutput: 剔除其中取关uid的动态
pull=>inputoutput: 拉取关注uid的动态

send_data=>subroutine: {uid,feed_uid,type}出队列

s->is_queue
is_queue(yes,right)->e
is_queue(no)->send_data->is_follow
is_follow(no)->in_feed->delete->continue
is_follow(yes,right)->send_feed->pull->recently
recently(no)->continue
recently(yes,right)->in_feed2->continue
continue->is_queue

关注取关处理队列flow

Feed数据清理

  • 脚本作为定时任务启动,时间间隔由功能上线后数据增长情况决定
  • 脚本遍历用户的收Feed和发Feed
  • 判断每组有序集的数量,对大于300条的数据,从最早的记录开始剔除,直到数量小于等于300条为止
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
s=>start: 定时清理脚本
e=>end: 结束

A=>operation: 遍历用户
delete=>operation: 剔除大于300条的早期数据

action=>condition: Feed类型

in_feed=>inputoutput: 收Feed有序集
send_feed=>inputoutput: 发Feed有序集

in_feed_data=>subroutine: {id\uid,time}
send_feed_data=>subroutine: {id,time}

s->A->action
action(yes)->in_feed->in_feed_data->delete->e
action(no)->send_feed->send_feed_data->delete->e

数据清理队列flow

功能扩展

操作合并

  • 当关注/取关操作量较大,或有用户频繁重复执行关注/取关时,可能需要对关注取关处理队列的操作进行合并
  • 入队列时,可判断队列中是否已存在相同uid和feed_uid的记录在等待消费,若存在,则剔除之前的,以最后操作为准

选择性推送

  • 当用户的粉丝量较大时,动态发布队列处理延迟会比较久
  • 可扩展为选择性推送,优先发送给当前在线的用户,对不在线的用户等待其上线后再执行拉取
  • 需要客户端配合,对社区用户在线状态进行处理,通知给服务端

动态类型扩展

  • 当前动态ID类型仅为帖子ID,即在用户发帖时产生动态,后续可能会对评论,回复等作为动态进行扩展
  • 当前动作标识仅有关注/取消关注,后续可能会有不看该好友动态/不展示给该好友我的动态这类的扩展

时间精度扩展

  • Feed有序集Score的时间精度为秒,如果并发量大时同秒的动态先后顺序按照的是Hash中的顺序排序
  • 如果有高精度要求,可能需要将有序集Score的时间精度设置为毫秒,将增加一定量的内存占用