" />
文章

分库分表方案中出现数据倾斜怎么解决?——MySQL + Sharding 与 Doris 双视角

分库分表方案中出现数据倾斜怎么解决?——MySQL + Sharding 与 Doris 双视角

面试指北 · 蔚来

参考文章:https://blog.csdn.net/qianshangding0708/article/details/125288754

下面从 MySQL + Sharding(OLTP 分库分表) 和 分布式数据库(以 Apache Doris 为代表的 MPP 架构) 两个层面,把数据倾斜的成因、定位手段、解决方案和监控运维讲清楚。原文的骨架保留,重点补充“为什么会这样”和“具体怎么落地”。


题意

面试官问“分库分表后数据倾斜怎么办”,通常不是想听一句“改分片键”,而是在考察四件事:

  1. 能不能分清两类倾斜:是存储倾斜(数据量分布不均),还是访问倾斜(数据量均匀但流量集中)。二者根因和解法完全不同,混为一谈基本就暴露了没实操过。
  2. 懂不懂分片键的取舍:写入要均匀、查询要能路由,这两个目标天然打架,你怎么平衡。
  3. 会不会分层作答:OLTP 的分库分表(ShardingSphere / MyCat)和 OLAP 的 MPP 数据库(Doris / ClickHouse / StarRocks)解决倾斜的抓手不一样,能不能自己把场景拆开。
  4. 有没有工程闭环:光说“再平衡”不够,还得说清楚怎么发现倾斜(监控指标)、怎么止血(应急)、怎么做容量规划(预防)。

答题节奏建议:先定义 + 分类 → 再分场景给方案 → 最后补监控与兜底。


结论

一句话版本:

数据倾斜的本质,是“分片/分区/分桶键的设计”与“真实数据分布、真实查询模式”不匹配,导致某几个分片或节点同时承担了过多的存储、写入或计算。解法是一条链:选对键 → 打散热点 → 在线再平衡 → 监控预警。

分场景版本:

  • 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)调整思路(按顺序排查)

  1. 看路由日志:ShardingSphere 打开 SQL 日志后能看到“逻辑 SQL → 实际 SQL → 目标数据源”,先确认是路由错了还是执行错了。
  2. 看执行计划:确认是不是广播查询、是不是归并了大量结果集。
  3. 看节点状态:分片节点是否存活、主从延迟多大、连接池是否被打满。分片层故障会直接表现为大面积读写报错。
  4. 看数据一致性:迁移/双写场景下,先比对源和目标的行数、校验和,再重试迁移任务;对一致性要求高的,用分布式事务或短暂锁写。

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”这两层。

  1. 分区/分桶策略不合理
    - 分桶列基数太低(比如用“省份”“渠道”做分桶),哈希后必然有桶特别大;
    - 单分区数据量过大,而分区数又少,导致 Tablet 粒度过粗、无法打散;
    - 分桶数设置过少(比如几十亿行只分了 8 个桶),单 Tablet 体积远超合理范围(官方一般建议单 Tablet 在 GB 量级)。
  2. 数据导入过程导致的倾斜
    - 单并发 Stream Load / 单批次 Broker Load 把所有数据推到一个协调节点;
    - 导入顺序高度集中(比如按某个低基数列排序后再导入),短时间内全压到少数桶上。
  3. 查询热点与计算倾斜
    - 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 常见报错与调整

  1. 导入报错:出现 too many filtered rows / 分发异常 / 版本冲突时,先核对分桶列、Key 列与目标表定义是否一致,唯一键表还要注意主键约束;再调 max_filter_ratio 或分批导入。
  2. 查询超时、单 BE 负载飙高:多半是分区过大或裁剪失效,检查 EXPLAIN 是否扫了全部分区;必要时拆分区、加 Rollup、或扩 BE。
  3. 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. 高频易错点

  1. 把“存储倾斜”和“访问倾斜”混为一谈。数据量均匀不代表没问题,一个大客户就能打爆一个分片。回答时最好主动补一句“我会先确认是哪一类”。
  2. 以为分片越多越好。分片数暴涨会让跨分片查询、结果归并、连接数、运维成本一起爆炸,通常要留出余量但别提前打满。
  3. 以为一致性哈希能解决热点。它只减少扩缩容迁移量,热门 key 该热还是热,得靠热点隔离、缓存、key 打散。
  4. 只加从库、不解决写倾斜。读写分离只分摊读,热点分片的写入压力一点没变。
  5. 取模扩容不留后路。% 16 → % 17 是灾难,尽量按 2 的幂扩,或一开始就用可动态迁移的方案。
  6. Doris 分桶列选“基数高但查询不带”的列。数据是均匀了,但查询无法定位桶,全部扫一遍,得不偿失。
  7. 只监控集群总量、不监控分片/Tablet 粒度。总 QPS 正常,掩盖了某个分片已经满负荷的事实。

小结

  1. 根本原因统一表述:分片/分区/分桶键与数据分布、访问模式不匹配,且缺乏在线再平衡机制。
  2. 四条主线:优化分片键(提高基数与离散度、匹配查询维度)→ 业务层打散热点(热数据单独拆、批量乱序写)→ 动态扩容与数据再平衡(预留分片、双倍扩容、Rebalance)→ 监控预警与容量规划(分片粒度指标 + 阈值告警)。
  3. OLTP 与 OLAP 分开答:MySQL 侧重分片键与迁移成本,Doris 侧重分区分桶设计与 Tablet 均衡、计算侧优化。
  4. 答题收口:先判断倾斜类型(存储 or 访问),再给止血手段(缓存、隔离、限流)和治本手段(改键、再平衡),最后补上监控与容量规划,形成闭环。

按这个思路回答,既能体现对分库分表机制的掌握,也能覆盖分布式数据库的运维视角,面试里通常能顺着往下聊到“你们怎么扩分片”“Doris 分桶数怎么定”这类深水区问题。

License:  CC BY 4.0