上百亿的用户关系系统存储应该如何设计
上百亿的用户关系系统存储应该如何设计
一、核心挑战分析
首先,我们必须清晰地认识到这个系统的核心挑战是什么:
- 海量数据存储:
- 关注关系:百亿级别。一个用户关注了谁 (
followees
),被谁关注了 (followers
)。这是一个巨大的图(Graph)。 - 动态(Feed)数据:用户发布的内容,百亿甚至千亿级别。
- 关注关系:百亿级别。一个用户关注了谁 (
- 读写模式极不均衡(读多写少):
- 写操作:关注/取关、发布动态。相对低频。
- 读操作:刷新关注页(Feed 流)。这是系统的核心功能,请求量极大,对延迟要求非常高。
- **“明星效应”/ 大 V 问题 (The Celebrity Problem)**:
- 普通用户的粉丝数很少(几十到几百)。
- 大 V(明星、机构)的粉丝数可能达到千万甚至上亿。
- 当一个大 V 发布一条动态时,如何高效地将这条动态推送(或让其可见)给所有粉丝,是一个巨大的挑战。直接操作上亿条记录是不可行的。
- 实时性要求:
- 用户发布动态后,应尽快出现在其粉丝的 Feed 流中。
- 关注/取关操作应立即生效。
二、数据模型与存储选型
1. 关注关系存储
这是一个典型的“多对多”关系。我们需要高效地查询:
get_followees(user_id)
-> 我关注了谁?get_followers(user_id)
-> 谁关注了我?
不建议示范:使用单一的关系型数据库(如 MySQL)。
- 原因:当数据量达到百亿级别时,
following
表会变得无比巨大。即使进行分库分表,对于大 V 的查询(SELECT * FROM following WHERE follower_id = 'celebrity_id'
)仍然是一场灾难,会引发严重的数据库热点和性能问题。
正确选型:面向海量关系查询的 NoSQL 数据库
- 方案:使用 Redis / TiKV / Cassandra 等键值(KV)存储或宽列存储。
- 模型:
- 关注列表 (Following List):
Key
:following:user_id
Value
:ZSET
(有序集合) 或SET
。Score
可以是关注时间戳,Member
是被关注者的user_id
。ZREVRANGE following:user_id 0 -1
可以快速分页获取我关注的人。
- 粉丝列表 (Followers List):
Key
:followers:user_id
Value
:ZSET
或SET
。Member
是关注者的user_id
。
- 关注列表 (Following List):
- 优点:
- 极高的读写性能:KV 查询是 O(1),ZSET 的范围查询也很快。
- 水平扩展:天然支持分布式和水平扩展。
- 大 V 问题:虽然单个 Key 仍然可能很大,但比关系型数据库的热点问题要好管理。Redis Cluster 或 TiKV 等可以自动处理分片。
三、Feed 流(动态推送)架构设计
这是整个系统设计的核心和难点。主要有两种模式:推模式 (Push) 和 拉模式 (Pull),以及结合两者的推拉结合模式。
1. 写时扩散(推模式 / Fan-out on Write)
- 工作原理:当一个用户
A
发布一条动态M
时,系统会立即将这条动态M
的 ID(或完整内容)推送并插入到其所有粉丝的“收件箱(Feed 流时间线)”中。 - 数据模型:
Feed Timeline (收件箱)
:Key: feed:user_id
,Value: ZSET
(Score 是动态发布时间,Member 是动态 ID)。
- 用户读取:用户刷新 Feed 流时,只需读取自己的“收件箱”
feed:user_id
即可。ZREVRANGE feed:user_id 0 49
就能获取最新的 50 条动态。 - 优点:
- **读取极快 (O(1))**:读操作非常简单,延迟低,因为 Feed 流是预先计算好的。对普通用户体验极佳。
- 缺点:
- 写入成本高:写操作被放大。一个有 100 万粉丝的用户发一条动态,就需要进行 100 万次写操作。
- 大 V 灾难:一个有 1 亿粉丝的大 V 发动态,会触发 1 亿次写操作,引发“**写风暴 (Write Storm)**”,可能导致整个系统延迟飙升甚至崩溃。
- 在线粉丝问题:很多粉丝可能是不活跃的,为他们推送属于资源浪费。
2. 读时聚合(拉模式 / Fan-in on Read)
- 工作原理:当一个用户
C
刷新 Feed 流时,系统实时地:- 获取
C
关注的所有人A, B, ...
的列表。 - 分别去获取
A, B, ...
这些人发布的最新动态。 - 将所有动态在内存中进行聚合、排序,然后返回给用户
C
。
- 获取
- 数据模型:
User's Outbox (发件箱)
:Key: outbox:user_id
,Value: ZSET
(存放该用户自己发布的所有动态 ID)。
- 用户读取:一个复杂的多步聚合操作。
- 优点:
- **写入极快 (O(1))**:用户发动态只需要往自己的“发件箱”里写一次即可。大 V 发动态毫无压力。
- 缺点:
- 读取极慢:如果一个用户关注了 2000 人,每次刷新都需要进行 2000 次查询再加一次复杂的内存排序,延迟会非常高,系统无法承受。
- 不适合普通用户。
四、终极方案:推拉结合 (Hybrid Approach) - 应对大V问题的关键
既然纯推和纯拉都有致命缺陷,业界的成熟方案都是将两者结合,针对不同类型的用户采用不同策略。
核心思想:
- 对普通用户:采用推模式。因为他们粉丝少,写扩散成本低,可以保证其粉丝的读取体验。
- 对大 V 用户:采用拉模式。他们的发布操作只写一次,避免写风暴。由其粉丝在读取时主动拉取。
实现细节:
- 用户打标:
- 系统需要一个机制来识别“大 V”。例如,设定一个阈值,粉丝数 > 10 万的被标记为大 V。这个标记可以是用户属性的一部分。
- 混合的发布逻辑:
- 当一个普通用户 U发布动态时:
- 执行推模式逻辑:获取 U 的所有粉丝,将动态 ID 推送到他们的 Feed 时间线中。
- 当一大 V发布动态时:
- 执行拉模式逻辑:只将动态 ID 写入自己的“发件箱 (
outbox:v_user_id
)”。
- 执行拉模式逻辑:只将动态 ID 写入自己的“发件箱 (
- 当一个普通用户 U发布动态时:
- 混合的读取逻辑:
- 当一个用户 C刷新 Feed 流时:
- 读取自己的“收件箱”:
ZREVRANGE feed:c_user_id 0 49
。这里面包含了所有关注的普通用户推送给他的动态。 - 获取
C
关注的大 V 列表:get_v_followees(c_user_id)
。这个列表可以被缓存,因为用户关注的大 V 不会频繁变化。 - 主动拉取大 V 动态:遍历这个大 V 列表,分别去他们的“发件箱”
outbox:v_user_id
中拉取最新的几条动态。 - 最终聚合:将步骤 1 的“收件箱”动态和步骤 3 拉取到的“大 V”动态在应用层或一个专门的聚合服务中进行内存合并、按时间戳排序,然后返回最终的 Feed 流给用户。
- 读取自己的“收件箱”:
- 当一个用户 C刷新 Feed 流时:
针对在线用户的优化 (进一步提升)
为了解决给离线用户推送造成的资源浪费,可以引入一个在线状态服务(可以用 Redis 或其他内存数据库实现)。
- 新的推模式逻辑:当普通用户发布动态时,只向其当前在线的粉丝进行实时推送。
- 对于离线粉丝,可以有几种处理方式:
- 惰性推送:当离线粉丝再次上线时,系统触发一个任务,将他离线期间错过的动态“补”进他的收件箱。
- 拉模式降级:当离线粉丝上线时,让他临时采用拉模式,主动去拉取所有关注者(包括普通用户)的动态来构建首页,之后再切换回正常的推模式。
五、整体架构图
1 |
|
总结
设计这样一个超大规模的系统,关键在于识别核心矛盾并进行拆解和权衡。
- 存储分离:将关系数据(关注)、时间线数据(Feed)、内容数据分离存储,选择最合适的存储引擎。
- 读写分离:通过推拉结合的模式,将读写压力分离。
- 用户分层:对普通用户和大 V 用户采用不同的策略,是解决“明星效应”问题的关键。
- 缓存和异步:在所有可行的环节大量使用缓存(如大 V 列表缓存),并利用消息队列将耗时的推送任务异步化。
- 服务化:将不同功能(用户服务、关系服务、发布服务、Feed流服务)拆分成独立的微服务,便于独立扩展和维护。
这是一个非常复杂的系统,但通过上述分层设计,我们可以构建出一个既能应对海量数据,又能保证核心功能性能和用户体验的健壮系统。
上百亿的用户关系系统存储应该如何设计
https://blog.longpi1.com/2025/09/16/上百亿的用户关系系统存储应该如何设计/