百亿级别的用户关系表,mysql由于大v问题导致数据倾斜严重怎么处理?
百亿级别的用户关系表,mysql由于大v问题导致数据倾斜严重怎么处理?
一、 问题分析:为什么 MySQL 扛不住?
首先,我们要清晰地认识到问题的根源:
- 百亿级别的存储瓶颈:
- 单张 MySQL 表存储百亿行数据,即使做了分库分表,单个分片的数据量依然巨大,B+ 树的层级会非常深,导致查询性能急剧下降。
- 索引本身也会占用巨大的磁盘空间,甚至可能比数据本身还大。
- 数据倾斜(大 V 问题)- 致命伤:
- 粉丝列表(我关注了谁):这个查询相对均匀,因为一个普通用户关注的人数有限(比如几百到几千)。
- 关注者列表(谁关注了我):这是问题的核心。一个明星“大 V”可能有数千万甚至上亿的粉丝。当查询这个大 V 的关注者列表时,所有请求都打到存储这个大 V 数据的那个数据库分片上,形成一个巨大的**热点 (Hotspot)**。
- 这个热点分片会因为 CPU、I/O、网络带宽被占满而变得极度缓慢,甚至崩溃,进而影响到存储在该分片上的其他普通用户。
- 写入风暴(关注/取关操作):
- 当大 V 发布一条动态时,可能会在短时间内引发大量的“关注”操作,形成写入热点。
- 关系复杂性:
- 粉丝关系不仅仅是
A 关注 B
,还涉及到“共同关注”、“好友关系(互相关注)”等复杂查询,这对关系型数据库的 JOIN 操作是巨大考验。
- 粉丝关系不仅仅是
二、 解决方案:分层架构与综合治理
单一技术无法解决这个问题,必须采用组合拳。核心思路是:分类治理,读写分离,冷热分离。
1. 基础存储层:分库分表是前提
百亿数据,分库分表是必须的,但要精细化设计。
表结构设计:
1
2
3
4
5
6
7
8
9
10
11-- 关注关系表 (Follows Table)
CREATE TABLE `follows` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) unsigned NOT NULL COMMENT '粉丝的用户ID',
`follower_id` bigint(20) unsigned NOT NULL COMMENT '被关注者的用户ID (大V的ID)',
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
-- 关键索引:满足两种核心查询
UNIQUE KEY `uk_user_follower` (`user_id`, `follower_id`),
KEY `idx_follower_created` (`follower_id`, `created_at`) -- 查询“谁关注了我”,并按时间排序
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;分片键 (Shard Key) 的选择:
- 查询“我关注了谁”:使用
user_id
作为分片键。SELECT follower_id FROM follows WHERE user_id = ?;
这样查询可以精确路由到单个分片,性能很好。 - 查询“谁关注了我”:使用
follower_id
作为分片键。SELECT user_id FROM follows WHERE follower_id = ?;
- 矛盾出现:一个表只能有一个分片键。如果用
user_id
分片,查询大 V 的粉丝列表就会变成一个广播查询(需要查询所有分片),性能灾难。如果用follower_id
分片,则查询“我关注了谁”又会变成广播查询。
- 查询“我关注了谁”:使用
解决方案:数据冗余/索引表
- 主表:
follows
表,使用user_id
作为分片键,用于存储“关注”关系。 - 索引表:
followers
表,结构与follows
几乎一样,但是使用follower_id
作为分片键,用于存储“被关注”关系。 - 写入操作: 当用户 A 关注用户 B 时,需要同时写入
follows
表和followers
表。这是一种“写扩散”的策略,用写入成本换取查询性能。 - 事务保证: 这两次写入需要保证最终一致性,可以通过分布式事务或更常用的可靠消息队列来实现。
- 主表:
2. 核心解决方案:大 V 数据识别与隔离 (冷热分离)
即使分了表,大 V 的粉丝列表依然可能撑爆一个分片。因此,必须对大 V 进行特殊处理。
- 步骤一:识别大 V
- 离线计算:通过定时任务(如 Spark/Hive)统计每个用户的粉丝数。
- 定义一个阈值,例如粉丝数 > 1000 万的,标记为“大 V”。将大 V 的列表维护在一个配置中心或者 Redis 中。
- 步骤二:读路径隔离 - 缓存优先
- 普通用户:
- 查询“谁关注了我”时,请求路由到
followers
表的对应分片。 - 可以在数据库上层加一层缓存(如 Redis),缓存用户最近的 N 个粉丝。
- 查询“谁关注了我”时,请求路由到
- 大 V 用户(核心):
- 禁止直接查库! 查询大 V 粉丝的请求,绝对不能穿透到后端的 MySQL。
- 数据流向:
- 当发生关注事件时,除了写入 MySQL,同时将这条关注关系发送到**消息队列 (Kafka)**。
- 有一个或多个离线/实时处理服务消费 Kafka 的数据。
- 将大 V 的全量粉丝列表存储在更适合大规模读取的系统中,如 Redis 的 Set/ZSet、HBase、或者**搜索引擎 (Elasticsearch)**。
- 查询路径:
- 当用户请求大 V 的第一页粉丝列表时,直接从 Redis/HBase/ES 中查询。
- 为什么这些系统可以?
- Redis: 内存数据库,速度极快。但存储全量粉丝成本高,适合存储活跃粉丝或最新粉丝。
- HBase (推荐): 分布式 NoSQL 数据库,天然支持海量行存储和范围扫描,没有单点瓶颈,非常适合存储大 V 的全量粉丝列表。RowKey 可以设计为
bigV_id + reverse_timestamp
来支持分页查询。 - Elasticsearch: 不仅能存,还能支持复杂的粉丝搜索(如按昵称、地域搜索)。
- 普通用户:
- 步骤三:写路径优化
- 当大 V 被关注时,写入操作
(user_id, bigV_id)
会集中在followers
表的某个分片上。 - 写操作合并/异步化:
- 将关注请求先写入**消息队列 (Kafka)**。
- 由后端消费者批量地、异步地写入数据库。这样可以削峰填谷,将瞬时的高并发写入压力平摊开。
- 前端可以给用户一个“操作成功”的即时反馈,后台慢慢处理即可。
- 当大 V 被关注时,写入操作
三、 整体架构图
四、 总结方案
- 基础存储层 (MySQL)
- 采用分库分表是基本盘。
- 使用“关注表”(按
user_id
分片)和“粉丝表”(按follower_id
分片)两份数据,通过写扩散解决不同维度的查询问题。
- 缓存层 (Redis)
- 为普通用户缓存粉丝列表、关注列表,提高性能。
- 为大 V 缓存最新或最活跃的一部分粉丝(例如,首页展示的粉丝)。
- 大 V 粉丝存储 (HBase/Elasticsearch)
- 识别大 V: 离线计算粉丝数,定义阈值。
- 存储隔离: 将大 V 的全量粉丝列表从 MySQL 中剥离出来,存储到更适合海量数据扫描的 NoSQL 数据库中(HBase 是一个非常好的选择)。
- 数据同步: 通过消息队列(Kafka)实时同步关注/取关事件,更新 HBase 中的数据。
- 服务层
- 服务层需要有逻辑判断:根据请求的用户 ID,判断其是否为大 V。
- 如果是普通用户,查询缓存 + MySQL。
- 如果是大 V,查询缓存 + HBase/ES。
- 写入优化
- 引入**消息队列 (Kafka)**,将对大 V 的关注/取关操作异步化处理,削峰填谷,保护数据库。
这个方案的核心是“分而治之”和“对症下药”。通过识别出导致问题的“大 V”,并为他们设计一套独立的、更强大的存储和查询链路,从而将他们与普通用户隔离开,保证整个系统的稳定和高性能。
百亿级别的用户关系表,mysql由于大v问题导致数据倾斜严重怎么处理?
https://blog.longpi1.com/2025/09/19/百亿级别的用户关系表,mysql由于大v问题导致数据倾斜严重怎么处理?/