MENU

swoole+redis实现高性能微博feed流方案

• October 24, 2019 • php阅读设置

一、需求

  1. 用户可以发送类似微博或朋友圈动态功能
  2. 用户具有关注、被关注、双向关注等社交属性
  3. 动态列表依赖双方社交关系展示(例如:朋友圈可以查看朋友之间的动态)
  4. 用户AB发送动态后,粉丝C可从动态大厅获取自己关注的人的所有动态内容
  5. 用户可设置动态查看权限,仅好友、仅朋友(即双向关注者)等
  6. 动态大厅最多保持300-500条记录(科学验证疲惫阅读量)

二、方案选型

在查阅了google搜索若干解决方案后,总结大致分为以下三种方案。

  1. 推模式(写扩散):用户A发送一条动态后,服务器循环用户A所有粉丝,向粉丝推送一条动态消息。缺点:数据存储成本过大,如果有100万粉丝,那么存储100万条数据。且无法根据用户双方关系发生变化而更新动态流。(例如用户A发完动态后,粉丝B取消关注A,但是在一定的时间差内,用户A的这条动态已经推送给了B)
  2. 拉模式(读扩散):用户主动请求好友关系接口,根据自身关注的人来检索好友的动态并排序汇总。缺点:用户关系变更敏感,查询检索过滤复杂度高、性能压力大。
  3. 推拉模式(读写扩散):结合推拉模式,读写均衡分摊压力。

最终我们选择推拉模式进行开发。

三、实现机制

我们使用Swoole + redis的有序集合SortedSets和消息队列List进行实现
优点:

  1. redis基于内存操作,吞吐速度远远高于mysql等
  2. Swoole、高性能php扩展,可以使用swoole的定时器和多进程来做消息队列分发

四、Redis缓存结构

  1. 每个用户一个“发送”动态流集合。用于存放自己发布的动态列表。(redis的有序集合SortedSets解决)
    redis结构如:key:user_feedlist_by_用户唯一标识 | member:动态id | Score:发布动态的时间(时间戳格式)
  1. 每个用户一个”收取“动态流集合。用于存放好友的动态列表。(redis的有序集合SortedSets解决)
    redis结构如:key:user_friend_feedlist_by_用户唯一标识 | menber:动态id+好友的用户id | Score:发布动态的时间(时间戳格式)
  1. 一个用户发送动态待处理消息队列(redis的List解决)
  2. 一个用户关系变更待处理消息队列(redis的List解决)
  3. 一个或多个负责维护用户发送动态的消息队列消费者进程,用于用户发送一条动态后进行消息推送(swoole多进程+定时器解决)
  4. 一个或多个负责维护用户关系变更的消息队列消费者进程,用于当双方好友关系或动态权限改变时对”动态流“进行处理,增删。(swoole多进程+定时器解决)
  5. 一个或多个负责维护用户的收/发动态流集合的定时进程。避免数据过多,到300-500条后清理之前不必要的动态集合。(swoole多进程+定时器解决)

五、具体实现

用户发布一条动态

  • 用户发送一条动态后,将动态的id、用户唯一标识uid、发布动态的当前时间戳进行封装打包成消息
  • 将打包的动态消息载入redis队列等待后端异步进程处理
    实现步骤:
用户发表动态成功 => 将用户的uid,动态id,发表的当前时间戳进行封装{uid:用户id,feed_id:动态id,ctime:动态发布时间} => 将封装的消息存入【发送动态待处理消息队列】 => 将封装的消息存入【“发送”动态流集合】。用于存放自己发布的动态列表

1zheFeed1.png

社交操作 关注/取消

  • 当用户对其他用户进行 关注/取消/拉黑等操作动作时,将用户唯一标识uid、目标用户uid、动作标识action【例如关注/取关】进行封装打包成消息
  • 将打包的动态消息载入redis队列等待后端异步进程处理
    实现步骤:
用户关注/取关 => 将用户uid、目标用户uid、操作标识action进行封装{uid:用户id,feed_uid:对方uid,action:attention/unsubscribe} => 将封装的消息存入【用户关系变更待处理消息队列】

1zheFeed2.png

发送动态待处理消息队列

  • 当后端消费者队列发现新消息时,取出首消息里面的数据。
  • 根据动态发布者的uid,加载其关注者列表(粉丝)。循环对其每个粉丝的【”收取“动态流集合】进行插入动态。(如遇粉丝数量庞大的时候,需要进行条件分批插入。例如:可根据活跃时间、互动程度、注册时间、账号资料完善度等进行有限推送,根据自身系统的需求灵活扩展)

1zheFeed2.png

用户关系变更待处理消息队列

  • 后端消费者进程根据【动作标识】 关注/取关/拉黑等操作对目标用户的【”收取“动态流集合】进行增减
  • 如果是关注,将被关注者的最近几条动态插入到自己的【”收取“动态流集合】
  • 如果是取关,将自己的【”收取“动态流集合】剔除对方的动态
  • 如果是拉黑[解除双方任何关系], 将自己的【”收取“动态流集合】以及对方的【”收取“动态流集合】剔除掉双方的动态

1zheFeed2.png

用户浏览动态

  • 系统只需要根据自身用户的【”收取“动态流集合】加载出动态内容,对内容进行筛选和过滤即可
  • 阅读顺序由集合里面的Score即时间戳排列,发布时间越晚,排列在用户阅读顺序最高

维护用户的收/发动态流集合进程

  • 当用户好友过多以后,用户的【”收取“动态流集合】内容就会过多,成千上万条。但实际上用户并不会真正浏览成千上万条动态。一般在浏览几百条动态后就会使用户感觉到疲劳,后面的数据根本用不上!所以才有前文提到的【维护用户的收/发动态流集合的定时进程】,对用户的【收/发动态流集合】数量维持在300-500左右即可,再多就是浪费内存
  • 所以我们需要有一个或多个进程去遍历用户的【”收取“动态流集合】,对集合进行删减,剔除掉过早的动态数据。只保留最近几百条的动态数据。

六、补充

  1. 用户粉丝量过大进行遍历时需要根据自身业务进行拆分成多个进程处理,或根据粉丝本身的活跃条件、是否在线等进行优先级写入动态消息。
  2. 如果用户量过大,用时间戳排列Score并不是十分准确,因为可能出现同时几个好友在那一秒发布动态,导致排列不准确!可以使用毫秒级精度代替时间戳精度
  3. 当用户量逐渐增多,动态消息队列的处理时间可能较久,会出现明明已经取消关注了但是对方动态还在的情况。这里可以考虑逐渐提升服务器配置以及进程数量,消息通道优化等!但是注意多进程之间并发的数据冲突问题。
Last Modified: October 28, 2019
Archives QR Code
QR Code for this page
Tipping QR Code