数据过载与分布式改造
通用技术科普合集,用「订单 / 用户 / 库存」这类中性例子讲解,不涉及具体公司、框架与业务实现。 内含四块密切相关的主题:分库分表 → 读写分离 → 全局唯一 ID(雪花)→ 分布式事务,它们经常伴随出现,组成一套面对“数据量大、并发高”的改造手段。
目录
零、总览:数据量/并发上来了怎么办
先给一张“先做什么、后做什么”的路线图,避免一上来就上重型方案:
单库单表(瓶颈) ↓ 读并发/连接大 ① 索引 + 缓存 + 读写分离 ← 成本低,先做 ↓ 单表数据量仍大 ② 归档(冷数据迁走)+ 垂直拆分 ← 把表变“收” ↓ 行数依旧巨大/写入瓶颈 ③ 分库分表(水平拆分) ← 最后手段,成本最高 └─带来新问题 → 需要:全局唯一ID + 分布式事务
#### 决策流程图(一次性看懂"先做哪个")
```mermaid
flowchart TD
A[数据量/并发达到了瓶颈?] -->|否| B[(单库单表现状<br/>够用就别动)]
A -->|是| C{先试便宜的}
C --> C1[索引优化 + 缓存]
C1 --> C2[读写分离<br/>主写+从读分担读并发]
C2 --> D{单表数据量/写入<br/>还是扛不住?}
D -->|否| E[归档冷数据 + 垂直拆分<br/>表变收]
E --> D
D -->|是| F[分库分表·水平拆分<br/>选分片键 + 路由算法]
F --> G{分片引发的配套问题}
G -->|ID 撞号| H1[全局唯一ID → 雪花算法]
G -->|跨库一致性| H2[分布式事务<br/>2PC/TCC / 本地消息+MQ / Saga]
G -->|跨分片查询/排序| H3[尽量带分片键 / 中间件合并]
H1 & H2 & H3 --> I[收窄对应方案落地<br/>定期 review 是否该退化成单库]
上图的判断顺序:先用便宜的(索引/缓存/读写分离/归档),不行才上分库分表;分片一旦上了,就要配套解决 ID 唯一、跨库事务与跨分片查询这三类问题。
核心铁律:能不分就不分、优先用便宜的(索引/缓存/读写分离/归档);实在不够才上分库分表;一旦分片,就要配套解决 ID 唯一性和跨库事务。
一、分库分表
1.1 为什么需要
单库单表随数据量增长会遇到三类瓶颈:
| 瓶颈 | 表现 |
|---|---|
| 存储瓶颈 | 表太大,磁盘/单库容量快满 |
| 连接瓶颈 | 并发高,一个库连接被打满、请求排队 |
| 查询瓶颈 | 表太大,索引也不能让查询快 |
1.2 是什么
- 分表:一张表拆成结构相同、按规则分布的 N 张表(
order_0 ~ order_15)。 - 分库:把拆出的表再分布到多个数据库实例。
- 两者常一起做,即“分库分表”。逻辑上仍是“那张 order 表”,物理上拆到了多库多张。
逻辑上:select * from orders where user_id = ?
物理上:orders_03 ← 中间件按 user_id 算出余数,自动路由到真实那张表
1.3 垂直拆分 vs 水平拆分
| 垂直拆分 | 水平拆分 | |
|---|---|---|
| 依据 | 按业务/功能拆字段 | 按数据行拆 |
| 例子 | 用户大表拆成 base/address/auth | orders_0/1/2… |
| 目的 | 表变“收” | 数据量分散 |
| 语境 | 先做 | “分库分表”多指它 |
1.4 分片键与路由
分片键决定“哪个分片进哪张表”,常用路由规则:
| 规则 | 做法 | 特点(原因见下) |
|---|---|---|
| 取模 mod | user_id % 16 按余数 |
简单均匀;扩容要 rehash |
| 范围 range | 按区间/时间分 | 扩容友好;可能热点不均 |
| 一致性哈希 | 一致性哈希 | 扩容迁移少;实现稍复杂 |
为什么“取模:扩容要 rehash”
取模路由的 key 分片是:16 张表 = user_id % 16,余数 0~15 各对应一张表。
一旦分片数从 16 涨到 32,取模的模数变了(%16→%32),同一个 user_id 的余数很可能从 15 变成另一个数字 → 它的数据就会算到“不同的表”。换句话说,改分片数会让大量已有数据的落点变化,必须把老数据按新规则重新搬家(rehash / 数据重分布)。这就是“扩容要 rehash”的成本来源。
代价是对称、均匀、好算,但分片数一变就要全量重新分布,是最“脆”的扩容方式。
为什么“范围:扩容友好、可能热点不均”
按“连续区间“分,如 user_id 0~1实现 → 表0,1001~2000 → 表1,或 时间 2024→2025 → 表0,...:
- 扩容友好:要加新区间,只要新增一张表承接“新来的一段”即可,老数据基本不动(不用 rehash),加表成本低。
- 热点不均:数据往往“新近”扎堆——新增的用户/最新订单都集中在最新那张表,老表几乎闲置。即某几张表很热、其它很冷,不能像取模那样把压力打散。
为什么“一致性哈希:迁移少、实现稍复杂”
一致性哈希把分片放到一个哈希环上,数据哈希后沿环顺时针找到最近的节点。
- 迁移少:加一台机器,只需把该机器与逆时针方向相邻节点之间的数据迁移给它,其余节点不用动——比取模“全量 rehash”好太多。
- 实现稍复杂:要维护哈希环、记录虚拟节点/数据归属,路由计算比取模麻烦;还要处理节点增删时的迁移节奏。
小结:取模最省事但扩容痛;范围好扩容但会热点不均;一致性哈希折中(迁移少但费脑子)。具体选哪个要看“扩容频率”和“数据增长速度”。
选分片键看两点:①每次查询都带得上;②选择最常出现的查询条件(通常尽量靠用户维度 user_id)。
1.5 中间件的“透明”与代价
平时业务像写单表:中间件按分片键路由、拼接、聚合。但必须带分片键,否则只能广播全分片再合并。
代价:
| 难题 | 缓解 |
|---|---|
| 跨分片查询慢 | 尽量带分片键;建映射/索引表 |
| 全局唯一 ID 撞号 | 用雪花 ID(见第三章) |
| 跨分片事务 | 尽量把同一用户绑一个分片;必要时分布式事务(见第四章) |
| 跨分片排序/分页/聚合 | 中间件合并,“取多补一” |
| 扩容 | 一致性哈希、双写迁移 |
心得:分库分表改造和运维成本最高,是“最后手段”。
二、读写分离
2.1 为什么
多数业务读多写少。并发上来,瓶颈常出在“读”——一主一库扛不住大量读,连接被占满连写都被拖累。
2.2 是什么
部署多个实例,写走主库(Master)、读走从库(Slave),从库可多个,分担读并发:
写 → 主库(Master)
用户 -->
读 → 从库1/2/3 (多副本)
2.3 数据怎么同步
主从复制:写主库 → 通过主库的变更日志把变更推给从库 → 从库重放。
写:主库执行 SQL
同步:主库日志推给各从库
读:从库消费日志,与主良趋近一致
因为是异步重放,从库总是比主库略“旧”一点点(毫秒~秒级),这就是“主从延迟”。
2.4 一致性与延迟应对
典型问题:刚写完立刻读,可能读不到(从库没同步完)。解决:
| 场景 | 处理 |
|---|---|
| 写后立刻要读回自己写的内容 | 该请求/该会话强制走主库 |
| 能接受秒级延迟 | 读从库 |
| 一致性要求极严 | 尽量都走主库 |
2.5 什么场景不适合
- 写多读少(分离收益小,费副本)
- 强一致要求(支付扣款、判重)
- 单表已极大(只靠副本解决不了“数据大”,要走分库/归档)
2.6 与分库分表的区别
| 读写分离 | 分库分表 | |
|---|---|---|
| 解决 | 读并发、连接 | 存储容量、数据量级 |
| 结构 | 同一张表多副本 | 拆成多张表/库 |
| 路由 | 读→从、写→主 | 分片键取模 |
| 成本 | 低 | 高 |
| 顺序 | 先做 | 最后做 |
三、全局唯一 ID 与雪花算法
3.1 为什么需要
订单/用户/流水都需要全局唯一标识,并且要:唯一、趋势有序、不依赖单独中心 DB。尤其在分库分表后,各分片各自自增多半会撞号。
3.2 数据库自增的局限
- 分片后各自自增 → 合并撞号;
- 发号中心 → 高并发下成热点/单点。
3.3 常见方案对比
| 方案 | 原理 | 特点 |
|---|---|---|
| 数据库自增 | 单库 | 简单,不适合分片/高并发 |
| Redis 自增 | INCR | 快,依赖缓存 |
| 时间戳+随机 | 拼 | 简单但易撞、长度大 |
| UUID | 本地生成 | 唯一但无序、索引差 |
| 雪花算法 | 时间+机器+序列拼接 | ⭐ 有序+本地生成+高吞吐 |
3.4 雪花算法位结构
64 位 long:
| 1 bit | 41 bit | 10 bit | 12 bit |
| 符号位(0) | 时间戳(毫秒) | 机器/工作ID | 序列号 |
- 时间戳:让 ID 随时间递增 → 单调有序、索引友好;
- 机器位:跨机器/进程唯一;
- 序列位:同一毫秒内自增(单机单毫秒 4096 个)。
3.5 坑与边界
| 坑 | 说明 | 应对 |
|---|---|---|
| 时钟回拨 | 时钟倒退会让 ID 重复 | 记录上次时间,回拨时等待/拒绝 |
| 机器位上限 | 节点数受限(如 1024) | 合理分配 nodeId |
| 非严格全局有序 | 只保证单机有序 | 一般足够 |
| 定长 64 位 | 部分场景要转字符串 | 转字符串处理 |
四、分布式事务
4.1 是什么、为什么难
一个动作跨多个库/服务改数据,希望全成或全败(原子)。单库靠锁+日志能做到,但跨库没有统一事务管理器原子裁定,且网络可能超时/部分提交,所以难。
例子:下单扣库存 + 扣余额 + 加积分,不同库,任一失败都要回滚。
4.2 本地 vs 分布式
| 本地事务 | 分布式事务 | |
|---|---|---|
| 范围 | 单库 | 多库/服务 |
| 靠什么 | DB ACID | 协调/补偿 |
| 性能 | 高 | 有额外开销 |
| 一致性 | 强 | 多只能“最终一致” |
铁律:能不做就不做——把相关数据放同一库,或用最终一致。
4.3 各方案对比
| 方案 | 思路 | 优点 | 缺点 |
|---|---|---|---|
| 2PC 两阶段 | 协调者问各参与者“能提交吗”,全可→提交,否则回滚 | 简单、强同步 | 协调者单点;全程锁资源;网络分区会卡住 |
| TCC | Try-预留 / Confirm-确认 / Cancel-取消 | 比 2PC 好控、粒度细 | 要写三套接口,侵入业务 |
| 本地消息 + MQ | 先写业务+本地消息表(同一本地事务),发 MQ,下游消费,失败重试+对账 | 高性能、低侵入、可扩展 | 秒~分钟级最终一致,需补偿 |
| Saga | 长流程拆本地步骤,每步正向+补偿;失败逆序回滚 | 适合长流程 | 每步手写补偿,多数最终一致 |
最常用是“本地消息 + MQ”:相对落地、带重试和对账,适合能容忍秒级一致的大多数业务。
五、决策速查:一口气选
| 场景 | 推荐 |
|---|---|
| 单库单表、量不大 | 本地事务、数据库自增即可,别折腾 |
| 读并发大 | 先缓存 + 读写分离 |
| 单表数据量巨大 | 分库分表(最后手段) |
| 分片后要让它唯一、有序、高性能 | 雪花算法生成 ID |
| 跨库但要求强同步 | 2PC / TCC |
| 解耦、可容忍秒级一致 | 本地消息 + MQ(最常用) |
| 长流程多步骤 | Saga |
| 本身复杂可规避 | 尽量把数据放同一库 / 拆成消息,少开分布式事务 |
附:速记表
| 概念 | 一句话 |
|---|---|
| 垂直拆分 | 按业务把表拆“收” |
| 水平拆分 | 按行把表拆 N 份(分库分表) |
| 分片键 | 决定行去哪张表的字段,查询必备 |
| 读写分离 | 主写从读,多副本分担读 |
| 主从延迟 | 异步复制,读可能略旧,特殊场景强制走主 |
| 雪花算法 | 时间+机器+序列 拼 64 位,唯一有序 |
| 分布式事务 | 多库同改,全成或全败 |
| 2PC / TCC | 相对强一致,但重/侵入 |
| 本地消息+MQ | 先落本地表再发消息,最终一致(最常用) |
| 铁律 | 能不分就不分;能本地事务就不分布式 |
变更记录
| 版本 | 日期 | 修改人 | 修改内容 |
|---|---|---|---|
| V1 | 2026-10-10 | AI | 合四篇为一:总览 + 分库分表 + 读写分离 + 全局唯一ID(雪花) + 分布式事务 + 决策速查 |
评论