3171 字
约 10 分钟
17
S11-A05 · 数据库扩容决策:从单库到分库分表的判断链

分库分表是最容易被提前消费的方案。它解决的问题很具体——写入吞吐、单表体量、连接数、备份窗口触顶;引入的问题很宽泛——跨分片事务、跨分片 join、全局排序、唯一约束全要另建机制。

正确的顺序不是"要不要分片",而是先证明瓶颈不在更便宜的层面。


1. 先排除四种假瓶颈

假瓶颈 识别方法 典型证据
慢查询 慢日志、digest 聚合 单条 SQL 慢,但 QPS 不高
缺索引 EXPLAIN 看 type/rows type=ALL,扫描行数远超返回行数
锁竞争 锁等待视图、死锁日志 大量 LOCK WAIT,CPU 不饱和
连接数打满 连接数与线程状态 Threads_running 接近上限,取连接超时
-- 按总耗时排序的语句(MySQL 8,performance_schema)
SELECT DIGEST_TEXT, COUNT_STAR, SUM_TIMER_WAIT/1e12 AS total_s
FROM performance_schema.events_statements_summary_by_digest
ORDER BY SUM_TIMER_WAIT DESC LIMIT 10;

SELECT * FROM sys.innodb_lock_waits;   -- 谁在等谁
SHOW STATUS LIKE 'Threads_%';

EXPLAIN SELECT id, amount FROM orders
WHERE user_id = 42 AND status = 'PAID' ORDER BY created_at DESC LIMIT 20;
-- key 为空或 rows 到百万级:先建 (user_id, status, created_at) 复合索引

四种假瓶颈的共同点是修复成本小时级,而分片是月级。跳过这一步,等于用架构改造掩盖一条缺失的索引。


2. 四种手段的前提与代价

手段 适用前提 主要代价
垂直拆分 业务边界清晰、表间 join 少 跨库 join 消失,应用层拼装
读写分离 读多写少、可容忍秒级延迟 复制延迟、读己之写
分库分表 写入吞吐或单表体量触顶 事务、join、排序、唯一约束全要重建
换引擎/换库 瓶颈在存储模型本身 迁移风险、团队经验、生态兼容

垂直拆分与读写分离可逆,分库分表基本不可逆。这个可逆性差异比性能收益更该影响决策。


3. 真正需要分片的信号

信号 量级判据(按实际压测校准) 可否替代
写入吞吐 接近单库 fsync 上限,主从延迟持续扩大 归档冷数据、异步化
单表体量 千万行级以上,索引不再全部常驻内存 分区表、冷热分离
连接数 活跃连接接近 max_connections,加机器无用 连接池、代理
备份窗口 全量备份+恢复超过可接受停机窗口 分表备份、物理备份
判断链(每层都要实测证据,不接受"预计会涨")
1. 单条 SQL 是否已优化到索引覆盖?  否 -> 优化
2. 冷数据可否归档到历史库/对象存储? 否 -> 归档
3. 读压力可否用从库分担?           否 -> 读写分离
4. 写入是否已到单机上限?           是 -> 分片

第 4 步的上限必须来自压测或生产曲线。写入触顶常表现为 commit 延迟上升 + 从库延迟扩大 + IO util 饱和三者同时出现。


4. 分片键的选择

方式 优点 代价 适用
业务维度(user_id/tenant_id) 同主体数据聚合,查询自然收敛 大客户热点 多数 SaaS 业务
哈希(取模/一致性哈希) 分布均匀、扩容可控 范围查询变广播 无主体维度
范围(时间/ID 段) 范围查询高效、易归档 新数据集中写热点 日志、流水、时序
选键三问
1. 最高频查询是否都带这个键?      否 -> 该查询变成跨分片
2. 是否存在超级大客户/大租户?      是 -> 二级拆分或独立库
3. 未来是否需要范围扫描?           是 -> 范围分片 + 索引表

分片键一旦选定,查询边界就被它固定。业务后来多一个"按手机号查订单"的需求,就得额外维护映射表。选键要把未来两年的查询形态列出来,而不是只看当下 SQL。


5. 分片后必然失去的能力

能力 失去原因 替代方案
跨分片事务 单机事务边界消失 本地事务+消息、TCC、Saga
跨分片 join 数据不在同一实例 冗余字段、宽表、应用层拼装
全局排序 各分片各自排序 归并排序、离线宽表
全局唯一约束 唯一索引只在单分片生效 全局 ID、独立去重表
跨分片聚合 count/sum 需汇总 预聚合表、近似计数
-- 跨分片分页:各分片取前 N,应用侧归并,不能直接 offset
SELECT id, amount, created_at FROM orders
WHERE user_id IN (...) ORDER BY created_at DESC LIMIT 20;   -- 在 shard-0..3 各执行

这些能力不是"性能变差",而是语义变了:一条 SQL 的事务保证现在要靠消息与补偿,一个 COUNT(*) 现在要汇总四个数。改造清单必须逐项写明替代方案,否则上线才发现报表算不出来。


6. 扩容时的数据迁移

方式 一致性 回滚难度 适用
双写 应用保证,易漏 低(关开关) 新老结构可并行
影子表 + CDC/触发器 数据库保证 中 不能改应用写入逻辑
在线 DDL 表级,需双倍空间 中 单表结构变更
停机迁移 强一致 低 可接受维护窗口
-- 一致性校验:按分片键取模分组比对聚合值,差异桶再下钻到主键级
SELECT MOD(user_id, 16) AS b, COUNT(*) AS c, SUM(amount) AS s
FROM orders WHERE created_at < '2026-01-01' GROUP BY b;

难点不是搬数据,而是搬完之后新旧还在同时写:双写要处理失败重试与顺序(同一主键后写覆盖前写),校验要能按桶定位差异,而不是只报总数不符。


7. 分布式 ID 与分页的坑

问题 表现 处理
自增 ID 冲突 多分片各自从 1 开始 雪花/号段/中心化自增
深分页 LIMIT 100000, 20 越翻越慢 游标(keyset)分页
跨分片 count 需汇总,慢且易超时 预聚合、近似计数、限范围
ID 有序性 随机 ID 让插入离散 趋势递增(时间前缀)
-- 游标分页:把 offset 换成上一页最后一条的排序键
SELECT id, created_at FROM orders
WHERE user_id = 42 AND created_at < :last_created_at
ORDER BY created_at DESC LIMIT 20;

雪花依赖时钟回拨保护;号段从库取一段缓存,重启会浪费一段;UUID 索引局部性差,不适合做聚簇主键。深分页的根因是数据库要扫描并丢弃前面的行,分片后每片都要丢弃,所以分片与游标分页几乎必须一起上。


8. 读写分离的延迟与读己之写

场景 风险 策略
下单后立刻查订单 从库未同步 写后短窗口走主库
列表页展示刚改的数据 看到旧值 会话粘滞主库一段时间
报表/导出 可容忍延迟 走从库并标注数据截止时间
强一致读 不能容忍 直连主库或改用共识复制
// 读己之写:写操作后把该用户的读请求钉在主库一段时间
if (justWrote(userId)) return master.query(sql);   // 窗口秒级
return replica.query(sql);

延迟是复制模型的固有属性。消除它只有两条路:读主库(放弃分离收益)或用共识复制(放弃异步性能)。"既要分离又要强一致"在异步复制下不成立。


9. NewSQL 与中间件的取舍

方案 改造量 运维复杂度 适用
应用层分片(JDBC SDK) 中,改配置与 SQL 习惯 低,无额外组件 团队能控 SQL
代理层(Proxy) 低,应用透明 中,多一跳与单点 多语言、少改应用
分布式数据库(NewSQL) 低,取决于 SQL 兼容度 高,新体系与新故障模式 强一致+水平扩展刚需
维持单库 + 归档 最低 最低 增长可预期且慢

NewSQL 的诱惑是"不用改应用",代价是把复杂度搬到运维。选型前问三句:跨分片事务是真支持还是看起来支持?扩容重分布要不要停机?故障时谁能定位到具体分片?没有分布式存储排障经验,一次 region 热点就可能烧掉自建分片的全周期预算。


10. 回滚预案

阶段 可回滚性 预案
影子表/双写 高 关开关,回单库读
读切换 高 路由配置回退
写切换 中 反向双写 + 差异修复
停止单库写入 低 需保留单库只读窗口与回灌脚本
回滚检查单
1. 单库是否仍在同步(binlog 消费是否存活)?
2. 反向双写的字段映射是否覆盖新加列?
3. 差异数据能否按主键枚举,而不只是报总数?
4. 回滚后 ID 生成是否会产生重复?

最容易犯的错是切换完成后立刻停掉单库同步,那一步之后回滚就从"改配置"变成"数据救援"。稳妥做法是保持双向同步至少一个完整业务周期(含月末、结算峰值)再评估下线。


11. 排错

现象 原因 处理
分片后查询变慢 查询未带分片键,变广播 补键或建映射表
某分片 CPU 独高 分片键倾斜、大客户 二级拆分或独立库
跨分片事务偶发不一致 消息重试非幂等 加去重表/幂等键
迁移后 count 对不上 软删除条件不一致 统一过滤条件后重算
从库延迟持续扩大 大事务、无主键表、并行复制不足 拆大事务、补主键

12. 边界

场景 更合适 理由
慢查询/缺索引 加索引、改 SQL 小时级修复,零架构风险
读多写少 加从库 + 缓存 可逆,成本线性
冷数据占多数 归档 + 分区表 热数据量降下来问题自然消失
单表千万级但查询简单 分区表 + 覆盖索引 避免跨分片复杂度
强一致 + 水平扩展刚需 NewSQL 应用改造量最小
增长缓慢可预估 单库 + 容量规划 分片收益覆盖不了复杂度

一句话:能靠索引和缓存解决就别分片,能加从库就别分库,能归档就别分区。分片把"一个数据库的问题"变成"一个分布式系统的问题"。


13. 自测

  1. 四种假瓶颈各自的识别证据是什么?为什么必须先排除它们?
  2. 四种扩容手段里哪一种基本不可逆?这个性质如何影响决策顺序?
  3. 判断"写入已到单机上限"时,通常有哪三个信号同时出现?
  4. 分片键选定后,查询边界被怎样固定?选键时要预先列出什么?
  5. 分片后跨分片分页为什么必须改游标?深分页在分片下为何更严重?
  6. 切换完成后立刻停掉单库同步,会把回滚变成什么?
S11-A05 · 数据库扩容决策:从单库到分库分表的判断链
https://www.clxhxhhr.top/posts/6042/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。