从决策到落地:如何根据业务优雅设计库表

❓ 什么时候需要分库分表?用什么指标判断?

❓ 垂直分还是水平分?分库还是分表?

❓ 分片键怎么选?多个查询维度怎么办?

❓ 父子表、关联表如何处理?

❓ 如何保证数据均匀分布?

❓ 面对新业务,如何系统性思考?

本文建立一套完整的决策框架,让你面对任何业务都能有条不紊地设计出最优方案。

一、决策框架:要不要分?怎么分?

1.1 第一个问题:需要分库分表吗?

7-Blog/后端与微服务/assets/分库分表决策树-69d83a38

在做任何分片决策前,必须先用指标量化问题。 很多团队在需求阶段就因为"感觉要扩展"而过度设计,最后成了维护的噩梦。

性能警戒线指标参考

指标 警戒线 一般应对策略 紧急应对
单表数据量 > 500万 评估分片方案 立即执行水平分片
单表文件大小 > 10GB 评估分片方案 立即执行水平分片
单库QPS > 5000 评估分库方案 立即分库
单库连接数占比 > 80% 评估分库方案 分库隔离压力
单表索引数 > 5个 考虑垂直分表 立即垂直分表
DDL耗时 > 1小时 考虑分片 必须分片

背后的原因解释

  • 500万行数据:MySQL B+树索引深度从3增加到4,查询性能从5ms下降到20ms
  • 10GB 大小:单表备份、恢复、索引重建等运维操作时间倍增,影响发布窗口
  • 5000 QPS:单个库的连接数、内存、磁盘IO接近饱和,响应时间开始波动
  • DDL 1小时:线上表锁定时间过长,影响业务可用性,必须进行分片后再做变更

1.2 分库分表的触发条件

7-Blog/后端与微服务/assets/何时考虑分库分表-b51f4c35

Level 1:不需要分片

  • 数据量 < 500万 行
  • QPS < 2000
  • 无明显业务瓶颈

方案:优化索引、缓存策略、读写分离(主从复制)

成本:低 | 效果:立竿见影


Level 2:需要简单分片

  • 数据量 500万 ~ 2000万
  • QPS 2000 ~ 5000
  • 单一查询维度(如:按user_id查询)

方案:简单哈希分片(ID % N) 或 时间范围分片

成本:中等 | 维护难度:低


Level 3:需要复杂分片

  • 数据量 > 2000万
  • QPS > 5000
  • 多维查询、频繁跨表关联

方案:虚拟槽分片 + 基因法 + 绑定表 + 映射表

成本:高 | 维护难度:高 | 收益:支持大规模业务

1.3 垂直分 vs 水平分

7-Blog/后端与微服务/assets/垂直分 vs 水平分-65b5ddd6

垂直分片:按业务模块拆分(解决耦合)

应用场景

  • 不同业务共用一个数据库,产生耦合(用户、订单、商品、支付混在一起)
  • 单库承载多个业务,每个业务的增长趋势不同
  • 某个业务需要独立的优化策略(如商品库需要缓存,用户库需要高并发)
拆分前(单库耦合):              拆分后(业务隔离):

┌────────────────────┐          ┌──────────────┐

│   app_db(混乱)    │          │  user_db     │

│                    │          │ ├─ d_user    │

│ ├─ d_user          │    →     │ ├─ d_address │

│ ├─ d_order         │          │ └─ d_prefer  │

│ ├─ d_product       │          └──────────────┘

│ ├─ d_inventory     │          ┌──────────────┐

│ └─ d_payment       │          │  order_db    │

└────────────────────┘          │ ├─ d_order   │

                                │ ├─ d_item    │

连接数争抢、故障串联、无法独立优化    │ └─ d_payment │

                                └──────────────┘

                                ┌──────────────┐

                                │  product_db  │

                                │ ├─ d_product │

                                │ └─ d_inventory

                                └──────────────┘

优势:

  ✓ 每个业务独立扩展,无干扰

  ✓ 单库压力分散,连接数可控

  ✓ 故障隔离,一个库挂不影响其他业务

  ✓ 便于独立优化(缓存策略、索引调优)

劣势:

  ✗ 无法解决单表数据量过大的问题

  ✗ 跨库 JOIN 复杂(需要应用层聚合)

  ✗ 跨库事务变复杂(需要分布式事务框架)

垂直分表:按访问模式拆分(解决大宽表)

什么时候用

  • 单表字段过多(> 50个)
  • 字段访问频率差异大(常访问字段 vs 偶尔访问字段)
  • 某些字段频繁更新,导致整个表频繁锁定

垂直分表架构

拆分前(大宽表):                拆分后(按访问模式):

┌──────────────────────┐         ┌───────────────────┐

│     d_user ()      │         │  d_user_base      │

│                      │         │ (热数据,频繁查询) │

│ id (PK)              │    →    │ ├─ id (FK)        │

│ name                 │         │ ├─ name           │

│ email                │         │ ├─ avatar         │

│ phone                │         │ ├─ status         │

│ password             │         │ └─ last_login     │

│ real_name            │         └───────────────────┘

│ id_number            │         ┌───────────────────┐

│ address              │         │  d_user_detail    │

│ age                  │         │ (冷数据,偶尔查询) │

│ gender               │         │ ├─ id (FK)        │

│ ... 40+ 字段         │         │ ├─ real_name      │

│                      │         │ ├─ id_number      │

└──────────────────────┘         │ ├─ address        │

                                 │ ├─ age            │

访问宽表 = 加载40个字段          │ ├─ gender         │

即使只需要3个                    │ └─ education      │

                                 └───────────────────┘

选择对比

对比维度 垂直分库 垂直分表
解决问题 业务耦合、单库压力 大宽表查询性能
实现难度 中等 简单
跨库操作 需要跨库JOIN 需要跨表JOIN
何时应用 业务模块独立清晰时 表字段 > 50时
扩展性

水平分片:按数据行分散(解决单表大)

应用场景

  • 单表数据量已经过大(> 500万)
  • 需要分散写入压力和读取压力
  • 主要查询条件明确(有明确的分片键)

四种分片策略对比

7-Blog/后端与微服务/assets/分片策略对比-c59270d0

1.4 决策矩阵

7-Blog/后端与微服务/assets/拆分方式决策矩阵-01d406ae

二、分片键选择:决定成败的关键

2.1 分片键选择原则

选择分片键是分片设计中最关键的决策,错误的选择会导致大量返工。

7-Blog/后端与微服务/assets/分片键选择的黄金法则-c96a2d35
原则 说明 好例子 ✓ 坏例子 ✗
频繁性 常用查询维度 选择查询中出现频率最高的字段 避免每个查询都做全路由 order_id(订单号) user_id(用户) create_date(创建日期) status(订单状态)
均匀性 数据分布均匀 分片键的值分布要均匀 避免集中在少数分片(热点) user_id(万级用户) product_id(千级商品) region(地区,只有10个值) gender(性别,只有3个值)
稳定性 生成后不可变 分片键值生成后不能改变 否则需要跨分片数据迁移 user_id(注册后永不改) order_id(创建后永不改) user_status(可以改变) age(会随时间变化)
扩展性 支持未来增长 选择能存储足够大范围值的类型 避免数据穿透(溢出) Long (64bit, 可存900万亿) Int (32bit,只可存32亿) Short (16bit,只可存3万)

常见业务表的分片键设计

业务表 推荐分片键 分片数 原因
d_user
(用户表)
user_id 64–128 查询频繁、数据分布均匀、主键稳定
d_order
(订单表)
order_id
+ user_id(基因)
256–512 订单量大、需要精准路由,同时支持用户维度查询
d_order_item
(订单明细)
order_id 同订单表 从属订单表,必须与订单数据同分片
d_product
(商品表)
product_id
+ seller_id(基因)
128–256 查询频繁、ID 值均匀,同时支持商家维度查询
d_user_address
(用户地址)
user_id 同用户表 从属用户数据,保持与用户数据同分片
d_payment
(支付单)
order_id 同订单表 与订单同库同表,支持高效 JOIN
d_log
(操作日志)
user_id
+ time
64–128 流式写入日志,按用户分片便于查询和归档

❌ 常见错误

❌ 错误1:用业务状态字段分片
// 错误做法

public String route(Order order) {

    return order.getStatus() == "PAID" ? "db_0" : "db_1";

}

问题分析:

└─ 订单状态分布:未支付 70% → 已支付 30%

   结果:db_0 有 70% 的数据,db_1 只有 30%

   → 严重数据倾斜,db_0 压力是 db_1 的 2.3 倍

   → 违反"均匀性"原则

解决:用user_id或order_id分片
❌ 错误2:分片键可变
// 错误做法

UPDATE orders SET user_id = ? WHERE order_id = ?;

问题分析:

└─ order_id 原来对应 user_id=100(分配到分片3)

   修改后,order_id 现在对应 user_id=200(应该分到分片2)

   → 数据需要从分片3迁移到分片2

   → 跨分片迁移复杂、容易出错、容易丢数据

现实中:

└─ 一旦分片键确定,通常禁止修改

   → 应用层必须验证分片键不能更新

   → 如果真要改,需要创建新record

解决:选择生成后永不改变的字段
❌ 错误3:分片键字段类型太小
// 错误做法

private short userId;  // 16位,最多65535

private int orderId;   // 32位,最多32亿

问题分析:

└─ 用户ID到达65535后,新用户无法创建(数据穿透)

└─ 订单ID到达32亿后,新订单无法创建(扩容困难)

实际场景:

├─ 抖音日活 7亿,ID至少需要36位

├─ 淘宝年成交 10 亿订单,ID 需要 34位

└─ 保险起见,一律用 Long (64位),可支持 900 万亿

解决:一律使用 Long 类型存储 ID
❌ 错误4:用多字段组合做分片键
// 错误做法

public String route(String department, String team) {

    return "shard_" + department + "_" + team;

}

问题分析:

├─ 组合字段无法被单独拆分

├─ 某时刻需要"按部门查询所有订单"

│  → 无法定位分片,需要扫所有分片

├─ 某时刻需要"按小组查询"

│  → 同样无法定位

└─ 数据分布取决于两个字段的组合

   → 很难保证均匀性

解决:只选一个分片键(主维度)

     其他维度用映射表或基因法

2.2 多维查询问题的识别

7-Blog/后端与微服务/assets/识别多维查询问题-48d20e07

三、多维查询解决方案

3.1 策略总览

7-Blog/后端与微服务/assets/多维查询三大解决策略-f60eac52

问题场景

假设:订单表按 order_id 分片,如何实现这两个查询?

-- 查询1:按订单ID查询(高效 ✓)

SELECT * FROM order WHERE order_id = 123;

→ 订单ID直接定位分片位置

-- 查询2:按用户ID查询(低效 ✗)

SELECT * FROM order WHERE user_id = 456;

→ 不知道user_id=456的订单在哪个分片,需要扫全部分片!

方案对比

7-Blog/后端与微服务/assets/跨分片查询解决方案对比-78f33d69

3.2 策略一:映射表法(Index Table)

核心思想:为第二查询维度建立"索引表",存储映射关系。

7-Blog/后端与微服务/assets/映射表法详解-ec098ff0
表结构设计:

┌──────────────────────────────────────┐

│ d_order (按 order_id 分片)           │

│ ├─ order_id (PK)                     │

│ ├─ user_id                           │

│ ├─ product_id                        │

│ ├─ amount                            │

│ └─ ...                               │

└──────────────────────────────────────┘

            ↑ 按 order_id 分片到 64 张表

┌──────────────────────────────────────┐

│ d_user_order_mapping (索引表)        │

│ (按 user_id 分片到 64 张表)          │

│ ├─ user_id (PK)                      │

│ ├─ order_id (FK → d_order)           │

│ ├─ create_time (排序用)              │

│ └─ ...                               │

└──────────────────────────────────────┘

查询流程:

  1️⃣ SELECT order_id FROM d_user_order_mapping

     WHERE user_id = 456

     → 按 user_id 精准定位分片

     → 得到:order_id = [100, 101, 102]

  2️⃣ SELECT * FROM d_order

     WHERE order_id IN (100, 101, 102)

     → 按 order_id 精准定位分片

     → 得到完整订单数据

优点

  • ✅ 查询精准,无全路由
  • ✅ 支持复杂条件(WHERE user_id = 456 AND status = 'PAID')
  • ✅ 映射表小,容易维护

缺点

  • ❌ 多一次查询,响应时间增加
  • ❌ 需要维护两张表数据一致性
  • ❌ 映射表也需要分片,增加系统复杂度

适用场景

  • ✅ 映射关系是 1:1(一个用户一条记录)
  • ✅ 查询频率不是极高
  • ✅ 能接受多一次往返延迟(通常 10-20ms)

3.3 策略二:基因法(Gene Injection)

核心思想:在生成订单ID时,将用户ID的"特征"编码进去,使两个ID对分片数取模结果一致。

7-Blog/后端与微服务/assets/基因法详解-a1801f4d

数学基础

关键性质:

如果 A 的最后 N 位被替换为 B 的最后 N 位

则 新A % (2^N) = B % (2^N)

具体例子:

假设分片数 = 16(= 2^4),需要用最后 4 位编码用户基因

user_id = 12345

12345 % 16 = 9 (二进制:1001)

order_id 生成过程:

  1. 生成基础 ID(雪花算法):6789012345678901(二进制:...111101012. 提取user_id基因:12345 & 0xF = 9(二进制:10013. 清除基础ID低4位,嵌入user基因:

     ...11110101 → ...11110000 (清除低4)

                → ...11110000 | 1001

                → ...11111001

  4. 最终order_id:6789012345678909

验证:

  6789012345678909 % 16 = 912345 % 16 = 9 ✓

  两个ID索引到同一个分片!

使用效果

生成订单:

order_id = OrderIdGenerator.generate(user_id=456)

// 返回包含user_id特征的order_id

查询1:按订单号查询

SELECT * FROM order WHERE order_id = ?

→ order_id % 16 = 8 → 分片8 ✓

查询2:按用户查询

SELECT * FROM order WHERE user_id = 456456 % 16 = 8 → 分片8 ✓ 同分片!

结果:两个查询都是精准路由,无全路由

优点

  • ✅ 无需额外映射表,存储节省
  • ✅ 完全精准路由,性能最优
  • ✅ 查询逻辑优雅,一张表足够
  • ✅ 支持任意维度(可嵌入多个基因)

缺点

  • ❌ 需要在ID生成阶段提前规划
  • ❌ 事后无法修改分片策略
  • ❌ ID包含业务含义,某些场景不适用

适用场景

  • ✅ 能在项目启动阶段确定查询维度
  • ✅ 数据量大,希望性能最优
  • ✅ 双维度查询(order_id + user_id)

3.4 策略三:冗余同步法

核心思想:在订单表中冗余user_id,然后维护两个方向的索引。

7-Blog/后端与微服务/assets/冗余同步法详解-18074722
表结构:

┌──────────────────────────────────────┐

│ d_order (按 order_id 分片)           │

│ ├─ order_id (PK)  分片键             │

│ ├─ user_id (FK)   冗余字段+索引       │  ← 同时维护基于user_id的索引

│ ├─ product_id                        │

│ ├─ amount                            │

│ └─ ...                               │

└──────────────────────────────────────┘

索引结构:

  主索引:INDEX(order_id)       [用于分片路由]

  副索引:INDEX(user_id)        [用于二维查询]

查询流程:

  1️⃣ SELECT * FROM d_order

     WHERE order_id = 123

     → 使用主索引,精准路由到分片 ✓

  2️⃣ SELECT * FROM d_order

     WHERE user_id = 456

     → 使用副索引,但仍需全路由扫描 ✗

       (因为user_id不是分片键)

优点

  • ✅ 实现简单,数据库层面解决
  • ✅ 无需应用层改造
  • ✅ 冗余数据维护容易

缺点

  • ❌ 第二查询仍需全路由扫描
  • ❌ 存储冗余(虽然不是问题)
  • ❌ 写入时需要维护多个索引,性能略低

适用场景

  • ✅ 第二查询频率较低
  • ✅ 能接受全路由扫描
  • ✅ 数据量不是极大(几千万以内)

3.5 策略选择决策树

7-Blog/后端与微服务/assets/多维查询策略选择决策树-9177d633

四、表关系处理策略

4.1 父子表:同分片设计(绑定表)

关键原则:一对多关系的子表,必须用父表的分片键做自己的分片键,保证数据同分片。

7-Blog/后端与微服务/assets/父子表同分片设计-7168410b
错误设计 ❌:

┌──────────────────────┐         ┌──────────────────────┐

│ d_order              │         │ d_order_item         │

│ (按order_id分片)     │         │ (按item_id分片) ✗    │

├──────────────────────┤         ├──────────────────────┤

│ order_id: 100        │         │ item_id: 1000        │

│ user_id: 5           │         │ order_id: 100 (FK)   │

│ amount: 999          │         │ product_id: 50       │

└──────────────────────┘         │ quantity: 5          │

       ↓ 分片0                   └──────────────────────┘

                                       ↓ 分片3

问题:

  order 在分片0,item 在分片3

  JOIN 时产生笛卡尔积,效率极差

SELECT o.*, oi.*

FROM d_order o

JOIN d_order_item oi ON o.order_id = oi.order_id

WHERE o.order_id = 100;

执行流程:

  1. 从分片0获取order (1条)

  2. 从分片3获取order_item(全表扫描) → 笛卡尔积

  3. 应用层过滤拼接

  效率:⭐

正确设计 ✅:

┌──────────────────────┐         ┌──────────────────────┐

│ d_order              │         │ d_order_item         │

│ (按order_id分片)     │         │ (按order_id分片) ✓   │

├──────────────────────┤         ├──────────────────────┤

│ order_id: 100        │         │ item_id: 1000        │

│ user_id: 5           │         │ order_id: 100 (FK)   │

│ amount: 999          │         │ product_id: 50       │

└──────────────────────┘         │ quantity: 5          │

       ↓ 分片0                   └──────────────────────┘

                                       ↓ 分片0 (同一分片!)

优势:

  order 和 item 都在分片0

  JOIN 在单库内完成,高效

SELECT o.*, oi.*

FROM d_order o

JOIN d_order_item oi ON o.order_id = oi.order_id

WHERE o.order_id = 100;

执行流程:

  1. 从分片0获取order (1条)

  2. 从分片0获取order_item (JOIN查询,索引查找)

  3. 单库JOIN,无笛卡尔积

  效率:⭐⭐⭐⭐⭐

配置为绑定表(ShardingSphere):

rules:

  sharding:

    tables:

      d_order:

        actualDataNodes: ds_${0..1}.d_order_${0..3}

        databaseStrategy:

          standard:

            shardingColumn: order_id

      d_order_item:

        actualDataNodes: ds_${0..1}.d_order_item_${0..3}

        databaseStrategy:

          standard:

            shardingColumn: order_id  # 使用相同分片键

    binding-tables:  # 标记为绑定表

      - d_order,d_order_item

好处:

  ✓ 中间件自动识别,不产生笛卡尔积

  ✓ 查询性能接近单表

4.2 绑定表:优化关联查询

7-Blog/后端与微服务/assets/绑定表详解-233d4625 7-Blog/后端与微服务/assets/绑定表分片设计:父子表同库策略-776cb86a

4.3 广播表:全库同步的小表-处理字典/配置数据

应用场景:小数据量(< 1万)、修改频率低、所有分片都需要的表(字典表、配置表)

7-Blog/后端与微服务/assets/广播表-6416a3bc
广播表示例:

┌──────────────────────────┐

│ d_program_category       │

│ (演出分类表)             │

├──────────────────────────┤

│ category_id: 1           │

│ category_name: "演唱会"   │

│ description: "..."       │

├──────────────────────────┤

│ category_id: 2           │

│ category_name: "话剧"     │

│ description: "..."       │

├──────────────────────────┤

│ ... (只有10-20条记录)     │

└──────────────────────────┘

存储方式:

  同步到 ds_0, ds_1, ds_2, ... 的所有库

  ds_0 中的 d_program_category ✓

  ds_1 中的 d_program_category ✓

  ds_2 中的 d_program_category ✓

查询时:

  本地 JOIN,无需跨库

  SELECT p.*, c.name

  FROM d_program p

  JOIN d_program_category c ON p.category_id = c.id

  WHERE p.program_id = 123

  → 完全在一个分片内完成 ✓

  配置方式:
  rules:

  sharding:

    broadcast-tables:  # 声明为广播表

      - d_program_category

      - d_ticket_category

优点

  • ✅ JOIN 完全本地化,性能最优
  • ✅ 无需应用层处理映射
  • ✅ 写入时自动同步到所有库

缺点

  • ❌ 存储冗余
  • ❌ 修改时需要同步所有库

何时使用

  • ✅ 数据量 < 1万
  • ✅ 修改频率极低(几天一次)
  • ✅ 所有分片都需要(字典、配置)

五、分片算法设计

5.1 分片算法类型

7-Blog/后端与微服务/assets/分片算法类型-dbfb635c

5.2 同频共振问题与层级分片

7-Blog/后端与微服务/assets/同频共振问题-0280f464

简单哈希 vs 层级分片

问题:同频共振(数据倾斜)

场景:2库2表,用简单哈希分片

简单做法(错误):

  分库:id % 2

  分表:id % 2

分布结果:

  id=0: 0%2=0 → db_0, 0%2=0 → table_0  [db_0.table_0] ✓

  id=1: 1%2=1 → db_1, 1%2=1 → table_1  [db_1.table_1] ✓

  id=2: 2%2=0 → db_0, 2%2=0 → table_0  [db_0.table_0] ✓

  id=3: 3%2=1 → db_1, 3%2=1 → table_1  [db_1.table_1] ✓

表格视图:

  ┌─────────────────────┐   ┌─────────────────────┐

  │        db_0         │   │         db_1        │

  ├──────────┬──────────┤   ├──────────┬──────────┤

  │ table_0  │ table_1  │   │ table_0  │ table_1  │

  ├──────────┼──────────┤   ├──────────┼──────────┤

  │ id=0,2,4 │ (空!)   │   │ (空!)   │ id=1,3,5 │

  │ (满)     │          │   │          │ (满)     │

  └──────────┴──────────┘   └──────────┴──────────┘

严重倾斜! 50% 的表完全为空

根本原因:用同一个哈希函数分库和分表

解决方案:层级分片(Hierarchical)

步骤1:计算全局槽位

  总槽位数 = db_count * table_count = 2 * 2 = 4

  id=0 → 0 % 4 = 0(全局槽位0)

  id=1 → 1 % 4 = 1(全局槽位1)

  id=2 → 2 % 4 = 2(全局槽位2)

  id=3 → 3 % 4 = 3(全局槽位3)

步骤2:映射到库和表

  槽位0:db = 0/2 = 0, table = 0%2 = 0 → db_0.table_0

  槽位1:db = 1/2 = 0, table = 1%2 = 1 → db_0.table_1

  槽位2:db = 2/2 = 1, table = 2%2 = 0 → db_1.table_0

  槽位3:db = 3/2 = 1, table = 3%2 = 1 → db_1.table_1

分布结果:

  ┌─────────────────────┐   ┌─────────────────────┐

  │        db_0         │   │         db_1        │

  ├──────────┬──────────┤   ├──────────┬──────────┤

  │ table_0  │ table_1  │   │ table_0  │ table_1  │

  ├──────────┼──────────┤   ├──────────┼──────────┤

  │ id=0,4,8 │ id=1,5,9 │   │ id=2,6,10│ id=3,7,11│

  │ (均匀)    │ (均匀)   │    │ (均匀)   │ (均匀)   │

  └──────────┴──────────┘   └──────────┴──────────┘

完美平衡!✓

5.3 虚拟槽分片:支持弹性扩容

7-Blog/后端与微服务/assets/虚拟槽分片2-bd6bd98e

传统哈希分片的问题:扩容时大量数据需要迁移

初始状态(4个分片):

  分片0:id=0,4,8,12...

  分片1:id=1,5,9,13...

  分片2:id=2,6,10,14...

  分片3:id=3,7,11,15...

需要扩容到8个分片:

  分片0:id=0,8,16,24...  (50%数据需要迁移)

  分片1:id=1,9,17,25...  (50%数据需要迁移)

  分片2:id=2,10,18,26... (50%数据需要迁移)

  分片3:id=3,11,19,27... (50%数据需要迁移)

  分片4:id=4,12,20,28... (新增)

  分片5:id=5,13,21,29... (新增)

  分片6:id=6,14,22,30... (新增)

  分片7:id=7,15,23,31... (新增)

问题:50% 的数据需要迁移,操作风险大

虚拟槽方案:

层级1:ID → 虚拟槽(固定,永不变化)

  总虚拟槽数 = 1024(固定值)

  id → id % 1024 得到槽位号(0-1023)

层级2:虚拟槽 → 物理库(可动态配置)

  初始映射:

    槽位 0-255   → db_0

    槽位 256-511 → db_1

    槽位 512-767 → db_2

    槽位 768-1023→ db_3

  扩容后:

    槽位 0-255   → db_0

    槽位 256-511 → db_5 (新加入)

    槽位 512-767 → db_1

    槽位 768-1023→ db_2

  扩容流程:

    1. 只需迁移"槽位256-511"的数据 (25%)

    2. 更新配置中心映射关系

    3. 无需改任何代码!

配置示例:

  yaml:

    sharding:

      virtual-slots: 1024

      slot-mapping:

        - range: "0-255"

          database: db_0

        - range: "256-511"

          database: db_5  # 修改这一行

        - range: "512-767"

          database: db_1

        - range: "768-1023"

          database: db_2

优势

  • ✅ 扩容时仅需迁移部分数据(通常25%)
  • ✅ 无需修改代码
  • ✅ 配置中心动态更新,即时生效
  • ✅ 支持在线扩容

成本:配置中心(Nacos/Consul)+ 槽位映射表

5.4 性能优化技巧

7-Blog/后端与微服务/assets/分片算法性能优化-dde3a5a9

六、场景示例

场景1:用户中心

表:d_user, d_user_address, d_user_prefer
 分片策略:
├─ 分片键:user_id
├─ 分片数:64(预估3年500万用户)
├─ 分片方式:哈希
├─ 关系设计:d_user_address 和 d_user_prefer 用 user_id 分片
├─ 查询优化:手机号、邮箱登录需要映射表 d_user_mobile, d_user_email
└─ 广播表:d_user_role(用户角色配置,全库同步)

场景2:订单系统

表:d_order, d_order_item, d_order_payment
 分片策略:
├─ 分片键:order_id(包含user_id基因)
├─ 分片数:256(预估35000万订单)
├─ 分片方式:基因法
├─ ID生成:OrderIdGenerator.generate(userId) 嵌入user_id
├─ 关系设计:
│  ├─ d_order_item 按 order_id 分片(与订单同库同表)
│  ├─ d_order_payment 按 order_id 分片(便于同库JOIN)
│  └─ 配置为绑定表
├─ 查询优化:
│  ├─ WHERE order_id = ? → 定位分片
│  ├─ WHERE user_id = ? → user_id % 256 定位(基因法保证正确)
│  └─ WHERE create_date BETWEEN ? → 全路由,需优化
└─ 补偿:
   ├─ 按日期范围的查询 → 维护日期汇总表
   └─ 订单统计 → 离线分析,不走在线库

场景3:商品与库存

表:d_product, d_product_sku, d_inventory, d_inventory_log
 分片策略:
├─ d_product 和 d_product_sku:product_id(seller_id基因)
├─ d_inventory:product_id(与sku绑定)
├─ d_inventory_log:product_id(操作日志)
├─ 分片数:128
├─ 关键设计:
│  └─ inventory 频繁并发更新
│     → 考虑按seller_id二级分片(库数 = seller总数/100)
├─ 查询:
│  ├─ seller查看自己商品 → seller_id定位库,再扫表
│  └─ 用户搜索商品 → ES外挂,不走分片库
└─ 冗余:
   └─ d_product表冗余seller_id(便于按卖家查询)

避免常见坑点

坑1:热点分片

问题示例:
SELECT * FROM order WHERE user_id = 1000000;
// 明星用户/大商家订单多,该分片QPS飙升
 检测:
- 监控各分片QPS分布
- 变异系数 > 0.3 时告警
 解决方案:
┌─────────────────────────────────┐
│ 方案A:二级分片(终极方案)     │
│ 按user_id分库,再按order_id分表 │
│ 使热点分散到多张表              │
├─────────────────────────────────┤
│ 方案B:缓存隔离                  │
│ 热点用户订单缓存到Redis          │
│ 99%查询走缓存,不经过数据库      │
├─────────────────────────────────┤
│ 方案C:数据隔离                  │
│ 活跃订单(最近30天)单独库       │
│ 归档订单在数据仓库               │
└─────────────────────────────────┘

坑2:分片键值分布不均

问题:user_id = 1,2,3,..., 1000000
但实际只有10,000个活跃用户,导致大量分片空闲
 解决:
├─ 监控分片数据量分布,变异系数< 20%
├─ 定期重新评估分片数,过多→合并,过少→拆分
└─ 使用虚拟槽+配置中心,动态调整映射关系

坑3:跨分片事务

问题:
@Transactional
public void payment(Long orderId, Long paymentId) {
    orderDao.updateStatus(orderId, "PAID");      // 分片A
    paymentDao.insert(paymentId);                 // 分片B
}
// 事务不会自动跨分片回滚!
 解决方案:
┌──────────────────────────────────┐
│ 方案A:Seata全局事务(强一致)   │
│ @GlobalTransactional             │
│ 自动处理跨分片回滚               │
├──────────────────────────────────┤
│ 方案B:最终一致(推荐,高性能)  │
│ 1. 本地事务创建订单               │
│ 2. 发送MQ消息                     │
│ 3. 异步消费者创建支付单           │
│ 4. 失败重试,保证最终一致         │
└──────────────────────────────────┘

七、设计验证与检查清单

7.1 数据均匀性验证

/**

 * 数据分布均匀性验证

 */

@Test

public void testDataDistribution() {

    int dbCount = 2;

    int tableCount = 4;

    int totalShards = dbCount * tableCount;

    Map<String, Integer> distribution = new HashMap<>();

    int testCount = 100000;

    for (int i = 0; i < testCount; i++) {

        long id = generateId(); // 模拟业务ID生成

        int globalIndex = (int) (id % totalShards);

        int dbIndex = globalIndex / tableCount;

        int tableIndex = globalIndex % tableCount;

        String shardKey = "ds_" + dbIndex + ".table_" + tableIndex;

        distribution.merge(shardKey, 1, Integer::sum);

    }

    // 验证每个分片的数据量

    int expectedPerShard = testCount / totalShards;

    int tolerance = (int) (expectedPerShard * 0.1); // 10% 容差

    distribution.forEach((shard, count) -> {

        System.out.println(shard + ": " + count);

        assertTrue(

            count >= expectedPerShard - tolerance &&

            count <= expectedPerShard + tolerance,

            "分片 " + shard + " 数据不均匀:" + count

        );

    });

}

7.2 设计检查清单

7-Blog/后端与微服务/assets/分库分表设计检查清单-d41e13be

在提交分片设计前,逐项检查:

分片键检查

  • 分片键是最常用的查询维度吗?
  • 分片键的值分布足够均匀吗?(不是三个大用户占70%)
  • 分片键生成后永不改变吗?(不会UPDATE)
  • 分片键字段类型足够大吗?(Long而非Int)
  • 预留了未来3年的增长空间吗?

多维查询检查

  • 列出所有高频查询维度(>10%)
  • 这些维度的查询路由方案是什么?
  • 是否都能定位分片(无全路由)?
  • 响应时间是否在可接受范围(通常<100ms)?

表关系检查

  • 所有一对多子表都用父表分片键分片吗?
  • 高频JOIN表都配置为绑定表吗?
  • 有没有跨分片JOIN的查询?(应该避免)
  • 小字典表都配置为广播表吗?

数据分布检查

  • 用实际数据验证了分布均匀性吗?(变异系数< 0.2)
  • 有没有热点分片(某个分片QPS是平均值的3倍以上)?
  • 分片数是否为2的幂(便于位运算)?
  • 分片大小是否合理(1000-2000万行最佳)?

扩容计划检查

  • 使用虚拟槽或其他弹性方案了吗?
  • 制定了3-5年的扩容规划吗?
  • 扩容时的数据迁移方案是什么?
  • 扩容对业务的影响是否最小化?

运维监控检查

  • 是否有分片大小监控?(告警阈值:单片>2000万)
  • 是否有分片数据倾斜监控?(告警阈值:变异系数>0.3)
  • 是否有分片QPS不均监控?(告警阈值:最大/平均>2)
  • 是否有热点分片告警?
  • 数据一致性校验方案是什么?

八、完整设计流程

8.1 面对新业务的思考流程

7-Blog/后端与微服务/assets/分库分表设计思考流程-ce15937f

8.2 设计决策流程图

7-Blog/后端与微服务/assets/分库分表设计决策流程-30fb206c

九、完整设计案例:秒杀系统

9.1 需求分析

业务指标:
  ├─ 预期3年GMV:1000亿
  ├─ 单笔订单均价:100元
  ├─ 预计订单数:10亿
  ├─ 峰值QPS:10万
  └─ 主要查询:按订单号、按用户ID、按创建时间

9.2 分片设计过程

STEP 1:计算分片数

10亿订单 ÷ 分片数 = 单片数据量
 推荐单片数据量:1000-2000万行
 10亿 ÷ 2000= 50分片(较保守)
10亿 ÷ 1500= 67分片(推荐)
 选择:64分片(2的幂,便于位运算)
单片数据量:10亿 ÷ 64 = 1562万行(在范围内)

STEP 2:分片键设计

分析查询维度:
  ├─ 查询1:WHERE order_id = ? (30%)
  ├─ 查询2:WHERE user_id = ? (40%)
  ├─ 查询3:WHERE create_time BETWEEN ? (20%)
  └─ 查询4:WHERE status = ? (10%)
 选择:
  主分片键 = order_id(最常用)
  但需要支持user_id查询
 方案:基因法(order_id包含user_id特征)

STEP 3:表关系设计

d_order (订单表)
├─ order_id (PK, 分片键)
├─ user_id (用户ID,基因来源)
├─ seller_id
├─ amount
└─ status
 d_order_item (订单明细)
├─ item_id (PK)
├─ order_id (FK, 分片键)  ← 与d_order使用同分片键
├─ product_id
├─ quantity
└─ price
 d_order_payment (支付单)
├─ payment_id (PK, 包含order_id基因)
├─ order_id (FK, 分片键)  ← 与d_order同分片
├─ amount
└─ status
 配置绑定表:
  binding-tables: [d_order, d_order_item, d_order_payment]

STEP 4:查询优化设计

查询1:按订单号查询
  SELECT * FROM d_order WHERE order_id = 100;
  → order_id 定位分片0
  → 单表查询 ✓✓✓
 查询2:按用户ID查询
  SELECT * FROM d_order WHERE user_id = 456;
  → user_id % 64 = 分片(基因法保证正确)
  → 单表查询 ✓✓✓
 查询3:按时间范围查询
  SELECT * FROM d_order
  WHERE create_time BETWEEN '2024-01-01' AND '2024-01-31';
  → 无分片键,需要全路由扫描 ✗
  → 优化方案:维护d_order_by_date汇总表
 查询4:订单统计
  SELECT status, COUNT(*) FROM d_order GROUP BY status;
  → 全路由聚合 ✗
  → 优化方案:离线分析,T+1统计表

STEP 5:特殊场景处理

热点问题(秒杀明星商品):
  某个product_id的订单集中在少数分片
  → 方案A:缓存热点数据到Redis
  → 方案B:库存单独库,支持更高并发
 库存扣减:
  需要原子性和并发性
  → 方案:库存中间表,支持乐观锁或悲观锁
 对账:
  订单和支付单的一致性校验
  → 方案:离线任务,T+1对账,不走在线库

STEP 6:扩容规划(预留增长空间)

初期架构(预期110亿订单):
  库数:2(ds_0, ds_1)
  单库表数:16(d_order_0 ~ d_order_15)
  总分片数:2 * 16 = 32分片
  单片容量:300万行
 1年后(预期30亿订单):
  库数:4(ds_0, ds_1, ds_2, ds_3)
  单库表数:8个
  总分片数:4 * 8 = 32分片(保持不变!)
  单片容量:900万行
 2年后(预期50亿订单):
  库数:8个
  单库表数:8个
  总分片数:8 * 8 = 64分片
  单片容量:780万行
 关键:使用虚拟槽,扩容时只需更新配置,0代码改动

十、总结:核心原则与最佳实践

7-Blog/后端与微服务/assets/分库分表设计核心原则-32c5a0ab

数据<500万 → 单表+缓存 → ⭐

高频单维度查询 → 简单哈希分片 → ⭐⭐

多维查询,数据3~5年内可规划 → 基因法 → ⭐⭐⭐

多维查询,难以事前规划 → 映射表 → ⭐⭐⭐

频繁突发增长 → 虚拟槽分片 → ⭐⭐⭐⭐

存储海量非结构化数据 → 分库+ES外挂 → ⭐⭐⭐⭐

原则 说明
充要性 没有明确业务诉求,不分片
前置性 ID生成阶段考虑基因法,事后无法修改
一致性 相关表使用相同/兼容的分片键
均衡性 定期监控分布,变异系数< 0.2
可扩展 预留3年增长空间,分片数预留50%
观测性 完善监控告警,及时发现热点

分库分表不是架构银弹。 它解决的是单机数据库的物理极限问题,但带来了分布式系统的复杂性。

最高境界是:业务同学完全不感知分片的存在,框架层面透明地处理所有复杂性。

关键是诊断先行、层级递进、观测驱动。用数据说话,而不是猜测;从简到复,不要过度设计;完善监控,及时调整。