背景
参考:developer.aliyun.com/article/706...
Feed流就是不停更新的信息单元,只要关注某些发布者就能获取到源源不断的新鲜信息,用户就可以在移动设备上逐条去浏览这些信息单元。 当前最流行的Feed流产品有微博、微信朋友圈、头条的资讯推荐、快手抖音的视频推荐等,还有一些变种,比如私信、通知等,这些系统都是Feed流系统。
特点
Feed流本质上是一个数据流,是将 "N个发布者的信息单元" 通过 "关注关系" 传送给 "M个接收者"。 
数据分类
- 发布者数据:发布者产生的数据需要按照发布者聚合,比如微博的个人首页、朋友圈的个人空间。对应存储表(永久保存)。
- 关注关系:系统中个体间的关系。微博中是关注(单向流)、朋友圈是互关(双向流)。对应关注表(永久保存)。
- 接受者数据。从不同发送者(关注的人)那获取的数据,按照某种顺序(一般是时间)组织在一起,比如微博的首页、朋友圈首页等。对应同步表(周期保存)。
产品层面需要考虑的因素
- 用户规模:不同用户规模涉及到底层的存储形式可能不同。
- 关注关系:如果是双向那就不存在大V,反之则存在。
- 排序规则:是按时间还是按推荐。
产品定义
| 类型 | 关注关系 | 是否有大V | 时效性 | 排序 |
|---|---|---|---|---|
| 微博类 | 单向 | 有 | 秒~分 | 时间 |
| 抖音类 | 单向/无 | 有 | 秒~分 | 推荐 |
| 朋友圈类 | 双向 | 无 | 秒 | 时间 |
| 私信类 | 双向 | 无 | 秒 | 时间 |
从上面表格可以看出来,主要分为两种区分:
-
关注关系是单向还是双向:
- 如果是单向,那么可能就会存在大V效应,同时时效性可以低一些,比如到分钟级别;
- 如果是双向,那就是好友,好友的数量有限,那么就不会有大V,这时候因为关系更亲密,时效性要求会更高,需要都秒级别。
-
排序是时间还是推荐:
- 用户对feed流最容易接受的就是时间,目前大部分都是时间。
- 但是有一些场景,是从全网数据里面根据用户的喜好给用户推荐和用户喜好度最匹配的内容,这个时候就需要用推荐了,这种情况一般也会省略掉关注了,相对于关注了全网所有用户,比如抖音、头条等。
存储结构
存储库最重要的特征有两点:
- 数据可靠不丢失。
- 由于会一直增长,需要易于水平扩展。
可以选为存储库的系统大概有两类:
| 特点 | 分布式NoSQL | 关系型数据库(分库分表) |
|---|---|---|
| 可靠性 | 极高 | 高 |
| 水平扩展能力 | 线性 | 需要改造 |
| 水平扩展速度 | 毫秒 | 无 |
| 常见系统 | Tablestore、Bigtable | MySQL、PostgreSQL |
- 对于可靠性,分布式NoSQL的可靠性要高于关系型数据库,一般都是存储三份,可靠性会更高。
- 水平扩展能力:对于分布式NoSQL数据库,数据天然是分布在多台机器上,当一台机器上的数据量增大后,可以通过自动分裂两部分,然后将其中一半的数据迁移到另一台机器上去,这样就做到了线性扩展。而关系型数据库需要在扩容时再次分库分表,分片策略决定扩展成本。
- 取余。避免使用%N的分片方式,这种方式极端情况下涉及所有数据迁移。
- 一致性哈希。使用一致性哈希可以将物理节点映射为多个虚拟节点,添加节点时只影响部分数据。
- 范围分片+动态分裂。节点1负责ID(0,1000),节点2负责ID(1000,2000)。当某个节点负责的ID数据量太大时会进行动态分裂,即拆分为更小的分片,节点1.1负责(0,500),节点1.2负责(500,1000)。
结论:
- 如果是自建系统,且不具备分布式NoSQL数据库运维能力,且数据规模不大,那么可以使用MySQL。
- 如果是基于云服务,那么就用分布式NoSQL,比如Tablestore或Bigtable。
- 如果数据规模很大,那么也要用分布式NoSQL,否则就是走上一条不归路。
存储库表设计结构(Tablestore举例)如下:
| 主键列 | 第一列主键 | 第二列主键 | 属性列 | 属性列 |
|---|---|---|---|---|
| 列名 | user_id | message_id | content | other |
| 解释 | 消息发送者用户ID | 消息顺序ID,可以使用timestamp。 | 内容 | 其他内容 |

同步方式
常见的方式有三种:
- 推模式(写扩散) :发送者发送了一个消息后,立即将这个消息推送给接收者,但是接收者此时不一定在线,那么就需要有一个地方存储这个数据,这个存储的地方称为:同步库 。推模式也叫写扩散的原因是,一个消息需要发送个多个粉丝,那么这条消息就会复制多份,写放大,所以也叫写扩散。这种模式下,对同步库的要求就是写入能力极强和稳定 。读取的时候因为消息已经发到接收者的收件箱了,只需要读一次自己的收件箱即可,读请求的量极小,所以对读的QPS需求不大。归纳下,推模式中对同步库的要求只有一个:写入能力强。
- 拉模式(读扩散) :这种是一种拉的方式,发送者发送了一条消息后,这条消息不会立即推送给粉丝,而是写入自己的发件箱 ,当粉丝上线后再去自己关注者的发件箱里面去读取,一条消息的写入只有一次,但是读取最多会和粉丝数一样,读会放大,所以也叫读扩散。拉模式的读写比例刚好和写扩散相反,那么对系统的要求是:读取能力强。另外这里还有一个误区,很多人在最开始设计feed流系统时,首先想到的是拉模式,因为这种和用户的使用体感是一样的,但是在系统设计上这种方式有不少痛点,最大的是每个粉丝需要记录自己上次读到了关注者的哪条消息,如果有1000个关注者,那么这个人需要记录1000个位置信息,这个量和关注量成正比的,远比用户数要大的多,这里要特别注意,虽然在产品前期数据量少的时候这种方式可以应付,但是量大了后就会事倍功半,得不偿失,切记切记。
- 推拉结合模式 :
- 按照用户群体分类:大V拉,普通用户推。大V存在大量粉丝,如果采用推模式,可能一条消息会扩散几百万次。基于此,采用大部分用户的消息都是写扩散,只有大V是读扩散。
- 按照用户活跃状态分类:在线推,离线拉。以大V举例,大V发送一条消息后,将消息推送给在线的粉丝,对于当前时刻离线的粉丝在上线后,从大V的发件箱中拉取消息。
结论:
- 如果产品中是双向关系,那么就采用推模式。
- 如果产品中是单向关系,且用户数少于1000万,那么也采用推模式,足够了。
- 如果产品是单向关系,单用户数大于1000万,那么采用推拉结合模式,这时候可以从推模式演进过来,不需要额外重新推翻重做。
- 永远不要只用拉模式。
- 如果是一个初创企业,先用推模式,快速把系统设计出来,然后让产品去验证、迭代,等客户数大幅上涨到1000万后,再考虑升级为推拉集合模式。
同步库表结构如下:
| 主键列 | 第一列主键 | 第二列主键 | 属性列 | 属性列 | 属性列 |
|---|---|---|---|---|---|
| 列名 | user_id | sequence_id | sender_id | message_id | other |
| 解释 | 消息接收者用户ID | 消息顺序ID,可以使用timestamp + send_user_id,也可以直接使用Tablestore的自增列。 | 发送者的用户ID | store_table中的message_id列的值,也就是消息ID。通过sender_id和message_id可以到store_table中查询到消息内容 | 其他内容,同步库中不需要包括消息内容。 |

用户关系
查询的时候需要支持查询关注列表或者粉丝列表,或者直接好友列表,这里就需要根据多个属性列查询需要索引能力,这里,存储系统也可以采用两类,关系型、分布式NoSQL数据库。 关注关系表设计结构如下: Table:user_relation_table
| 主键顺序 | 第一列主键 | 第一列主键 | 属性列 | 属性列 |
|---|---|---|---|---|
| Table字段名 | user_id | follow_user_id | timestamp | other |
| 备注 | 用户ID | 粉丝用户ID | 关注时间 | 其他属性列 |
查询的时候:
- 如果需要查询某个人的粉丝列表:使用TermQuery查询固定user_id,且按照timestamp排序。
- 如果需要查询某个人的关注列表:使用TermQuery查询固定follow_user_id,且按照timestamp排序。
- 当前数据写入Table后,需要5~10秒钟延迟后会在多元索引中查询到,未来会优化到2秒以内。
推送session池
发送者将消息发送后,接收者如何知道自己有新消息来了?客户端周期性去刷新?如果是这样子,那么系统的读请求压力会随着客户端增长而增长,如果某天平台爆发了一个热点消息,大量休眠设备登陆,这个时候就会出现"查询风暴",一下子就把系统打垮了,所有的用户都不能用了。 可以在服务端维护一个推送session池,这个里面记录哪些用户在线,然后当用户A发送了一条消息给用户B后,服务端在写入存储库和同步库后,再通知一下session池中的用户B的session,告诉他:你有新消息了。然后session-B再去读消息,然后有消息后将消息推送给客户端。或者有消息后给客户端推送一下有消息了,客户端再去拉。
session表设计结构如下:
| 主键列顺序 | 第一列主键 | 第二列主键 | 属性列 |
|---|---|---|---|
| 列名 | user_id | device_id | last_sequence_id |
| 备注 | 接收者用户ID | 设备ID,同一个用户可能会有多个设备,不同设备的读取位置可能不一致,所以这里需要一个设备ID。如果不需要支持多终端,则这一列可以省略。 | 该接收者已经推送给客户端的最新的顺序ID |
评论
评论的属性和存储库差不多,但是多了一层关系:被评论的消息,所以只要将评论按照被被评论消息分组组织即可,然后查询时也是一个范围查询就行。 评论表设计结构如下:
| 主键列顺序 | 第一列主键 | 第二列主键 | 属性列 | 属性列 | 属性列 |
|---|---|---|---|---|---|
| 字段名 | message_id | comment_id | comment_content | reply_to | other |
| 备注 | 微博ID或朋友圈ID等消息的ID | 这一条评论的ID | 评论内容 | 回复给哪个用户 | 其他 |
点赞
"赞"或"like"功能很流行,赞功能的实现和评论类似,只是比评论少了一个内容,所以选择方式和评论一样。
系统结构

系统流转示例
系统架构
txt
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Feed流系统整体架构 │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
┌─────────────────┐
│ 客户端 (APP) │
│ iOS / Android │
└────────┬────────┘
│
┌──────────────────┼──────────────────┐
│ HTTP/HTTPS │ WebSocket │
▼ ▼ │
┌─────────────────┐ ┌─────────────────┐ │
│ API Gateway │ │ WebSocket LB │ │
│ (Nginx) │ │ (Nginx/HAProxy)│ │
└────────┬────────┘ └────────┬────────┘ │
│ │ │
┌────────────────────────┼────────────────────┼──────────────────┤
│ │ │ │
▼ ▼ ▼ │
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ Feed Service │ │ User Service │ │ Push Service │◄────────┘
│ (发布/拉取) │ │ (用户/关注) │ │ (WebSocket) │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
│ │ │
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ 消息队列层 │
│ ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ Kafka / RocketMQ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ feed_publish│ │ feed_sync │ │ feed_notify │ │ comment_topic│ │ like_topic │ │ │
│ │ │ Topic │ │ Topic │ │ Topic │ │ │ │ │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ 异步处理层 │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Sync Worker │ │ Notify Worker │ │ Comment Worker │ │ Like Worker │ │
│ │ (写扩散处理) │ │ (推送通知) │ │ (评论处理) │ │ (点赞处理) │ │
│ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │
└────────────┼─────────────────────┼─────────────────────┼─────────────────────┼──────────────────────────────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ 存储层 │
│ │
│ ┌───────────────────────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ Tablestore / HBase (分布式NoSQL) │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ store_table │ │ sync_table │ │relation_table│ │comment_table│ │ like_table │ │ │
│ │ │ (存储表) │ │ (同步表) │ │ (关注表) │ │ (评论表) │ │ (点赞表) │ │ │
│ │ │ 永久保存 │ │ 周期保存 │ │ 永久保存 │ │ 永久保存 │ │ 永久保存 │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ └───────────────────────────────────────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────┐ ┌───────────────────────────────────────┐ │
│ │ Redis Cluster │ │ MySQL │ │
│ │ ┌─────────────┐ ┌─────────────┐ │ │ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │session_pool │ │ feed_cache │ │ │ │ user_table │ │ bigv_table │ │ │
│ │ │ (Session池) │ │ (Feed缓存) │ │ │ │ (用户表) │ │ (大V表) │ │ │
│ │ └─────────────┘ └─────────────┘ │ │ └─────────────┘ └─────────────┘ │ │
│ │ ┌─────────────┐ ┌─────────────┐ │ └───────────────────────────────────────┘ │
│ │ │ like_count │ │pull_position│ │ │
│ │ │ (点赞计数) │ │ (拉取位置) │ │ │
│ │ └─────────────┘ └─────────────┘ │ │
│ └───────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
场景设定
txt
┌─────────────────────────────────────────────────────────────────────────────────────────┐
│ 场景设定 │
├─────────────────────────────────────────────────────────────────────────────────────────┤
│ 大V用户:bigV_001(粉丝数:500万) │
│ 发布新动态:message_id = "msg_20241229_010",内容:"新年快乐!" │
│ 发布时间:2024-12-29 15:00:00 │
│ │
│ 粉丝状态分布: │
│ ┌─────────────────────────────────────────────────────────────────────────────────┐ │
│ │ 在线粉丝:100万(在Session池中有记录,采用推模式) │ │
│ │ 离线粉丝:400万(不在Session池中,采用拉模式) │ │
│ └─────────────────────────────────────────────────────────────────────────────────┘ │
│ │
│ 以离线粉丝 fan_003 为例: │
│ - 关注了3个大V:bigV_001, bigV_002, bigV_003 │
│ - 上次在线时间:2024-12-28 18:30:00(昨天) │
│ - 对 bigV_001 的拉取位置:msg_20241228_005 │
│ - 离线期间 bigV_001 发了5条新消息:msg_20241229_006 ~ msg_20241229_010 │
└─────────────────────────────────────────────────────────────────────────────────────────┘
核心表结构
txt
┌─────────────────────────────────────────────────────────────────────────────────────────┐
│ 1. store_table(存储表/发件箱)- 永久保存 │
├───────────────┬───────────────────┬──────────────┬─────────────────────┬───────────────┤
│ user_id │ message_id │ content │ timestamp │ seq_no │
│ (发布者ID) │ (消息ID) │ (内容) │ (发布时间) │ (序列号) │
├───────────────┼───────────────────┼──────────────┼─────────────────────┼───────────────┤
│ bigV_001 │ msg_20241228_005 │ 晚安 │ 2024-12-28 23:00 │ 1005 │
│ bigV_001 │ msg_20241229_006 │ 早上好 │ 2024-12-29 08:00 │ 1006 │
│ bigV_001 │ msg_20241229_007 │ 吃早餐了 │ 2024-12-29 09:00 │ 1007 │
│ bigV_001 │ msg_20241229_008 │ 开始工作 │ 2024-12-29 10:00 │ 1008 │
│ bigV_001 │ msg_20241229_009 │ 午餐时间 │ 2024-12-29 12:00 │ 1009 │
│ bigV_001 │ msg_20241229_010 │ 新年快乐! │ 2024-12-29 15:00 │ 1010 │
└───────────────┴───────────────────┴──────────────┴─────────────────────┴───────────────┘
┌─────────────────────────────────────────────────────────────────────────────────────────┐
│ 2. sync_table(同步表/收件箱)- 周期保存 │
├───────────────┬───────────────┬───────────────┬───────────────────┬─────────────────────┤
│ user_id │ sequence_id │ sender_id │ message_id │ timestamp │
│ (接收者ID) │ (收件箱序列号) │ (发送者ID) │ (消息ID) │ (时间戳) │
├───────────────┼───────────────┼───────────────┼───────────────────┼─────────────────────┤
│ fan_001 │ seq_501 │ bigV_001 │ msg_20241229_010 │ 2024-12-29 15:00 │
│ fan_002 │ seq_302 │ bigV_001 │ msg_20241229_010 │ 2024-12-29 15:00 │
│ ... │ ... │ ... │ ... │ ... │
└───────────────┴───────────────┴───────────────┴───────────────────┴─────────────────────┘
│ 注意:同步表不存储消息内容,通过 sender_id + message_id 回查存储表获取 │
┌─────────────────────────────────────────────────────────────────────────────────────────┐
│ 3. pull_position_table(拉取位置表)- 记录每个粉丝对每个大V的拉取位置 │
├───────────────┬───────────────┬───────────────────┬─────────────────────────────────────┤
│ user_id │ bigv_id │ last_pull_msg_id │ last_pull_time │
│ (粉丝ID) │ (大V ID) │ (上次拉到的消息ID) │ (上次拉取时间) │
├───────────────┼───────────────┼───────────────────┼─────────────────────────────────────┤
│ fan_003 │ bigV_001 │ msg_20241228_005 │ 2024-12-28 18:30:00 │
│ fan_003 │ bigV_002 │ msg_20241227_012 │ 2024-12-27 20:15:00 │
│ fan_003 │ bigV_003 │ msg_20241229_001 │ 2024-12-29 09:00:00 │
└───────────────┴───────────────┴───────────────────┴─────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────────────────┐
│ 4. session_pool(Session池)- 记录在线用户,Redis实现 │
├───────────────┬───────────────┬───────────────────┬─────────────────────────────────────┤
│ user_id │ device_id │ last_sequence_id │ online_status │
│ (用户ID) │ (设备ID) │ (已推送的最新序列) │ (在线状态) │
├───────────────┼───────────────┼───────────────────┼─────────────────────────────────────┤
│ fan_001 │ device_A │ seq_500 │ ONLINE │
│ fan_002 │ device_B │ seq_301 │ ONLINE │
│ fan_003 │ - │ - │ OFFLINE(不在池中) │
└───────────────┴───────────────┴───────────────────┴─────────────────────────────────────┘
大V发送动态
txt
┌─────────────────────────────────────────────────────────┐
│ bigV_001 发送动态 │
│ POST /api/feed/publish │
│ { user_id: "bigV_001", content: "新年快乐!" } │
└────────────────────────────┬────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Step 1: 写入存储表(大V的发件箱) │
│ ┌──────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ INSERT INTO store_table (user_id, message_id, content, timestamp, seq_no) │ │
│ │ VALUES ('bigV_001', 'msg_20241229_010', '新年快乐!', '2024-12-29 15:00:00', 1010) │ │
│ └──────────────────────────────────────────────────────────────────────────────────────────┘ │
│ 消息永久保存在大V的发件箱中,所有粉丝都可以从这里拉取 │
└────────────────────────────────────────────────────┬───────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Step 2: 查询粉丝列表 │
│ ┌──────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ SELECT follow_user_id FROM user_relation_table WHERE user_id = 'bigV_001' │ │
│ │ → 返回 500万 粉丝ID │ │
│ └──────────────────────────────────────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────┬───────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Step 3: 查询Session池,区分在线/离线粉丝 │
│ ┌──────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ SELECT user_id FROM session_pool WHERE user_id IN (粉丝列表) AND online_status = 'ONLINE' │ │
│ │ → 在线:100万 离线:400万 │ │
│ └──────────────────────────────────────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────┬───────────────────────────────────────────┘
│
┌──────────────────────────┴──────────────────────────┐
▼ ▼
┌──────────────────────────────────────────────────────┐ ┌──────────────────────────────────────────────────────┐
│ 在线粉丝处理(推模式) │ │ 离线粉丝处理(拉模式) │
│ │ │ │
│ Step 4a: 批量写入同步表(收件箱) │ │ Step 4b: 不做任何操作 │
│ ┌────────────────────────────────────────────────┐ │ │ ┌────────────────────────────────────────────────┐ │
│ │ // 100万条写入,分批处理 │ │ │ │ 消息保留在 bigV_001 的存储表(发件箱)中 │ │
│ │ for (batch : partition(onlineFans, 1000)) { │ │ │ │ 等待离线粉丝上线后主动拉取 │ │
│ │ sync_table.batchInsert( │ │ │ │ │ │
│ │ batch, // 粉丝ID列表 │ │ │ │ 离线粉丝的拉取位置保持不变: │ │
│ │ "bigV_001", // 发送者 │ │ │ │ fan_003 对 bigV_001: msg_20241228_005 │ │
│ │ "msg_20241229_010" // 消息ID │ │ │ │ (下次上线时从这个位置开始拉取) │ │
│ │ ); │ │ │ └────────────────────────────────────────────────┘ │
│ │ } │ │ │ │
│ └────────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────┘
│ │
│ Step 5a: 通过Session长连接推送通知 │
│ ┌────────────────────────────────────────────────┐ │
│ │ sessionPool.notifyUsers( │ │
│ │ onlineFans, │ │
│ │ { type: "NEW_FEED", count: 1 } │ │
│ │ ); │ │
│ │ 客户端收到通知后,主动拉取最新消息 │ │
│ └────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────┘
在线粉丝 fan_001 接收消息
txt
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
│ fan_001 的 Session 收到推送通知 │
│ { type: "NEW_FEED", count: 1 } │
└────────────────────────────────────────────────────┬───────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Step 1: 客户端发起拉取请求 │
│ ┌──────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ GET /api/feed/timeline?last_seq=seq_500 │ │
│ │ fan_001 上次读到 seq_500,请求 seq_500 之后的新消息 │ │
│ └──────────────────────────────────────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────┬───────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Step 2: 从同步表(收件箱)读取新消息 │
│ ┌──────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ SELECT * FROM sync_table │ │
│ │ WHERE user_id = 'fan_001' AND sequence_id > 'seq_500' 客户端请求时如果没有携带 last_seq,服务端会从 session_table 读取 │ │
│ │ ORDER BY sequence_id DESC LIMIT 20 │ │
│ │ │ │
│ │ 返回结果: │ │
│ │ sequence_id │ sender_id │ message_id │ │
│ │ seq_501 │ bigV_001 │ msg_20241229_010 ← 刚推送的新消息 │ │
│ └──────────────────────────────────────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────┬───────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Step 3: 回查存储表获取消息内容 │
│ ┌──────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ SELECT content, timestamp FROM store_table │ │
│ │ WHERE user_id = 'bigV_001' AND message_id = 'msg_20241229_010' │ │
│ │ │ │
│ │ 返回结果:{ content: "新年快乐!", timestamp: "2024-12-29 15:00:00" } │ │
│ └──────────────────────────────────────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────┬───────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Step 4: 返回给客户端展示 │
│ ┌──────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ 客户端 Timeline: │ │
│ │ ┌────────────────────────────────────────────────────────────────────────────────────┐ │ │
│ │ │ [bigV_001] 新年快乐! 15:00 ← 新消息 │ │ │
│ │ │ [user_xxx] xxxxxxxxx 14:30 │ │ │
│ │ │ [bigV_002] xxxxxxxxx 14:00 │ │ │
│ │ └────────────────────────────────────────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────────────────────────────────────┘ │
│ 同时更新 Session 中的 last_sequence_id = seq_501 │
└────────────────────────────────────────────────────────────────────────────────────────────────┘
离线粉丝 fan_003 上线后的拉取流程
txt
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
│ fan_003 上线,打开APP │
│ 时间:2024-12-29 16:00:00(离线了约22小时) │
└────────────────────────────────────────────────────┬───────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Step 1: 建立Session连接,加入Session池 │
│ ┌──────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ session_pool.register( │ │
│ │ user_id: "fan_003", │ │
│ │ device_id: "iPhone_xxx", │ │
│ │ online_status: "ONLINE" │ │
│ │ ) │ │
│ └──────────────────────────────────────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────┬───────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Step 2: 查询关注的大V列表 │
│ ┌──────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ SELECT user_id FROM user_relation_table │ │
│ │ WHERE follow_user_id = 'fan_003' AND is_bigv = true │ │
│ │ │ │
│ │ 返回:[bigV_001, bigV_002, bigV_003] │ │
│ └──────────────────────────────────────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────┬───────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Step 3: 查询每个大V的拉取位置 ⭐ 关键步骤 │
│ ┌──────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ SELECT bigv_id, last_pull_msg_id, last_pull_time │ │
│ │ FROM pull_position_table │ │
│ │ WHERE user_id = 'fan_003' AND bigv_id IN ('bigV_001', 'bigV_002', 'bigV_003') │ │
│ │ │ │
│ │ ┌────────────────────────────────────────────────────────────────────────────────────┐ │ │
│ │ │ 查询结果:fan_003 的拉取位置 │ │ │
│ │ │ bigv_id │ last_pull_msg_id │ last_pull_time │ 含义 │ │ │
│ │ │ ───────────┼────────────────────┼───────────────────────┼────────────────────────│ │ │
│ │ │ bigV_001 │ msg_20241228_005 │ 2024-12-28 18:30:00 │ 昨天拉到第5条消息 │ │ │
│ │ │ bigV_002 │ msg_20241227_012 │ 2024-12-27 20:15:00 │ 前天拉到第12条消息 │ │ │
│ │ │ bigV_003 │ msg_20241229_001 │ 2024-12-29 09:00:00 │ 今早拉到第1条消息 │ │ │
│ │ └────────────────────────────────────────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────┬───────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Step 4: 从每个大V的发件箱增量拉取 ⭐ 关键步骤 │
│ │
│ ┌──────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ 对于 bigV_001(从 msg_20241228_005 之后开始拉取): │ │
│ │ │ │
│ │ SELECT * FROM store_table │ │
│ │ WHERE user_id = 'bigV_001' │ │
│ │ AND message_id > 'msg_20241228_005' ← 从上次位置之后开始(不包含上次的消息) │ │
│ │ ORDER BY message_id ASC │ │
│ │ LIMIT 50 │ │
│ │ │ │
│ │ 拉取到的新消息(5条): │ │
│ │ ┌────────────────────────────────────────────────────────────────────────────────────┐ │ │
│ │ │ message_id │ content │ timestamp │ 状态 │ │ │
│ │ │ ──────────────────┼──────────────┼─────────────────────┼───────────────────────── │ │ │
│ │ │ msg_20241229_006 │ 早上好 │ 2024-12-29 08:00 │ 新消息 │ │ │
│ │ │ msg_20241229_007 │ 吃早餐了 │ 2024-12-29 09:00 │ 新消息 │ │ │
│ │ │ msg_20241229_008 │ 开始工作 │ 2024-12-29 10:00 │ 新消息 │ │ │
│ │ │ msg_20241229_009 │ 午餐时间 │ 2024-12-29 12:00 │ 新消息 │ │ │
│ │ │ msg_20241229_010 │ 新年快乐! │ 2024-12-29 15:00 │ 新消息(最新) │ │ │
│ │ └────────────────────────────────────────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ 对于 bigV_002(从 msg_20241227_012 之后开始拉取): │ │
│ │ 拉取到 3 条新消息:msg_20241228_001 ~ msg_20241228_003 │ │
│ └──────────────────────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ 对于 bigV_003(从 msg_20241229_001 之后开始拉取): │ │
│ │ 拉取到 1 条新消息:msg_20241229_002 │ │
│ └──────────────────────────────────────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────┬───────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Step 5: 更新拉取位置 ⭐ 关键步骤 │
│ ┌──────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ 批量更新 fan_003 对每个大V的拉取位置: │ │
│ │ │ │
│ │ UPDATE pull_position_table SET │ │
│ │ last_pull_msg_id = CASE bigv_id │ │
│ │ WHEN 'bigV_001' THEN 'msg_20241229_010' ← 更新为本次拉到的最新消息ID │ │
│ │ WHEN 'bigV_002' THEN 'msg_20241228_003' │ │
│ │ WHEN 'bigV_003' THEN 'msg_20241229_002' │ │
│ │ END, │ │
│ │ last_pull_time = '2024-12-29 16:00:00' ← 更新拉取时间 │ │
│ │ WHERE user_id = 'fan_003' │ │
│ │ │ │
│ │ ┌────────────────────────────────────────────────────────────────────────────────────┐ │ │
│ │ │ 更新后的拉取位置表: │ │ │
│ │ │ bigv_id │ last_pull_msg_id │ last_pull_time │ 变化 │ │ │
│ │ │ ───────────┼────────────────────┼───────────────────────┼────────────────────────│ │ │
│ │ │ bigV_001 │ msg_20241229_010 │ 2024-12-29 16:00:00 │ ✅ 005 → 010 │ │ │
│ │ │ bigV_002 │ msg_20241228_003 │ 2024-12-29 16:00:00 │ ✅ 012 → 003 │ │ │
│ │ │ bigV_003 │ msg_20241229_002 │ 2024-12-29 16:00:00 │ ✅ 001 → 002 │ │ │
│ │ └────────────────────────────────────────────────────────────────────────────────────┘ │ │
│ │ 下次 fan_003 再上线时,将从这些新位置开始拉取 │ │
│ └──────────────────────────────────────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────┬───────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Step 6: 合并消息并写入同步表(收件箱) │
│ ┌──────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ 将从各个大V拉取的消息合并,按时间排序,写入 fan_003 的同步表 │ │
│ │ │ │
│ │ sync_table(fan_003 的收件箱)新增记录: │ │
│ │ ┌────────────────────────────────────────────────────────────────────────────────────┐ │ │
│ │ │ user_id │ sequence_id │ sender_id │ message_id │ timestamp │ │ │
│ │ │ ─────────┼─────────────┼────────────┼───────────────────┼────────────────────────│ │ │
│ │ │ fan_003 │ seq_201 │ bigV_001 │ msg_20241229_006 │ 2024-12-29 08:00 │ │ │
│ │ │ fan_003 │ seq_202 │ bigV_002 │ msg_20241228_001 │ 2024-12-29 08:30 │ │ │
│ │ │ fan_003 │ seq_203 │ bigV_001 │ msg_20241229_007 │ 2024-12-29 09:00 │ │ │
│ │ │ fan_003 │ seq_204 │ bigV_003 │ msg_20241229_002 │ 2024-12-29 09:30 │ │ │
│ │ │ fan_003 │ seq_205 │ bigV_001 │ msg_20241229_008 │ 2024-12-29 10:00 │ │ │
│ │ │ fan_003 │ seq_206 │ bigV_001 │ msg_20241229_009 │ 2024-12-29 12:00 │ │ │
│ │ │ fan_003 │ seq_207 │ bigV_001 │ msg_20241229_010 │ 2024-12-29 15:00 │ │ │
│ │ │ ... │ ... │ ... │ ... │ ... │ │ │
│ │ └────────────────────────────────────────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────┬───────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Step 7: 返回给客户端展示 │
│ ┌──────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ 客户端 Timeline(按时间倒序展示): │ │
│ │ ┌────────────────────────────────────────────────────────────────────────────────────┐ │ │
│ │ │ ┌────────────────────────────────────────────────────────────────────────────┐ │ │ │
│ │ │ │ [bigV_001] 新年快乐! 15:00 │ │ │ │
│ │ │ └────────────────────────────────────────────────────────────────────────────┘ │ │ │
│ │ │ ┌────────────────────────────────────────────────────────────────────────────┐ │ │ │
│ │ │ │ [bigV_001] 午餐时间 12:00 │ │ │ │
│ │ │ └────────────────────────────────────────────────────────────────────────────┘ │ │ │
│ │ │ ┌────────────────────────────────────────────────────────────────────────────┐ │ │ │
│ │ │ │ [bigV_001] 开始工作 10:00 │ │ │ │
│ │ │ └────────────────────────────────────────────────────────────────────────────┘ │ │ │
│ │ │ ┌────────────────────────────────────────────────────────────────────────────┐ │ │ │
│ │ │ │ [bigV_003] xxxxxxxxx 09:30 │ │ │ │
│ │ │ └────────────────────────────────────────────────────────────────────────────┘ │ │ │
│ │ │ ┌────────────────────────────────────────────────────────────────────────────┐ │ │ │
│ │ │ │ [bigV_001] 吃早餐了 09:00 │ │ │ │
│ │ │ └────────────────────────────────────────────────────────────────────────────┘ │ │ │
│ │ │ ┌────────────────────────────────────────────────────────────────────────────┐ │ │ │
│ │ │ │ [bigV_002] xxxxxxxxx 08:30 │ │ │ │
│ │ │ └────────────────────────────────────────────────────────────────────────────┘ │ │ │
│ │ │ ┌────────────────────────────────────────────────────────────────────────────┐ │ │ │
│ │ │ │ [bigV_001] 早上好 08:00 │ │ │ │
│ │ │ └────────────────────────────────────────────────────────────────────────────┘ │ │ │
│ │ └────────────────────────────────────────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────────────────────────────────────┘ │
│ 同时更新 Session 中的 last_sequence_id = seq_207 │
└────────────────────────────────────────────────────────────────────────────────────────────────┘
评论
表结构设计
txt
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ comment_table(评论表)- 存储:Tablestore / HBase │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ 主键设计 │
│ ┌─────────────────┬─────────────────────────┬─────────────────────────────────────────────────────────────┐ │
│ │ 列名 │ 类型 │ 说明 │ │
│ ├─────────────────┼─────────────────────────┼─────────────────────────────────────────────────────────────┤ │
│ │ message_id │ String (PK1, 分区键) │ 被评论的动态ID,同一动态的评论物理上连续存储 │ │
│ │ comment_id │ String (PK2, 排序键) │ 评论ID,格式:时间戳_用户ID,保证有序且唯一 │ │
│ └─────────────────┴─────────────────────────┴─────────────────────────────────────────────────────────────┘ │
│ │
│ 属性列 │
│ ┌─────────────────┬─────────────────────────┬─────────────────────────────────────────────────────────────┐ │
│ │ 列名 │ 类型 │ 说明 │ │
│ ├─────────────────┼─────────────────────────┼─────────────────────────────────────────────────────────────┤ │
│ │ user_id │ String │ 评论者ID │ │
│ │ content │ String │ 评论内容 │ │
│ │ reply_to_user │ String │ 回复目标用户ID(楼中楼),NULL表示直接评论动态 │ │
│ │ reply_to_cmt │ String │ 回复目标评论ID(楼中楼) │ │
│ │ timestamp │ Long │ 评论时间戳 │ │
│ │ status │ Integer │ 状态:0-正常,1-已删除,2-隐藏 │ │
│ │ like_count │ Integer │ 评论点赞数(冗余字段,定时同步) │ │
│ └─────────────────┴─────────────────────────┴─────────────────────────────────────────────────────────────┘ │
│ │
│ 示例数据 │
│ ┌───────────────────┬─────────────────────────┬──────────┬────────────┬──────────────┬──────────────┐ │
│ │ message_id │ comment_id │ user_id │ content │ reply_to_user│ reply_to_cmt│ │
│ ├───────────────────┼─────────────────────────┼──────────┼────────────┼──────────────┼──────────────┤ │
│ │ msg_001 │ 1703836800000_userA │ userA │ 说得好! │ NULL │ NULL │ │
│ │ msg_001 │ 1703836900000_userB │ userB │ 同意楼上 │ userA │ 上一条ID │ │
│ │ msg_001 │ 1703837000000_userC │ userC │ 不太认同 │ NULL │ NULL │ │
│ │ msg_002 │ 1703838000000_userA │ userA │ 哈哈哈 │ NULL │ NULL │ │
│ └───────────────────┴─────────────────────────┴──────────┴────────────┴──────────────┴──────────────┘ │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Redis 缓存设计 │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ 1. 评论计数缓存 │
│ Key: comment_count:{message_id} │
│ Value: 123 (评论数) │
│ TTL: 1小时,过期后从数据库重新统计 │
│ │
│ 2. 热门评论缓存(可选) │
│ Key: hot_comments:{message_id} │
│ Value: List<Comment> (前N条热门评论,按点赞数排序) │
│ TTL: 10分钟 │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
发表评论流程
txt
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ 发表评论完整流程 │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
用户B Comment Service 数据库 动态作者(大V)
│ │ │ │
│ POST /api/comment │ │ │
│ { │ │ │
│ message_id: "msg_001", │ │ │
│ content: "说得好!", │ │ │
│ reply_to_user: null │ │ │
│ } │ │ │
│─────────────────────────────────>│ │ │
│ │ │ │
│ │ 1. 参数校验 │ │
│ │ - 内容长度检查 │ │
│ │ - 敏感词过滤 │ │
│ │ - 用户是否被禁言 │ │
│ │ │ │
│ │ 2. 生成评论ID │ │
│ │ comment_id = timestamp_userB │ │
│ │ │ │
│ │ 3. 写入评论表 ──────────────>│ │
│ │ INSERT comment_table │ │
│ │ │ │
│ │ 4. 更新评论计数 ────────────>│ Redis │
│ │ INCR comment_count:msg_001│ │
│ │ │ │
│ │ 5. 查询动态作者 ────────────>│ store_table │
│ │ 获取 msg_001 的 user_id │ │
│ │<────── bigV_001 ─────────────│ │
│ │ │ │
│ │ 6. 发送通知 ─────────────────────────────────────────────>│
│ │ 写入 notification_table │ │
│ │ { │ │
│ │ user_id: bigV_001, │ │
│ │ type: COMMENT, │ │
│ │ from_user: userB, │ │
│ │ message_id: msg_001, │ │
│ │ comment_id: xxx │ │
│ │ } │ │
│ │ │ │
│ │ 7. 如果大V在线,推送通知 ─────────────────────────────────>│
│ │ WebSocket: "有人评论了你的动态" │
│ │ │ │
│<──── 返回成功 ──────────────────│ │ │
│ { comment_id: xxx } │ │ │
│ │ │ │
楼中楼回复
txt
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ 楼中楼回复流程 │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
用户C回复用户B的评论
POST /api/comment
{
message_id: "msg_001", // 原动态ID
content: "同意!",
reply_to_user: "userB", // 回复目标用户
reply_to_comment: "cmt_001" // 回复目标评论ID
}
│
▼
┌──────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ 写入评论表 │
│ ┌─────────────────┬─────────────────────────┬──────────┬────────────┬──────────────┬──────────────┐ │
│ │ message_id │ comment_id │ user_id │ content │ reply_to_user│ reply_to_cmt│ │
│ │ msg_001 │ 1703837000000_userC │ userC │ 同意! │ userB │ cmt_001 │ │
│ └─────────────────┴─────────────────────────┴──────────┴────────────┴──────────────┴──────────────┘ │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ 发送两条通知 │
│ │
│ 1. 通知动态作者 (bigV_001) 2. 通知被回复者 (userB) │
│ ┌────────────────────────────────────┐ ┌────────────────────────────────────┐ │
│ │ type: COMMENT │ │ type: REPLY │ │
│ │ to_user: bigV_001 │ │ to_user: userB │ │
│ │ from_user: userC │ │ from_user: userC │ │
│ │ content: "userC评论了你的动态" │ │ content: "userC回复了你的评论" │ │
│ └────────────────────────────────────┘ └────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────┘
点赞
表结构设计
txt
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ like_table(点赞明细表)- 存储:Tablestore / HBase │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ 主键设计 │
│ ┌─────────────────┬─────────────────────────┬─────────────────────────────────────────────────────────────┐ │
│ │ 列名 │ 类型 │ 说明 │ │
│ ├─────────────────┼─────────────────────────┼─────────────────────────────────────────────────────────────┤ │
│ │ message_id │ String (PK1, 分区键) │ 被点赞的动态ID │ │
│ │ user_id │ String (PK2) │ 点赞者ID,联合主键保证同一用户只能点赞一次 │ │
│ └─────────────────┴─────────────────────────┴─────────────────────────────────────────────────────────────┘ │
│ │
│ 属性列 │
│ ┌─────────────────┬─────────────────────────┬─────────────────────────────────────────────────────────────┐ │
│ │ 列名 │ 类型 │ 说明 │ │
│ ├─────────────────┼─────────────────────────┼─────────────────────────────────────────────────────────────┤ │
│ │ timestamp │ Long │ 点赞时间 │ │
│ │ like_type │ Integer │ 点赞类型:1-👍,2-❤️,3-😄,4-😮,5-😢,6-😠 │ │
│ └─────────────────┴─────────────────────────┴─────────────────────────────────────────────────────────────┘ │
│ │
│ 示例数据 │
│ ┌───────────────────┬──────────────┬─────────────────┬─────────────┐ │
│ │ message_id │ user_id │ timestamp │ like_type │ │
│ ├───────────────────┼──────────────┼─────────────────┼─────────────┤ │
│ │ msg_001 │ userA │ 1703836800000 │ 1 │ │
│ │ msg_001 │ userB │ 1703836900000 │ 1 │ │
│ │ msg_001 │ userC │ 1703837000000 │ 2 │ │
│ │ msg_002 │ userA │ 1703838000000 │ 1 │ │
│ └───────────────────┴──────────────┴─────────────────┴─────────────┘ │
│ │
│ 特点: │
│ - 联合主键 (message_id, user_id) 保证幂等,同一用户对同一动态只能有一条记录 │
│ - 重复点赞:直接覆盖(幂等) │
│ - 取消点赞:删除记录 │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Redis 计数设计(解决热点问题) │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
方案1:简单计数(适合普通动态)
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Key: like_count:{message_id} │
│ Value: 12345 │
│ 操作:INCR(点赞)/ DECR(取消) │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
方案2:分桶计数(适合大V爆款动态,解决热点Key问题)
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ 问题:大V发了爆款动态,短时间100万人点赞,单Key成为热点 │
│ │
│ 解决方案:将计数分散到多个桶 │
│ │
│ Key: like_count:{message_id}:bucket:{0-99} │
│ │
│ 点赞时: │
│ bucket_id = hash(user_id) % 100 │
│ INCR like_count:msg_001:bucket:37 │
│ │
│ 查询时: │
│ 方式1:MGET所有桶,求和 │
│ 方式2:定时任务聚合到主Key,查询主Key │
│ │
│ 示例: │
│ ┌────────────────────────────────────┬─────────────┐ │
│ │ Key │ Value │ │
│ │ like_count:msg_001:bucket:0 │ 1234 │ │
│ │ like_count:msg_001:bucket:1 │ 1289 │ │
│ │ like_count:msg_001:bucket:2 │ 1256 │ │
│ │ ... │ ... │ │
│ │ like_count:msg_001:bucket:99 │ 1301 │ │
│ │ ─────────────────────────────────────────────── │ │
│ │ like_count:msg_001 (聚合值) │ 125000 │ ← 定时任务更新 │
│ └────────────────────────────────────┴─────────────┘ │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
点赞/取消点赞流程
txt
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ 点赞流程 │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
用户A Like Service 数据库 动态作者
│ │ │ │
│ POST /api/like │ │ │
│ { │ │ │
│ message_id: "msg_001", │ │ │
│ action: "LIKE" │ │ │
│ } │ │ │
│─────────────────────────────────>│ │ │
│ │ │ │
│ │ 1. 检查是否已点赞 │ │
│ │────── GET ────────────────────>│ like_table │
│ │ (msg_001, userA) │ │
│ │<───── NULL(未点赞)────────────│ │
│ │ │ │
│ │ 2. 写入点赞记录(幂等写入) │ │
│ │────── PUT ────────────────────>│ like_table │
│ │ (msg_001, userA, 1, now) │ │
│ │ │ │
│ │ 3. 更新点赞计数 │ │
│ │────── INCR ──────────────────>│ Redis │
│ │ like_count:msg_001 │ │
│ │ │ │
│ │ 4. 发送通知(异步) │ │
│ │─────────────────────────────────────────────────────────>│
│ │ "userA赞了你的动态" │ │
│ │ │ │
│<──── 返回成功 ──────────────────│ │ │
│ { liked: true, count: 1235 }│ │ │
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ 取消点赞流程 │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
用户A Like Service 数据库
│ │ │
│ POST /api/like │ │
│ { │ │
│ message_id: "msg_001", │ │
│ action: "UNLIKE" │ │
│ } │ │
│─────────────────────────────────>│ │
│ │ │
│ │ 1. 删除点赞记录 │
│ │────── DELETE ────────────────>│ like_table
│ │ (msg_001, userA) │
│ │ │
│ │ 2. 更新点赞计数 │
│ │────── DECR ─────────────────>│ Redis
│ │ like_count:msg_001 │
│ │ │
│<──── 返回成功 ──────────────────│ │
│ { liked: false, count: 1234}│ │
上述场景涉及到的技术问题
热点key的读写解决方案
txt
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Feed流系统热点Key完整解决方案 │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ │
│ 用户请求 │
│ │ │
│ ▼ │
│ ┌─────────────────────────────┐ │
│ │ 热点检测模块 │ │
│ │ (滑动窗口统计访问频率) │ │
│ └──────────────┬──────────────┘ │
│ │ │
│ ┌──────────────────┴──────────────────┐ │
│ │ │ │
│ 普通Key 热点Key │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ 直接访问Redis │ │ 热点处理模块 │ │
│ └─────────────────┘ └────────┬────────┘ │
│ │ │
│ ┌─────────────────────┴─────────────────────┐ │
│ │ │ │
│ 读请求 写请求 │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────────────┐ ┌──────────────────────────┐ │
│ │ 读热点处理 │ │ 写热点处理 │ │
│ │ │ │ │ │
│ │ 1. 本地缓存(Caffeine) │ │ 1. 分桶计数 │ │
│ │ TTL: 1秒 │ │ 100个桶,分散写压力 │ │
│ │ │ │ │ │
│ │ 2. 多副本读取 │ │ 2. 异步消息队列 │ │
│ │ 随机选择副本 │ │ Kafka削峰 │ │
│ │ │ │ │ │
│ │ 3. 读写分离 │ │ 3. 合并写入 │ │
│ │ 从Slave读取 │ │ 累积后批量写入 │ │
│ └──────────────────────────┘ └──────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
读热点解决方案
- 本地缓存。本地缓存未命中时才访问Redis。
- 读写分离+多副本。
txt
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Redis 读写分离 │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
100万读请求/分钟
│
▼
┌──────────────────────────────────────────────────────────────┐
│ 读负载均衡 │
└──────────────────────────────────────────────────────────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌──────────────┐┌──────────────┐┌──────────────┐┌──────────────┐
│ Slave-1 ││ Slave-2 ││ Slave-3 ││ Slave-4 │
│ (只读) ││ (只读) ││ (只读) ││ (只读) │
└──────────────┘└──────────────┘└──────────────┘└──────────────┘
▲ ▲ ▲ ▲
│ │ │ │
└──────────────┴──────┬───────┴──────────────┘
│ 主从复制
│
┌──────────────┐
│ Master │ <─── 写请求
│ (读写) │
└──────────────┘
优点:
- 读请求分散到多个Slave,线性扩展读能力
- 适合读多写少场景
缺点:
- 主从复制有延迟(毫秒级)
- 架构复杂度增加
- 热点Key复制(Key分片读)
txt
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ 热点Key复制 │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
原始Key:like_count:msg_001 → 单节点承受所有请求
复制为多个Key:
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ │
│ like_count:msg_001:0 → Node-1 │
│ like_count:msg_001:1 → Node-2 │
│ like_count:msg_001:2 → Node-3 │
│ like_count:msg_001:3 → Node-4 │
│ ... │
│ like_count:msg_001:N → Node-N │
│ │
│ 读取时:随机选择一个副本Key读取 │
│ replicaId = random.nextInt(N) │
│ key = "like_count:msg_001:" + replicaId │
│ │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
随机读取还是可能存在
写热点解决方案
- 分桶计数
txt
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ 分桶计数架构 │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
问题:100万人同时点赞,INCR like_count:msg_001 成为单点瓶颈
100万写请求/分钟
│
▼
┌──────────────────────────────────────────────────────────────┐
│ Hash(user_id) % 100 │
└──────────────────────────────────────────────────────────────┘
│ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼
┌──────────┐┌──────────┐┌──────────┐┌──────────┐┌──────────┐┌──────────┐
│ bucket:0 ││ bucket:1 ││ bucket:2 ││ bucket:3 ││ ... ││bucket:99 │
│ INCR ││ INCR ││ INCR ││ INCR ││ INCR ││ INCR │
│ =1234 ││ =1289 ││ =1256 ││ =1301 ││ ... ││ =1278 │
└──────────┘└──────────┘└──────────┘└──────────┘└──────────┘└──────────┘
│ │ │ │ │ │
└──────────┴──────────┴──────────┴──────────┴──────────┘
│
▼
定时聚合(每秒/每分钟)
│
▼
┌──────────────────┐
│ like_count:msg_001│
│ = 125000 │
└──────────────────┘
效果:
- 100个桶,每个桶承担1万QPS
- 写压力分散到不同的Redis节点
- 异步写入 + 消息队列
txt
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ 异步写入架构 │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
100万写请求/分钟
│
▼
┌──────────────────────────────────────────────────────────────┐
│ API Server │
│ (立即返回成功) │
└──────────────────────────────────────────────────────────────┘
│
│ 发送消息(异步)
▼
┌──────────────────────────────────────────────────────────────┐
│ Kafka / RocketMQ │
│ like_topic │
│ (多分区,并行消费) │
└──────────────────────────────────────────────────────────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌──────────────┐┌──────────────┐┌──────────────┐┌──────────────┐
│ Consumer-1 ││ Consumer-2 ││ Consumer-3 ││ Consumer-4 │
│ Partition-0 ││ Partition-1 ││ Partition-2 ││ Partition-3 │
└──────┬───────┘└──────┬───────┘└──────┬───────┘└──────┬───────┘
│ │ │ │
│ 批量合并写入(每100条/每1秒) │
│ │ │ │
└───────────────┴───────┬───────┴───────────────┘
│
▼
┌──────────────┐
│ Redis │
│ Database │
└──────────────┘
优点:
- 削峰填谷,平滑写入压力
- 支持批量合并,减少写入次数
- API快速响应,用户体验好
缺点:
- 数据有延迟(秒级)
- 需要处理消息积压
- 合并写入
txt
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ 合并写入 │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
原理:将多个写请求合并为一个,减少写入次数
T0: INCR like:msg_001 → 累积 +1
T1: INCR like:msg_001 → 累积 +2
T2: INCR like:msg_001 → 累积 +3
...
T99: INCR like:msg_001 → 累积 +100
T100: 合并写入 → INCRBY like:msg_001 100 (一次写入代替100次)
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ │
│ 请求队列 合并器 Redis │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ +1 (user_A) │ │ │ │ │ │
│ │ +1 (user_B) │ ─────────> │ 累积: +100 │ ─── 每100ms ───> │ INCRBY 100 │ │
│ │ +1 (user_C) │ │ │ │ │ │
│ │ ... │ │ │ │ │ │
│ │ +1 (user_N) │ │ │ │ │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
大key的读写解决方案
读大key解决方案
- 水平拆分
txt
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ 拆分大Key - 将一个大Key拆分为多个小Key │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
原始:
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Key: followers:bigV_001 │
│ Type: Set │
│ 元素: [fan_001, fan_002, fan_003, ... fan_5000000] (500万) │
│ 大小: ~50MB │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
│
▼ 拆分
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Key: followers:bigV_001:0 → [fan_000001 ~ fan_050000] (5万) ~500KB │
│ Key: followers:bigV_001:1 → [fan_050001 ~ fan_100000] (5万) ~500KB │
│ Key: followers:bigV_001:2 → [fan_100001 ~ fan_150000] (5万) ~500KB │
│ ... │
│ Key: followers:bigV_001:99 → [fan_4950001 ~ fan_5000000] (5万) ~500KB │
│ │
│ 拆分数量: 100个分片 │
│ 每个分片: 5万元素,约500KB │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
- 垂直拆分
txt
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ 垂直拆分 - 将大对象拆分为多个小对象,按需加载 │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
原始(大对象):
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Key: feed_detail:msg_001 │
│ Value: { │
│ "feed": { "id": "msg_001", "content": "...", "user": {...} }, // 基础信息 ~1KB │
│ "comments": [{...}, {...}, ...], // 评论列表 ~2MB │
│ "likeUsers": [{...}, {...}, ...], // 点赞用户 ~1MB │
│ "shareUsers": [{...}, {...}, ...] // 转发用户 ~1MB │
│ } │
│ 总大小: ~5MB │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
│
▼ 垂直拆分
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Key: feed:msg_001:basic → 基础信息 ~1KB (首屏加载) │
│ Key: feed:msg_001:comments → 评论列表 ~2MB (展开时加载) │
│ Key: feed:msg_001:comments:page:1 → 评论第1页 ~20KB (分页加载) │
│ Key: feed:msg_001:comments:page:2 → 评论第2页 ~20KB │
│ Key: feed:msg_001:like_users → 点赞用户 ~1MB (点击时加载) │
│ Key: feed:msg_001:share_users → 转发用户 ~1MB (点击时加载) │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
- scan分批读取 只需检查 SCAN 返回的游标值是否为0,为0表示迭代完成。
txt
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ 分批读取 - 使用游标分批获取数据,避免一次性加载 │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
错误方式:
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ HGETALL big_hash → 一次性返回10万个字段 → 阻塞Redis → 网络传输慢 │
│ SMEMBERS big_set → 一次性返回100万元素 → OOM风险 → 客户端内存溢出 │
│ LRANGE big_list 0 -1 → 一次性返回全部元素 → 超时 → 请求失败 │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
正确方式:
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ HSCAN big_hash 0 COUNT 1000 → 每次返回约1000个字段 → 多次迭代 → 不阻塞 │
│ SSCAN big_set 0 COUNT 1000 → 每次返回约1000个元素 → 多次迭代 → 内存可控 │
│ LRANGE big_list 0 999 → 分页读取 → 多次请求 → 响应快 │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
- 压缩存储
txt
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ 压缩存储 - 减少Value大小 │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
原始:5MB JSON字符串
压缩后:~500KB(压缩率约90%)
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ │
│ 原始数据 (5MB) │
│ { │
│ "feed": {...}, │
│ "comments": [{...}, {...}, ...], // 1000条评论 │
│ "likeUsers": [{...}, {...}, ...], // 1000个点赞用户 │
│ ... │
│ } │
│ │
│ │ │
│ ▼ GZIP压缩 │
│ │
│ 压缩数据 (~500KB) │
│ H4sIAAAAAAAAA6tWKkktLlGyUlAqS8wpTgUA8KxLFwwAAAA= │
│ │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
写大key解决方案
- 分批写入
txt
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ 分批写入 - 将大量数据分批写入,避免一次性大量写入 │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
错误方式:
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ SADD followers:bigV_001 fan_001 fan_002 ... fan_100000 (一次性添加10万元素) │
│ → 耗时长,阻塞Redis │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
正确方式:
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ 第1批: SADD followers:bigV_001 fan_001 ... fan_1000 (1000个) │
│ 第2批: SADD followers:bigV_001 fan_1001 ... fan_2000 (1000个) │
│ ... │
│ 第100批: SADD followers:bigV_001 fan_99001 ... fan_100000 (1000个) │
│ → 每批耗时短,不阻塞Redis │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘