分库分表怎么做?分片键怎么选?会带来哪些问题?

深入高频场景题约 9 分钟读完

一句话回答

单表大到索引和 DDL、备份都吃力,或者写入超出单机能力时才考虑分库分表;在那之前先用索引优化、缓存、读写分离、归档冷数据来扛。拆分分垂直拆分(按业务拆库、按冷热拆列)和水平拆分(同样的表结构按分片键把行分散到多个库表)。分片键要选查询最常带的、分布均匀的、不会变的字段,比如订单按 user_id。代价是要解决分布式 ID、跨分片查询和分页、跨分片事务和扩容迁移,所以能不拆就不拆。

详细解析

什么时候需要拆,拆之前先试什么

需要拆的信号:

  • 写入瓶颈:单主库的写 TPS、连接数或磁盘 I/O 到顶,读写分离只能分担读,解决不了写
  • 数据量太大:热数据放不进 Buffer Pool,查询频繁读磁盘;加字段、加索引、备份恢复的耗时让人无法接受
  • 容量:数据量接近单机磁盘的上限

常听到的"单表超过 2000 万行就要拆",通常的解释是 3 层 B+ 树的估算(见 B+ 树):假设每行 1KB,3 层大约能放 2000 万行,再多就要 4 层。但上层节点一般都缓存在 Buffer Pool 里,多一层只是多一次查找,性能不会在某个行数突然下降。各种行数阈值都只是特定表结构和硬件下的经验值,真正的依据是监控指标和业务增长预期。

拆之前先试成本低的办法:

  1. SQL 和索引优化,见 慢查询怎么优化
  2. 加缓存挡住读流量,见 缓存一致性
  3. 读写分离,用从库分担读,见 主从复制
  4. 历史数据归档:订单只在线保留近一两年,更早的迁到归档库或数据仓库
  5. 升级硬件,或者用分区表:分区表还在一个实例里,对应用透明,但解决不了单机瓶颈

垂直拆分和水平拆分

做法 解决的问题 例子
垂直分库 按业务把表拆到不同的库 业务之间互相影响、单库压力大 用户库、订单库、商品库,往往和微服务拆分一起做
垂直分表 把一张宽表按列拆成两张 行太宽,每页能放的行少 商品基础信息和大字段的详情描述分开
水平分表 同一个库里,行按规则分到多张结构相同的表 单表太大 t_order_0 ~ t_order_7
水平分库 行分散到多台机器的多个库 单机的写入、容量瓶颈 4 个库,每库 8 张表

垂直拆分是业务层面的边界划分,一般先做;水平拆分才是真正的"分片"。只分表不分库,所有表还在一台机器上,解决不了单机瓶颈。

分片策略

策略 做法 优点 缺点
哈希取模 hash(key) % N 数据和流量分布均匀 扩容要重新分布数据;范围查询要查所有分片
范围 ID 1~1000 万在分片 0,以此类推 扩容只要加新分片,范围查询集中 新数据都写在最后一个分片,形成热点
按时间 按月或按年建表 和归档天然契合,查近期数据快 当月的表最热;跨月查询要查多张表
映射表 单独存"键 → 分片"的对应关系 最灵活,可以单独迁移某个大客户 多一次查询,映射表本身要高可用

哈希取模常和预分片一起用:一开始就分成足够多的逻辑分片(比如 32 张表),初期放在少数几台机器上,扩容时只是把整张表搬到新机器,不用重新计算每一行的位置。按节点数取模的扩容问题,也可以用 一致性哈希 缓解。

分片键怎么选

  • 查询最常带的条件:订单最常按用户查,按 user_id 分片,查"我的订单"只访问一个分片;不带分片键的查询只能广播到所有分片
  • 分布均匀:避免数据和流量倾斜。按商家分片时,一个大商家可能占掉一半订单
  • 不会变:分片键一改,数据就要搬到别的分片
  • 让事务尽量落在一个分片内:同一个用户的订单和订单明细用同一个分片键,事务就不用跨库

另一个维度也要查时,比如商家查自己的订单:可以按 seller_id 再冗余一份数据,通过订阅 binlog 异步同步(见 binlog 的其他用途);复杂的多条件搜索交给 Elasticsearch。按订单号查订单可以用基因法:生成订单号时把 user_id 的低几位嵌进去,订单号和用户 ID 就能路由到同一个分片,见下面的代码。

拆分带来的问题

问题 原因 常见做法
主键冲突 各库的自增 ID 会重复 雪花算法、号段模式,见 分布式 ID
跨分片查询 不带分片键的查询要查所有分片再合并 尽量带分片键;冗余异构表;搜索交给 Elasticsearch
跨分片排序分页 LIMIT 1000, 10 要每个分片都取前 1010 条再归并 限制页数,改成游标分页(见 深分页)
跨分片 JOIN 不同库的表没法直接 JOIN 小的字典表在每个库各放一份;关联的表用相同分片键放在一起;应用层组装
跨分片事务 本地事务管不到别的库 设计上避免;必须时用分布式事务,见 分布式事务
全局唯一约束 唯一索引只在单个分片内有效 用户名这类字段单独建一张按自身分片的唯一索引表
扩容迁移 分片数变化,数据要重新分布 预分片;双写 + 校验后切换
运维 一次 DDL、备份要在几十个库上执行 依赖中间件和自动化工具

用什么实现

  • 客户端分片:如 ShardingSphere-JDBC,以 jar 包形式嵌在 Java 应用里,没有额外的网络跳转,但只能用于 JVM 上的应用
  • 代理分片:如 ShardingSphere-Proxy、Vitess,应用像连普通 MySQL 一样连代理,语言无关,但多一跳网络,代理本身要做高可用
  • 分布式数据库:如 TiDB,兼容 MySQL 协议,数据按范围切成 Region 自动分布和调度,自带分布式事务,应用基本不用关心分片,代价是要接受和 MySQL 不同的性能特征和运维体系

代码示例

4 个库、每库 8 张表,按 user_id 路由,订单号用基因法(Node.js):

JavaScript
const DB_COUNT = 4
const TABLES_PER_DB = 8
const SLOTS = DB_COUNT * TABLES_PER_DB // 32 张分表,取 2 的幂,方便基因法
const GENE_BITS = 5n // 32 = 2^5,取 ID 的低 5 位做"基因"

// 先算落在第几张表,再算这张表在哪个库
// id 可以传字符串或 BigInt,超过 2^53 的 ID 不能用 Number 传,会丢精度
function route(id) {
  const slot = Number(BigInt(id) % BigInt(SLOTS))
  return {
    db: `order_db_${Math.floor(slot / TABLES_PER_DB)}`,
    table: `t_order_${slot % TABLES_PER_DB}`,
  }
}

// seq 来自号段这类发号器;订单号的低 5 位直接取自 user_id
// seq 要小于 2^58,左移 5 位后才不会超出 BIGINT 的范围
function genOrderId(seq, userId) {
  const gene = BigInt(userId) & ((1n << GENE_BITS) - 1n)
  return (BigInt(seq) << GENE_BITS) | gene
}

route(1001) // { db: 'order_db_1', table: 't_order_1' }
route(genOrderId(123456789, 1001)) // 同上,按订单号也能找到同一张表

订单号的低 5 位和 user_id 相同,所以两者对 32 取模的结果一样。前提是分片数是 2 的幂,并且基因位数要按将来可能扩到的分片数预留。

面试官可能追问

扩容时数据怎么迁移才能不停服?

常见做法是双写:先让应用同时写老库和新库,新库写失败不影响主流程,记下来补偿;再把存量数据迁过去,迁移时以老库为准;然后全量校验、修复差异;校验通过后先切一部分读流量到新库,观察没问题再全部切过去,最后停掉老库的写入。每一步都要能回滚。不想改业务代码时,也可以用订阅 binlog 的同步工具把增量同步到新库,代替应用双写。预分片能让迁移简单很多:整张表搬到新机器,不用拆开重新计算。

分区表和分库分表有什么区别?

分区表是 MySQL 自带的功能,一张逻辑表在底层存成多个分区,对应用完全透明,查询带上分区键时只扫描相关分区。但所有分区还在同一个实例里,CPU、磁盘和写入能力的上限没变。另外,表上的每个唯一键(包括主键)都必须包含分区表达式用到的全部列,限制不小。分库分表把数据放到多台机器上,能突破单机上限,代价是应用或中间件要处理路由。

一开始就分库分表,是不是一劳永逸?

不是。过早拆分会让每个查询都要考虑分片键,开发、测试、运维成本都会上升,很多业务的数据量其实永远到不了那个程度。比较稳妥的做法是先做好垂直拆分和读写分离,表设计时把分布式 ID 和分片键考虑进去,等监控显示单库确实扛不住时再做水平拆分。

易错点

  • 只分表不分库,解决的是单表太大的问题,解决不了单机的写入和容量瓶颈
  • 分片键选错的代价很大,事后更换基本等于把所有数据重新迁移一遍
  • 跨分片分页时,偏移量越大,每个分片要多返回的行越多,深分页的问题会成倍放大
  • "单表多少行就要拆"的数字都是经验值,判断要看监控指标和增长趋势

AI 模拟面试官

用自己的话回答,AI 对照参考答案打分、指出遗漏,再追问,最多 3 轮

登录后就可以和 AI 面试官对练,面试记录也会保存下来。登录

这道题你掌握了吗?

选一个最接近的状态,没掌握的题会出现在"我的进度 · 待复习"里。

学习记录暂存在本机浏览器。登录后自动同步到账号,换设备也能看到。