单库单表撑不住的信号
一张订单表做到几千万行之后,问题开始接连出现:查询越来越慢、写入高峰时锁等待、单表索引膨胀、备份恢复时间长到影响业务。很多团队先做的其实是加索引、读写分离、归档冷数据,这些能把问题往后推,但当数据量和并发继续增长,单台数据库的磁盘、内存和连接数终究会碰到物理上限。
分库分表的本质,是把一台数据库扛不动的压力,分散到多台数据库、多张表上去。它是牺牲部分便利性换取水平扩展能力的手段,因此绝不是越早越好——没有到那个量级就硬拆,只会凭空制造跨库 join、分布式事务这些麻烦。
- 水平切分按某个字段把同一张表的数据拆散到多个库表,是扩展主力。
- 分片键决定数据分布是否均匀、查询能否命中单分片。
- 分布式 ID多库后不能共用自增主键,要全局唯一发号。
- 跨片代价非分片键查询会广播到所有片,性能骤降。
垂直拆分与水平切分的区别
"拆"有两个方向,经常被混为一谈。垂直拆分是按业务把不同的表放到不同的库:用户库、订单库、商品库各管一摊,类似微服务在数据层的体现。它解决的是业务耦合与单机容量,拆完之后每张表本身可能还是很大。
水平切分才是真正解决单表过大的手段:把同一张表(比如订单表)按某个字段的取值,拆成 order_0、order_1、order_2 多张结构相同的表,甚至分散到多个库。每条数据根据分片规则落进其中一片。两张思路经常组合使用——先垂直按业务分库,再对最大的那张表水平切分。
分片键:分库分表最关键的决策
选什么字段做分片键,直接决定了这套方案的成败。理想的分片键要满足三点:取值分布均匀(避免热点片)、绝大多数查询都带着它(避免广播)、不会频繁修改(数据一旦按它定位,改键就要迁移数据)。
| 分片方式 | 规则 | 优点 / 缺点 |
|---|---|---|
| 哈希取模 | id % N 决定落哪片 | 分布均匀;扩容要迁移大量数据 |
| 范围分片 | 按 id 区间或时间范围 | 易扩容、便于范围查询;易产生热点 |
| 查表法 | 维护一张分片映射表 | 灵活;多一次查表开销 |
订单场景里,用户 id 是常见选择:同一个用户的订单落在同一片,按用户查订单时只扫一片;而按订单号查、按时间范围统计,就需要跨片汇总。分片键的选择,本质上是在"哪种查询最多"上做取舍。
分片路由:请求该落到哪张表
应用拿到一条 SQL 后,要能根据分片键的值算出它该落到哪个库、哪张表,这就是分片路由。最朴素的写法是在代码里自己算:table = "order_" + (userId % 8)。但手写路由会把业务代码和分片规则搅在一起,所以通常用中间件统一做:应用仍然写逻辑表名 order,中间件在后台自动改写成物理表 order_3 并路由。
-- 应用层看到的逻辑 SQL
SELECT * FROM order WHERE user_id = 10086 AND status = 1;
-- 中间件路由后实际下发到分片 2 的物理表
SELECT * FROM order_2 WHERE user_id = 10086 AND status = 1;
关键在于:查询里必须带分片键。如果一条查询只按时间、不带 user_id,中间件不知道该落哪片,只能把请求广播到全部 8 片再聚合,这种"全片扫描"就是分库分表后最慢的一类查询,要尽量通过业务设计或冗余字段规避。
分布式 ID:不能再用自增主键
单库自增主键在分库后立刻失效:order_0 和 order_1 各自自增,会生成重复 id。因此需要一个全局唯一的发号机制。常见方案有几种:号段模式(一次性从号段库取一段 id 缓存在本地用)、雪花算法(时间戳加机器位加序列号,本地生成、趋势递增)、以及数据库或 Redis 自增。
雪花算法是最常用的折中:不依赖中心服务,本地就能生成,且 id 大致按时间递增,对索引插入友好。它的代价是依赖时钟,机器时间回拨可能造成重复,需要在发号时做时钟检查。
跨片查询与分布式事务的代价
拆分之后,几类原本免费的能力都要重新付出代价。第一是跨片 join:用户表和订单表若分片键不同,关联查询就要在内存里聚合,复杂关联最好在设计时让它们共置一个分片键。第二是跨片分页与排序:全局排序必须各片都取、再归并,深分页代价很高。第三是分布式事务:一次操作跨两个库时,本地事务管不到,要退回上一篇讲的最终一致性方案。
分库分表不是免费的扩展,它把"数据库能帮你做的事"还回给了应用层——join、事务、全局唯一,都要自己设计。
用 ShardingSphere 落地
Apache ShardingSphere 是国内最常用的分库分表中间件,它以 JDBC 驱动或代理的形式接入,应用无感知。以 ShardingSphere-JDBC 为例,你只需配置逻辑表、分片键与分片算法,它就自动完成 SQL 解析、路由、改写与结果归并。
# 逻辑表 order,按 user_id 取模分到 8 张表
rules:
- !SHARDING
tables:
order:
actualDataNodes: ds0.order_${0..7}
tableStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: order_inline
shardingAlgorithms:
order_inline:
type: INLINE
props:
algorithmExpression: order_${user_id % 8}
配置之后,应用代码里照常写 SELECT * FROM order WHERE user_id = ?,中间件自动路由。除了分表,它还能做读写分离、分布式事务、数据脱敏,是一套完整的治理层。
拆分节奏与常见坑
最后是几条实战提醒:
- 能不分就不分:先靠索引、读写分离、归档把问题顶住,真到量级再拆。
- 分片键优先选高频查询必带的字段,并保证数据均匀,避免热点片。
- 提前规划分片数量:取模分片扩容要迁移数据,初期就留足冗余或用一致性哈希。
- 避免不带分片键的查询,确需全局检索时走 Elasticsearch 这类专门的检索系统。
- 拆分要配套数据迁移双写与校验,平滑切换而不是一刀断。
分库分表是数据库扩展里最重的一招,一旦拆下去就回不了头。它换来的是近乎无限的水平扩展,代价是应用层要自己解决 join、事务与全局 id。想清楚哪类查询最多、数据会长到多大,再动手——这比选哪款中间件重要得多。