分库分表方案中出现数据倾斜怎么解决?——MySQL + Sharding 与 Doris 双视角
分库分表方案中出现数据倾斜怎么解决?——MySQL + Sharding 与 Doris 双视角
面试指北 · 蔚来
参考文章:https://blog.csdn.net/qianshangding0708/article/details/125288754
下面从 MySQL + Sharding(OLTP 分库分表) 和 分布式数据库(以 Apache Doris 为代表的 MPP 架构) 两个层面,把数据倾斜的成因、定位手段、解决方案和监控运维讲清楚。原文的骨架保留,重点补充“为什么会这样”和“具体怎么落地”。
题意
面试官问“分库分表后数据倾斜怎么办”,通常不是想听一句“改分片键”,而是在考察四件事:
- 能不能分清两类倾斜:是存储倾斜(数据量分布不均),还是访问倾斜(数据量均匀但流量集中)。二者根因和解法完全不同,混为一谈基本就暴露了没实操过。
- 懂不懂分片键的取舍:写入要均匀、查询要能路由,这两个目标天然打架,你怎么平衡。
- 会不会分层作答:OLTP 的分库分表(ShardingSphere / MyCat)和 OLAP 的 MPP 数据库(Doris / ClickHouse / StarRocks)解决倾斜的抓手不一样,能不能自己把场景拆开。
- 有没有工程闭环:光说“再平衡”不够,还得说清楚怎么发现倾斜(监控指标)、怎么止血(应急)、怎么做容量规划(预防)。
答题节奏建议:先定义 + 分类 → 再分场景给方案 → 最后补监控与兜底。
结论
一句话版本:
数据倾斜的本质,是“分片/分区/分桶键的设计”与“真实数据分布、真实查询模式”不匹配,导致某几个分片或节点同时承担了过多的存储、写入或计算。解法是一条链:选对键 → 打散热点 → 在线再平衡 → 监控预警。
分场景版本:
- MySQL + Sharding:优先在分片键上做文章(高基数、高离散、高查询覆盖、低变更),配合热点单独拆、历史数据归档、预留分片 + 双倍扩容、读写分离与缓存兜底;倾斜已成事实时靠在线重分片 / 数据迁移纠偏。
- Doris 等 MPP:靠分区(Partition)+ 分桶(Bucket)设计保证 Tablet 均匀,靠 Rebalance / Tablet Move / 扩 BE 纠偏,靠 Rollup / 物化视图 / 分区裁剪减少少数节点上的计算集中。
要点展开
一、MySQL + Sharding 的数据倾斜
1. 为什么会产生倾斜
(1)分片键设计不合理——最常见的一类
- 顺序字段做分片键:用自增主键、时间戳取模或做范围分片,新数据会持续落到同一个“尾部”分片(或同一时间分片),形成单点热分片。尤其是自增主键
% N,如果主键是连续分配的,短时间内落在同一分片的概率并不低,而且写入天然有“时间局部性”。 - 基数太低:用状态、类型、性别这种只有几种取值的字段分片,等于把全表按 3~5 份劈开,每个分片都巨大,同时还可能所有“待支付订单”的查询全打到同一个分片上。
- 分片键与查询维度不匹配:按
order_id分片,但 90% 的查询是“查某个用户的订单列表”。结果每条 SQL 都缺分片键,中间件只能广播到所有分片再聚合,路由成本、连接数、结果归并全被放大,表现得就像“所有分片都在被热点访问”。
(2)业务热点数据访问——访问倾斜
- 头部效应:少数大客户、爆款商品、明星主播的订单/流量远高于均值,它们的分片被反复读写,而其他分片很闲。注意这类倾斜数据量可能是均匀的,但 QPS 极不均匀。
- 瞬时批量写入:秒杀、抢购、批量导入、定时任务,短时间内全部涌向一个分片,打满该分片的连接池和磁盘 IO。
(3)历史数据 + 增量数据不均匀
- 按时间范围分片时,早期分片沉淀了全量历史,新建的分片很空;反过来,如果按“最新时间”路由,又会把写入全压在最新分片。存储空间、备份耗时、DDL 时间都会失衡。
(4)扩容策略滞后
- 业务涨了 3 倍,分片数还是两年前定的,硬件也没加。
- 更关键的是不支持在线重分片:一旦定了
% 16,想扩到% 32就得全量迁移,成本高到没人敢动,于是倾斜就一直烂在那里。
2. 具体可行的解决方案
(1)优化分片键——治本
选分片键可以用“三高一低”来记:
| 维度 | 含义 | 反例 |
|---|---|---|
| 高基数 | 取值足够多,散得开 | 状态、类型、性别 |
| 高离散 | 哈希/取模后分布均匀 | 时间戳、自增 ID(有局部性) |
| 高覆盖 | 大部分核心 SQL 都带这个字段 | 冷门字段,带了等于没带 |
| 低变更 | 业务上基本不更新 | 用户等级、归属地这类会变的 |
具体手段:
- 哈希取模:
user_id % 分片数,让数据均匀落到各分片。 - 复合分片键:主维度哈希 + 次维度(如时间)做库内分区,既保证分散,又便于按时间归档。
- 分布式 ID 替代自增主键:雪花算法(Snowflake)或号段模式,避免插入全部“落单点”。注意雪花 ID 取模同样均匀,但它是趋势递增的,用它做分片键没问题,用它做范围查询要注意。
- 基因法(gene sharding):解决“按 A 分片、按 B 查询”的经典矛盾。比如订单按
user_id分片,但下单后要用order_id反查,可以在生成order_id时把user_id的低若干位“拼”进订单号后缀,这样order_id % 分片数 == user_id % 分片数,用订单号也能直接路由到唯一分片,不用广播。 - 异构索引表:确实存在两个高频查询维度且无法用基因法时,可以建一张按另一维度分片的“索引表”,只冗余少量字段(ID + 路由键),用空间换路由。
ShardingSphere 的分片配置大概长这样(示意):
rules:
- !SHARDING
tables:
t_order:
actualDataNodes: ds_${0..3}.t_order_${0..7}
tableStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: order_inline
shardingAlgorithms:
order_inline:
type: INLINE
props:
algorithm-expression: t_order_${user_id % 8}
同时别忘了配 绑定表(Binding Table)(如 t_order 与 t_order_item 用同一个分片键、同样的分片规则),这样关联查询不会退化成笛卡尔积广播;以及 广播表(如字典表)来解决小表 join。
(2)业务层面打散热点——治标但见效快
- 热表/热用户单独拆分:把访问量极高的大客户、热门商品单独路由到专用分片或专用库。可以做成“白名单 + 路由覆盖”的形式,业务代码里做一次路由判断即可。
- 批量写入乱序/分批:大批量插入先在内存或本地文件里打散顺序,再分批、多线程发送到不同分片,避免长时间压着同一个分片。加一点随机抖动(jitter)也能缓解同一时刻的集中写入。
- 异步化与削峰:写请求进 MQ,消费端按分片维度限流,把瞬时尖峰摊平。
(3)分片再平衡(Re-Sharding)与弹性扩容
- 预留分片:初始化就多建几倍的分片(如 8 库 64 表),后续扩容只是迁移,不用改分片逻辑。
- 双倍扩容:分片数按 2 的幂扩张。原来
% 4,扩到% 8时,原库i的数据只会分裂到i和i+4两个库,只有一半数据需要迁移,而且在迁移期间可以双写/灰度读。这是成本最低的扩容方式,非常值得在面试里点出来。 - 一致性哈希:扩缩容时只影响相邻节点的数据,迁移量小。但要清醒:一致性哈希解决的是“迁移量”,不是“热点”——某个 key 本身特别热,哈希得再均匀还是压在同一个节点上;一般要配虚拟节点来改善分布。
- 分区归档:用 MySQL 分区表按时间分区,历史分区定期
EXCHANGE PARTITION到归档库,保证线上热点分区体量可控。注意 MySQL 单表分区数、以及分区表对唯一索引的限制(唯一索引必须包含分区列)。
(4)缓存与读写分离兜底
- 读写分离:读走从库、写走主库,把查询压力分摊出去。但要盯住主从延迟——倾斜的分片上延迟往往也更大,读到旧数据的概率更高。
- 多级缓存:热点数据放本地缓存(Caffeine)或 Redis,减少打到倾斜分片上的读请求。注意缓存击穿、以及热点 key 在 Redis 集群上同样会倾斜(可以用本地缓存 + key 打散)。
3. 分库分表出现报错如何调整
(1)常见报错场景
- 查询路由失败:SQL 缺分片键、用了跨库子查询、
IN列表过长、OR条件跨分片,中间件无法定位到具体分片。 - 插入/更新失败:分片键值非法或超出实际节点范围(分片数改了但表达式没改),或者某个分片节点宕机导致写失败。
- 主键冲突:没有接入分布式 ID,各分片仍用自增主键,合并或迁移时撞车。
- 迁移过程冲突:重分片时唯一索引冲突、数据版本覆盖。
(2)调整思路(按顺序排查)
- 看路由日志:ShardingSphere 打开 SQL 日志后能看到“逻辑 SQL → 实际 SQL → 目标数据源”,先确认是路由错了还是执行错了。
- 看执行计划:确认是不是广播查询、是不是归并了大量结果集。
- 看节点状态:分片节点是否存活、主从延迟多大、连接池是否被打满。分片层故障会直接表现为大面积读写报错。
- 看数据一致性:迁移/双写场景下,先比对源和目标的行数、校验和,再重试迁移任务;对一致性要求高的,用分布式事务或短暂锁写。
4. 监控与容量规划
(1)必须监控的指标
- 分片粒度的 QPS/TPS、慢 SQL 数量与耗时分布
- 分片数据量与行数(定期统计,是最直接的倾斜信号)
- 连接数、活跃线程数、连接池等待
- CPU / 内存 / 磁盘 IO / 磁盘使用率 / 网络带宽
- 主从延迟(秒级)
- 中间件侧:路由次数、广播查询次数、归并耗时
(2)预警与弹性扩容
- Prometheus + Grafana 采集,按“分片间最大/最小比值”设阈值(比如某分片 QPS 或数据量超过均值 2 倍就告警),比单看绝对值更能发现倾斜。
- 容量水位线:单分片磁盘使用率 > 70% 预警、单表行数接近预设阈值(例如经验值单表 500 万~2000 万行,具体看字段宽度和索引)就启动扩容评估。
- 扩容动作提前演练:迁移脚本、双写开关、回滚方案,都要有。
二、Apache Doris(MPP 分布式数据库)的数据倾斜
1. Doris 数据倾斜的原因
先明确模型:Doris 的表 = Partition(分区,通常按时间 Range/List) × Bucket(分桶,按 Distributed Key 哈希),每个分桶的一个副本就是一个 Tablet,Tablet 落在某个 BE 上。倾斜就发生在“Tablet 分布不均”或“计算落在少数 BE”这两层。
- 分区/分桶策略不合理
- 分桶列基数太低(比如用“省份”“渠道”做分桶),哈希后必然有桶特别大;
- 单分区数据量过大,而分区数又少,导致 Tablet 粒度过粗、无法打散;
- 分桶数设置过少(比如几十亿行只分了 8 个桶),单 Tablet 体积远超合理范围(官方一般建议单 Tablet 在 GB 量级)。 - 数据导入过程导致的倾斜
- 单并发 Stream Load / 单批次 Broker Load 把所有数据推到一个协调节点;
- 导入顺序高度集中(比如按某个低基数列排序后再导入),短时间内全压到少数桶上。 - 查询热点与计算倾斜
- OLAP 场景下,某些大查询反复扫同一个大分区;
- Join 时大表 shuffle 分布不均,个别 instance 处理的数据量远大于其他 instance,整体被木桶效应拖慢;
- 建模不当(缺 Rollup / 物化视图),大量聚合都在明细层实时算。
典型现象:BE 之间磁盘使用率差异明显、SHOW TABLETS 里 Tablet 大小差异悬殊、Query Profile 中某些 instance 的耗时是其他 instance 的数倍。
2. Doris 数据倾斜的解决方案
(1)设计合理的分区列与分桶列
- 分区:优先按时间做 Range 分区,配合动态分区(dynamic partition)自动建/删分区,避免单分区无限膨胀,同时让分区裁剪生效。
- 分桶列:选高基数 + 查询高频的列(如
user_id、order_id),并尽量让分桶列与常用 Join 条件重合,这样才有机会走 Colocate Join(同分布的表本地关联,省掉 shuffle)。 - 分桶数:按“预估数据量 / 单桶目标大小”估算,且尽量取 BE 节点数的整数倍,让 Tablet 能均匀铺到各 BE 上。
示意建表:
CREATE TABLE dwd_order (
dt DATE NOT NULL,
order_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
amount DECIMAL(18,2) NULL
)
DUPLICATE KEY(dt, order_id, user_id)
PARTITION BY RANGE(dt) ()
DISTRIBUTED BY HASH(user_id) BUCKETS 32
PROPERTIES (
"dynamic_partition.enable" = "true",
"dynamic_partition.time_unit" = "DAY",
"dynamic_partition.start" = "-30",
"dynamic_partition.end" = "3",
"replication_num" = "3"
);
(2)导入时打散
- 用多并发 Stream Load 或 Routine Load,把大文件切成多份并行导入;
- 导入前打乱数据顺序,避免同一分桶值连续涌入;
- 合理设置
max_filter_ratio、strict_mode,避免因少量脏数据导致整批失败反复重试。
(3)数据再平衡与存储优化
- Doris 自带副本均衡机制:BE 上下线、磁盘增减会触发 Tablet 调度;也可手动
ADMIN REBALANCE DISK;做磁盘间均衡。 - 下线节点用
ALTER SYSTEM DECOMMISSION BACKEND "be_host:port";让数据平滑迁走,而不是直接停进程。 - 对过期分区做归档 / 删除,减小核心分区体量。
(4)SQL 与索引优化
- 建 Rollup / 物化视图 预聚合,把“少数节点扛全部聚合”的情况前移到导入期;
- 保证分区裁剪生效(where 里带分区列,且不要对分区列做函数运算);
- Join 时小表放右边做 build 端、大表做 probe 端,减少广播与 shuffle 的数据量;
- 实在倾斜严重的超大查询,可以先按分桶列做一次聚合再加总。
3. Doris 常见报错与调整
- 导入报错:出现
too many filtered rows/ 分发异常 / 版本冲突时,先核对分桶列、Key 列与目标表定义是否一致,唯一键表还要注意主键约束;再调max_filter_ratio或分批导入。 - 查询超时、单 BE 负载飙高:多半是分区过大或裁剪失效,检查
EXPLAIN是否扫了全部分区;必要时拆分区、加 Rollup、或扩 BE。 - Rebalance 失败或卡住:先查 BE 磁盘空间是否充足、网络是否正常、Tablet 是否处于不正常副本状态(
SHOW REPLICA STATUS),硬件/网络问题不解决,重试也没用。
4. 监控与调整
- FE / BE 指标:CPU、内存、磁盘 IO、查询延迟、Load 任务状态、连接数。
- Tablet 维度:Tablet 数量与大小分布、副本状态、每个 BE 的 Tablet 数。
- 查询维度:
set enable_profile = true;后拿 Query Profile,看各 instance 的耗时和行数差异,最能直观定位计算倾斜。 - 容量规划:按数据增长趋势提前扩 BE,保持 BE 之间数据量与负载大致持平。
易错点与对比
1. 两类系统的对比
| 维度 | MySQL + Sharding | Doris 等 MPP |
|---|---|---|
| 倾斜的载体 | 分库分表(逻辑分片) | Partition × Bucket(Tablet) |
| 主因 | 分片键设计 + 热点访问 | 分区/分桶列设计 + 数据导入 + Join shuffle |
| 治本手段 | 换分片键、基因法、异构索引表 | 换分桶列、调分桶数、改分区粒度 |
| 纠偏手段 | 在线重分片、双倍扩容、数据迁移 | Rebalance、Tablet Move、扩 BE |
| 查询侧优化 | 绑定表、广播表、读写分离、缓存 | Rollup / 物化视图、Colocate Join、分区裁剪 |
| 监控重点 | 分片 QPS、数据量、主从延迟 | Tablet 分布、BE 负载、Query Profile |
2. 高频易错点
- 把“存储倾斜”和“访问倾斜”混为一谈。数据量均匀不代表没问题,一个大客户就能打爆一个分片。回答时最好主动补一句“我会先确认是哪一类”。
- 以为分片越多越好。分片数暴涨会让跨分片查询、结果归并、连接数、运维成本一起爆炸,通常要留出余量但别提前打满。
- 以为一致性哈希能解决热点。它只减少扩缩容迁移量,热门 key 该热还是热,得靠热点隔离、缓存、key 打散。
- 只加从库、不解决写倾斜。读写分离只分摊读,热点分片的写入压力一点没变。
- 取模扩容不留后路。
% 16 → % 17是灾难,尽量按 2 的幂扩,或一开始就用可动态迁移的方案。 - Doris 分桶列选“基数高但查询不带”的列。数据是均匀了,但查询无法定位桶,全部扫一遍,得不偿失。
- 只监控集群总量、不监控分片/Tablet 粒度。总 QPS 正常,掩盖了某个分片已经满负荷的事实。
小结
- 根本原因统一表述:分片/分区/分桶键与数据分布、访问模式不匹配,且缺乏在线再平衡机制。
- 四条主线:优化分片键(提高基数与离散度、匹配查询维度)→ 业务层打散热点(热数据单独拆、批量乱序写)→ 动态扩容与数据再平衡(预留分片、双倍扩容、Rebalance)→ 监控预警与容量规划(分片粒度指标 + 阈值告警)。
- OLTP 与 OLAP 分开答:MySQL 侧重分片键与迁移成本,Doris 侧重分区分桶设计与 Tablet 均衡、计算侧优化。
- 答题收口:先判断倾斜类型(存储 or 访问),再给止血手段(缓存、隔离、限流)和治本手段(改键、再平衡),最后补上监控与容量规划,形成闭环。
按这个思路回答,既能体现对分库分表机制的掌握,也能覆盖分布式数据库的运维视角,面试里通常能顺着往下聊到“你们怎么扩分片”“Doris 分桶数怎么定”这类深水区问题。