--- title: "03-从决策到落地:如何根据业务优雅设计库表" created: 2025-12-11 aliases: - 从决策到落地:如何根据业务优雅设计库表 tags: - 项目 --- # 从决策到落地:如何根据业务优雅设计库表 > ❓ 什么时候需要分库分表?用什么指标判断? > > ❓ 垂直分还是水平分?分库还是分表? > > ❓ 分片键怎么选?多个查询维度怎么办? > > ❓ 父子表、关联表如何处理? > > ❓ 如何保证数据均匀分布? > > ❓ 面对新业务,如何系统性思考? > > 本文建立一套完整的决策框架,让你面对任何业务都能有条不紊地设计出最优方案。 ## **一、决策框架:要不要分?怎么分?** ### **1.1 第一个问题:需要分库分表吗?** ![[2-Learning/05-项目/08-企业级项目深读/02-damai_pro/04-各模块吃透/01-数据库表关系/02-各模块分库分表是怎么做的/assets/分库分表决策树-69d83a38.jpg]] **在做任何分片决策前,必须先用指标量化问题。** 很多团队在需求阶段就因为"感觉要扩展"而过度设计,最后成了维护的噩梦。 **性能警戒线指标参考**: | 指标 | 警戒线 | 一般应对策略 | 紧急应对 | | --- | --- | --- | --- | | **单表数据量** | > 500万 | 评估分片方案 | 立即执行水平分片 | | **单表文件大小** | > 10GB | 评估分片方案 | 立即执行水平分片 | | **单库QPS** | > 5000 | 评估分库方案 | 立即分库 | | **单库连接数占比** | > 80% | 评估分库方案 | 分库隔离压力 | | **单表索引数** | > 5个 | 考虑垂直分表 | 立即垂直分表 | | **DDL耗时** | > 1小时 | 考虑分片 | **必须分片** | **背后的原因解释**: - **500万行数据**:MySQL B+树索引深度从3增加到4,查询性能从5ms下降到20ms - **10GB 大小**:单表备份、恢复、索引重建等运维操作时间倍增,影响发布窗口 - **5000 QPS**:单个库的连接数、内存、磁盘IO接近饱和,响应时间开始波动 - **DDL 1小时**:线上表锁定时间过长,影响业务可用性,必须进行分片后再做变更 ### **1.2 分库分表的触发条件** ![[2-Learning/05-项目/08-企业级项目深读/02-damai_pro/04-各模块吃透/01-数据库表关系/02-各模块分库分表是怎么做的/assets/何时考虑分库分表-b51f4c35.jpg]] **Level 1:不需要分片** - 数据量 < 500万 行 - QPS < 2000 - 无明显业务瓶颈 **方案**:优化索引、缓存策略、读写分离(主从复制) **成本**:低 | **效果**:立竿见影 --- **Level 2:需要简单分片** - 数据量 500万 ~ 2000万 - QPS 2000 ~ 5000 - 单一查询维度(如:按user\_id查询) **方案**:简单哈希分片(ID % N) 或 时间范围分片 **成本**:中等 | **维护难度**:低 --- **Level 3:需要复杂分片** - 数据量 > 2000万 - QPS > 5000 - 多维查询、频繁跨表关联 **方案**:虚拟槽分片 + 基因法 + 绑定表 + 映射表 **成本**:高 | **维护难度**:高 | **收益**:支持大规模业务 ### **1.3 垂直分 vs 水平分** ![[2-Learning/05-项目/08-企业级项目深读/02-damai_pro/04-各模块吃透/01-数据库表关系/02-各模块分库分表是怎么做的/assets/垂直分 vs 水平分-65b5ddd6.jpg]] #### 垂直分片:按业务模块拆分(解决耦合) **应用场景**: - 不同业务共用一个数据库,产生耦合(用户、订单、商品、支付混在一起) - 单库承载多个业务,每个业务的增长趋势不同 - 某个业务需要独立的优化策略(如商品库需要缓存,用户库需要高并发) ```text 拆分前(单库耦合): 拆分后(业务隔离): ┌────────────────────┐ ┌──────────────┐ │ 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 偶尔访问字段) - 某些字段频繁更新,导致整个表频繁锁定 **垂直分表架构**: ```text 拆分前(大宽表): 拆分后(按访问模式): ┌──────────────────────┐ ┌───────────────────┐ │ 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万) - 需要分散写入压力和读取压力 - 主要查询条件明确(有明确的分片键) **四种分片策略对比**: ![[2-Learning/05-项目/08-企业级项目深读/02-damai_pro/04-各模块吃透/01-数据库表关系/02-各模块分库分表是怎么做的/assets/分片策略对比-c59270d0.jpg]] ### **1.4 决策矩阵** ![[2-Learning/05-项目/08-企业级项目深读/02-damai_pro/04-各模块吃透/01-数据库表关系/02-各模块分库分表是怎么做的/assets/拆分方式决策矩阵-01d406ae.jpg]] ## **二、分片键选择:决定成败的关键** ### **2.1 分片键选择原则** 选择分片键是分片设计中**最关键的决策**,错误的选择会导致大量返工。 ![[2-Learning/05-项目/08-企业级项目深读/02-damai_pro/04-各模块吃透/01-数据库表关系/02-各模块分库分表是怎么做的/assets/分片键选择的黄金法则-c96a2d35.jpg]] | 原则 | 说明 | 好例子 ✓ | 坏例子 ✗ | | --- | --- | --- | --- | | **频繁性** 常用查询维度 | 选择查询中出现频率最高的字段 避免每个查询都做全路由 | 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 | 流式写入日志,按用户分片便于查询和归档 | **❌ 常见错误**: ```java ❌ 错误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分片 ``` ```sql ❌ 错误2:分片键可变 // 错误做法 UPDATE orders SET user_id = ? WHERE order_id = ?; 问题分析: └─ order_id 原来对应 user_id=100(分配到分片3) 修改后,order_id 现在对应 user_id=200(应该分到分片2) → 数据需要从分片3迁移到分片2 → 跨分片迁移复杂、容易出错、容易丢数据 现实中: └─ 一旦分片键确定,通常禁止修改 → 应用层必须验证分片键不能更新 → 如果真要改,需要创建新record 解决:选择生成后永不改变的字段 ``` ```text ❌ 错误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 ``` ```java ❌ 错误4:用多字段组合做分片键 // 错误做法 public String route(String department, String team) { return "shard_" + department + "_" + team; } 问题分析: ├─ 组合字段无法被单独拆分 ├─ 某时刻需要"按部门查询所有订单" │ → 无法定位分片,需要扫所有分片 ├─ 某时刻需要"按小组查询" │ → 同样无法定位 └─ 数据分布取决于两个字段的组合 → 很难保证均匀性 解决:只选一个分片键(主维度) 其他维度用映射表或基因法 ``` ### **2.2 多维查询问题的识别** ![[2-Learning/05-项目/08-企业级项目深读/02-damai_pro/04-各模块吃透/01-数据库表关系/02-各模块分库分表是怎么做的/assets/识别多维查询问题-48d20e07.jpg]] ## **三、多维查询解决方案** ### **3.1 策略总览** ![[2-Learning/05-项目/08-企业级项目深读/02-damai_pro/04-各模块吃透/01-数据库表关系/02-各模块分库分表是怎么做的/assets/多维查询三大解决策略-f60eac52.jpg]] ### 问题场景 **假设**:订单表按 `order_id` 分片,如何实现这两个查询? ```sql -- 查询1:按订单ID查询(高效 ✓) SELECT * FROM order WHERE order_id = 123; → 订单ID直接定位分片位置 -- 查询2:按用户ID查询(低效 ✗) SELECT * FROM order WHERE user_id = 456; → 不知道user_id=456的订单在哪个分片,需要扫全部分片! ``` **方案对比**: ![[2-Learning/05-项目/08-企业级项目深读/02-damai_pro/04-各模块吃透/01-数据库表关系/02-各模块分库分表是怎么做的/assets/跨分片查询解决方案对比-78f33d69.jpg]] ### **3.2 策略一:映射表法(Index Table)** **核心思想**:为第二查询维度建立"索引表",存储映射关系。 ![[2-Learning/05-项目/08-企业级项目深读/02-damai_pro/04-各模块吃透/01-数据库表关系/02-各模块分库分表是怎么做的/assets/映射表法详解-ec098ff0.jpg]] ```sql 表结构设计: ┌──────────────────────────────────────┐ │ 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对分片数取模结果一致。 ![[2-Learning/05-项目/08-企业级项目深读/02-damai_pro/04-各模块吃透/01-数据库表关系/02-各模块分库分表是怎么做的/assets/基因法详解-a1801f4d.jpg]] **数学基础**: ```text 关键性质: 如果 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(二进制:...11110101) 2. 提取user_id基因:12345 & 0xF = 9(二进制:1001) 3. 清除基础ID低4位,嵌入user基因: ...11110101 → ...11110000 (清除低4位) → ...11110000 | 1001 → ...11111001 4. 最终order_id:6789012345678909 验证: 6789012345678909 % 16 = 9 ✓ 12345 % 16 = 9 ✓ 两个ID索引到同一个分片! ``` **使用效果**: ```sql 生成订单: 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 = 456 → 456 % 16 = 8 → 分片8 ✓ 同分片! 结果:两个查询都是精准路由,无全路由 ``` **优点**: - ✅ 无需额外映射表,存储节省 - ✅ 完全精准路由,性能最优 - ✅ 查询逻辑优雅,一张表足够 - ✅ 支持任意维度(可嵌入多个基因) **缺点**: - ❌ 需要在ID生成阶段提前规划 - ❌ 事后无法修改分片策略 - ❌ ID包含业务含义,某些场景不适用 **适用场景**: - ✅ 能在项目启动阶段确定查询维度 - ✅ 数据量大,希望性能最优 - ✅ 双维度查询(order\_id + user\_id) ### **3.4 策略三:冗余同步法** **核心思想**:在订单表中冗余user\_id,然后维护两个方向的索引。 ![[2-Learning/05-项目/08-企业级项目深读/02-damai_pro/04-各模块吃透/01-数据库表关系/02-各模块分库分表是怎么做的/assets/冗余同步法详解-18074722.jpg]] ```sql 表结构: ┌──────────────────────────────────────┐ │ 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 策略选择决策树** ![[2-Learning/05-项目/08-企业级项目深读/02-damai_pro/04-各模块吃透/01-数据库表关系/02-各模块分库分表是怎么做的/assets/多维查询策略选择决策树-9177d633.jpg]] ## **四、表关系处理策略** ### **4.1 父子表:同分片设计(绑定表)** **关键原则**:一对多关系的子表,必须用父表的分片键做自己的分片键,保证数据同分片。 ![[2-Learning/05-项目/08-企业级项目深读/02-damai_pro/04-各模块吃透/01-数据库表关系/02-各模块分库分表是怎么做的/assets/父子表同分片设计-7168410b.jpg]] ```sql 错误设计 ❌: ┌──────────────────────┐ ┌──────────────────────┐ │ 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 绑定表:优化关联查询** ![[2-Learning/05-项目/08-企业级项目深读/02-damai_pro/04-各模块吃透/01-数据库表关系/02-各模块分库分表是怎么做的/assets/绑定表详解-233d4625.jpg]] ![[2-Learning/05-项目/08-企业级项目深读/02-damai_pro/04-各模块吃透/01-数据库表关系/02-各模块分库分表是怎么做的/assets/绑定表分片设计:父子表同库策略-776cb86a.jpg]] ### **4.3 广播表:全库同步的小表-处理字典/配置数据** **应用场景**:小数据量(< 1万)、修改频率低、所有分片都需要的表(字典表、配置表) ![[2-Learning/05-项目/08-企业级项目深读/02-damai_pro/04-各模块吃透/01-数据库表关系/02-各模块分库分表是怎么做的/assets/广播表-6416a3bc.jpg]] ```sql 广播表示例: ┌──────────────────────────┐ │ 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 分片算法类型** ![[2-Learning/05-项目/08-企业级项目深读/02-damai_pro/04-各模块吃透/01-数据库表关系/02-各模块分库分表是怎么做的/assets/分片算法类型-dbfb635c.jpg]] ### **5.2 同频共振问题与层级分片** ![[2-Learning/05-项目/08-企业级项目深读/02-damai_pro/04-各模块吃透/01-数据库表关系/02-各模块分库分表是怎么做的/assets/同频共振问题-0280f464.jpg]] #### 简单哈希 vs 层级分片 **问题:同频共振(数据倾斜)** ```text 场景: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 虚拟槽分片:支持弹性扩容** ![[2-Learning/05-项目/08-企业级项目深读/02-damai_pro/04-各模块吃透/01-数据库表关系/02-各模块分库分表是怎么做的/assets/虚拟槽分片2-bd6bd98e.jpg]] **传统哈希分片的问题**:扩容时大量数据需要迁移 ```yaml 初始状态(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 性能优化技巧** ![[2-Learning/05-项目/08-企业级项目深读/02-damai_pro/04-各模块吃透/01-数据库表关系/02-各模块分库分表是怎么做的/assets/分片算法性能优化-dde3a5a9.jpg]] ## **六、场景示例** **场景1:用户中心** ```text 表: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:订单系统** ```text 表:d_order, d_order_item, d_order_payment 分片策略: ├─ 分片键:order_id(包含user_id基因) ├─ 分片数:256(预估3年5000万订单) ├─ 分片方式:基因法 ├─ 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:商品与库存** ```text 表: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:热点分片** ```sql 问题示例: SELECT * FROM order WHERE user_id = 1000000; // 明星用户/大商家订单多,该分片QPS飙升 检测: - 监控各分片QPS分布 - 变异系数 > 0.3 时告警 解决方案: ┌─────────────────────────────────┐ │ 方案A:二级分片(终极方案) │ │ 按user_id分库,再按order_id分表 │ │ 使热点分散到多张表 │ ├─────────────────────────────────┤ │ 方案B:缓存隔离 │ │ 热点用户订单缓存到Redis │ │ 99%查询走缓存,不经过数据库 │ ├─────────────────────────────────┤ │ 方案C:数据隔离 │ │ 活跃订单(最近30天)单独库 │ │ 归档订单在数据仓库 │ └─────────────────────────────────┘ ``` **坑2:分片键值分布不均** ```text 问题:user_id = 1,2,3,..., 1000000 但实际只有10,000个活跃用户,导致大量分片空闲 解决: ├─ 监控分片数据量分布,变异系数< 20% ├─ 定期重新评估分片数,过多→合并,过少→拆分 └─ 使用虚拟槽+配置中心,动态调整映射关系 ``` **坑3:跨分片事务** ```java 问题: @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 数据均匀性验证** ```java /** * 数据分布均匀性验证 */ @Test public void testDataDistribution() { int dbCount = 2; int tableCount = 4; int totalShards = dbCount * tableCount; Map 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 设计检查清单** ![[2-Learning/05-项目/08-企业级项目深读/02-damai_pro/04-各模块吃透/01-数据库表关系/02-各模块分库分表是怎么做的/assets/分库分表设计检查清单-d41e13be.jpg]] 在提交分片设计前,逐项检查: #### 分片键检查 - 分片键是最常用的查询维度吗? - 分片键的值分布足够均匀吗?(不是三个大用户占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 面对新业务的思考流程** ![[2-Learning/05-项目/08-企业级项目深读/02-damai_pro/04-各模块吃透/01-数据库表关系/02-各模块分库分表是怎么做的/assets/分库分表设计思考流程-ce15937f.jpg]] ### **8.2 设计决策流程图** ![[2-Learning/05-项目/08-企业级项目深读/02-damai_pro/04-各模块吃透/01-数据库表关系/02-各模块分库分表是怎么做的/assets/分库分表设计决策流程-30fb206c.jpg]] ## **九、完整设计案例:秒杀系统** ### 9.1 需求分析 ```text 业务指标: ├─ 预期3年GMV:1000亿 ├─ 单笔订单均价:100元 ├─ 预计订单数:10亿 ├─ 峰值QPS:10万 └─ 主要查询:按订单号、按用户ID、按创建时间 ``` ### 9.2 分片设计过程 **STEP 1:计算分片数** ```text 10亿订单 ÷ 分片数 = 单片数据量 推荐单片数据量:1000-2000万行 10亿 ÷ 2000万 = 50分片(较保守) 10亿 ÷ 1500万 = 67分片(推荐) 选择:64分片(2的幂,便于位运算) 单片数据量:10亿 ÷ 64 = 1562万行(在范围内) ``` **STEP 2:分片键设计** ```text 分析查询维度: ├─ 查询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:表关系设计** ```text 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:查询优化设计** ```sql 查询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:特殊场景处理** ```text 热点问题(秒杀明星商品): 某个product_id的订单集中在少数分片 → 方案A:缓存热点数据到Redis → 方案B:库存单独库,支持更高并发 库存扣减: 需要原子性和并发性 → 方案:库存中间表,支持乐观锁或悲观锁 对账: 订单和支付单的一致性校验 → 方案:离线任务,T+1对账,不走在线库 ``` **STEP 6:扩容规划(预留增长空间)** ```text 初期架构(预期1年10亿订单): 库数: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代码改动 ``` ## **十、总结:核心原则与最佳实践** ![[2-Learning/05-项目/08-企业级项目深读/02-damai_pro/04-各模块吃透/01-数据库表关系/02-各模块分库分表是怎么做的/assets/分库分表设计核心原则-32c5a0ab.jpg]] > 数据<500万 → 单表+缓存 → ⭐ > > 高频单维度查询 → 简单哈希分片 → ⭐⭐ > > 多维查询,数据3~5年内可规划 → 基因法 → ⭐⭐⭐ > > 多维查询,难以事前规划 → 映射表 → ⭐⭐⭐ > > 频繁突发增长 → 虚拟槽分片 → ⭐⭐⭐⭐ > > 存储海量非结构化数据 → 分库+ES外挂 → ⭐⭐⭐⭐ > > | 原则 | 说明 | > | --- | --- | > | **充要性** | 没有明确业务诉求,不分片 | > | **前置性** | ID生成阶段考虑基因法,事后无法修改 | > | **一致性** | 相关表使用相同/兼容的分片键 | > | **均衡性** | 定期监控分布,变异系数< 0.2 | > | **可扩展** | 预留3年增长空间,分片数预留50% | > | **观测性** | 完善监控告警,及时发现热点 | > > **分库分表不是架构银弹。** 它解决的是单机数据库的物理极限问题,但带来了分布式系统的复杂性。 > > 最高境界是:**业务同学完全不感知分片的存在,框架层面透明地处理所有复杂性。** > > 关键是**诊断先行、层级递进、观测驱动**。用数据说话,而不是猜测;从简到复,不要过度设计;完善监控,及时调整。 --- **企业级项目导航**:⬅️ [[02-节目服务与支付服务分库分表设计思维全景|02-节目服务与支付服务分库分表设计思维全景]] | 03-从决策到落地:如何根据业务优雅设计库表 | ➡️ [[04-分布式分库分表:基因法完全解读|04-分布式分库分表:基因法完全解读]]