单库单表撑不住的信号

一张订单表做到几千万行之后,问题开始接连出现:查询越来越慢、写入高峰时锁等待、单表索引膨胀、备份恢复时间长到影响业务。很多团队先做的其实是加索引、读写分离、归档冷数据,这些能把问题往后推,但当数据量和并发继续增长,单台数据库的磁盘、内存和连接数终究会碰到物理上限。

分库分表的本质,是把一台数据库扛不动的压力,分散到多台数据库、多张表上去。它是牺牲部分便利性换取水平扩展能力的手段,因此绝不是越早越好——没有到那个量级就硬拆,只会凭空制造跨库 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、事务、全局唯一,都要自己设计。