虚拟分片路由是什么
前提概要
🎯 基因法的初衷
在订单表分库分表场景中,采用了基因法来解决数据路由问题。
核心思路很简单:在生成订单编号时,将库表的路由信息(基因)嵌入其中。这样无论使用订单编号还是用户ID,都能准确定位到数据所在的分片。
⚠️ 基因法的致命缺陷
但是,这种方案有个无法回避的问题:扩容难题 💥
由于订单编号中已经硬编码了固定的库表基因(比如2库4表的分片信息),当业务增长需要扩容到4库8表时,原有订单编号中的路由信息就会失效。
这时候你会面临两难选择 😰:
- 要么对历史数据进行大规模迁移,重新生成订单号
- 要么维护多套复杂的路由规则
无论哪种方案,都会带来巨大的运维成本和业务风险。
🚀 虚拟分片路由:优雅的解决方案
为了彻底解决这个问题,我们引入了虚拟分片路由方案 ✨
核心思想是在物理分片和路由规则之间,增加一层虚拟分片映射表。将原本硬编码在订单号中的物理分片信息,转换为虚拟分片ID。
扩容时的优势:
- ✅ 只需调整虚拟分片到物理分片的映射关系
- ✅ 不需要改变订单号的生成逻辑
- ✅ 实现平滑扩容和业务无感知迁移
真正做到了一次设计,持续扩容 🎉
一、核心思想
1.1 什么是虚拟分片?
虚拟分片是在物理分片和数据之间增加的一层逻辑映射:
数据 → 虚拟分片ID → 路由表 → 物理库表
三层架构:
┌─────────────────────────────────────────────────────────┐
│ 第1层:数据基因 │
│ 订单号/用户ID → 基因位(3位)→ 物理分片索引(0-7) │
│ 例如:userId=13 → 基因101 → 物理分片5 │
└─────────────────┬───────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 第2层:虚拟分片映射 │
│ 8个物理分片 → 扩展到1024个虚拟分片 │
│ 每个物理分片对应128个虚拟分片 │
│ - 物理分片0 → 虚拟分片0-127 │
│ - 物理分片1 → 虚拟分片128-255 │
│ - ... │
│ - 物理分片7 → 虚拟分片896-1023 │
└─────────────────┬───────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 第3层:路由表映射 │
│ 虚拟分片ID → 实际物理库表(可动态调整) │
│ ┌──────────┬────────────┬────────────┐ │
│ │虚拟分片ID│ 物理库 │ 物理表 │ │
│ ├──────────┼────────────┼────────────┤ │
│ │ 0 │ damai_order_0 │ d_order_0 │ ← 初始配置 │
│ │ 1 │ damai_order_0 │ d_order_0 │ │
│ │ ... │ ... │ ... │ │
│ │ 127 │ damai_order_0 │ d_order_0 │ │
│ │ 128 │ damai_order_0 │ d_order_1 │ │
│ │ ... │ ... │ ... │ │
│ └──────────┴────────────┴────────────┘ │
│ │
│ 扩容时修改(在现有库中增加表): │
│ ┌──────────┬────────────┬────────────┐ │
│ │ 0 │ damai_order_0 │ d_order_4 │ ← 迁移后 │
│ │ 1 │ damai_order_0 │ d_order_0 │ │
│ │ ... │ ... │ ... │ │
│ └──────────┴────────────┴────────────┘ │
└──────────────────────────────────────────────────────────┘
1.2 核心优势
| 维度 | 传统方案(无虚拟分片) | 虚拟分片方案 |
|---|---|---|
| 扩容方式 | 新增库或表,整体迁移 | 在现有库中增加表,拆分迁移 |
| 扩容粒度 | 1/24(整个物理分片,约4.2%数据) | 24个物理分片→48个(每张表拆半,分批迁移) |
| 单次迁移量 | 整张表全部数据 | 每张表的后半部分(约2.1%总数据) |
| 灵活性 | 低(一次性迁移) | 高(可分24次迁移,每次1张表) |
| 涉及表类型 | 订单表、购票人订单表、订单记录表 | 同左(3类表共享路由表) |
| 风险等级 | 较高 | 较低(可分批执行) |
| 回滚能力 | 困难(需迁移回去) | 简单(修改路由表即可) |
| 代码改动 | 可能需要 | 不需要(只改路由表) |
二、设计原理
2.1 订单号生成(保持原有逻辑)
关键:订单号生成逻辑完全不变,只嵌入3位基因。
/**
* 生成订单编号(基因法)
*
* 嵌入信息:
* - 表基因(2位):2^2 = 4个表
* - 库基因(1位):2^1 = 2个库
* - 总计:3位基因
*
* @param userId 用户ID
* @param tableCount 分表数量(4)
* @param databaseCount 分库数量(2)
* @return 订单编号
*/
public synchronized long getOrderNumber(long userId, long tableCount, long databaseCount) {
//省略...
}
示例(2库×4表):
| 用户ID | 二进制 | 后3位(基因) | 订单号最后3位 | 物理分片 |
|---|---|---|---|---|
| 1 | 001 | 001 | 001 | db_0.table_1 |
| 2 | 010 | 010 | 010 | db_0.table_2 |
| 3 | 011 | 011 | 011 | db_0.table_3 |
| 4 | 100 | 100 | 100 | db_1.table_0 |
| 5 | 101 | 101 | 101 | db_1.table_1 |
2.2 虚拟分片计算(核心算法)
核心概念:虚拟分片是什么?
问题背景:
- 原始物理分片只有8个(2库×4表)
- 扩容时最小单位是1/8(12.5%),粒度太粗
- 需要更灵活的扩容粒度
虚拟分片解决方案:
- 将8个物理分片"细分"为1024个虚拟分片
- 每个物理分片对应128个虚拟分片(1024 ÷ 8 = 128)
- 扩容时可以按虚拟分片迁移,粒度变细
虚拟分片映射图:
物理分片 虚拟分片范围 说明
─────────────────────────────────────────────────
物理分片0 → 虚拟分片0-127 (128个虚拟分片)
物理分片1 → 虚拟分片128-255 (128个虚拟分片)
物理分片2 → 虚拟分片256-383 (128个虚拟分片) ← 示例在这里
物理分片3 → 虚拟分片384-511 (128个虚拟分片)
物理分片4 → 虚拟分片512-639 (128个虚拟分片)
物理分片5 → 虚拟分片640-767 (128个虚拟分片)
物理分片6 → 虚拟分片768-895 (128个虚拟分片)
物理分片7 → 虚拟分片896-1023 (128个虚拟分片)
计算逻辑(两步定位):
第1步:根据基因位确定 "大区域"(物理分片索引)
→ 确定数据在8个物理分片中的哪一个
第2步:根据模运算确定"小区域"(虚拟分片内偏移)
→ 确定数据在128个虚拟分片中的哪一个
最终:虚拟分片ID = 物理分片索引 × 128 + 虚拟偏移
形象比喻:
就像找一本书:
1. 先找书柜(物理分片索引:0-7,共8个书柜)
2. 再找书架(虚拟偏移:0-127,每个书柜有128个书架)
3. 最终位置 = 书柜号 × 128 + 书架号
示例:
- 书柜2(物理分片2),书架82(虚拟偏移82)
- 最终位置 = 2 × 128 + 82 = 338号书架
虚拟分片的核心逻辑都放到了calculateLogicalShardId方法中,后续在讲解代码流程时,会详细的讲解此方法的执行过程
public int calculateLogicalShardId(Long shardingKey)
三、虚拟分片路由表的设计
3.1 虚拟分片路由表结构
CREATE TABLE `d_sharding_route_mapping` (
-- 省略其他字段
`logical_shard_id` int NOT NULL COMMENT '逻辑分片ID(0-1023)',
-- 省略其他字段
) ENGINE=InnoDB COMMENT='虚拟分片路由映射表';
3.2 表字段 logical_shard_id 详细解释
📚 3.2.1 什么是 logical_shard_id?
logical_shard_id = 虚拟分片ID = 逻辑分片ID
- 范围:0 - 1023(总共1024个虚拟分片)
- 作用:在物理分片(8个)和分片键之间建立一个中间映射层
🎯 3.2.2 为什么需要虚拟分片?
对比两种方案:
| 方案 | 映射方式 | 扩容方式 | 灵活性 |
|---|---|---|---|
| 直接映射 | 分片键 → 物理表 | 必须迁移整张表 | ❌ 低 |
| 虚拟分片 | 分片键 → 虚拟分片ID → 物理表 | 只迁移部分虚拟分片 | ✅ 高 |
3.2.3 示例对比:
❌ 没有虚拟分片:
orderNumber → hash计算 → damai_order_0.d_order_2
扩容时:d_order_2 的所有数据都要迁移
✅ 有虚拟分片:
orderNumber → 计算 → 虚拟分片338 → 查路由表 → damai_order_0.d_order_2
扩容时:只迁移虚拟分片320-383(一半数据)到新表
3.2.4 📊 虚拟分片的分配规则
初始状态(2库×4表=8个物理分片):
- 1024个虚拟分片 ÷ 8个物理分片 = 每个物理分片128个虚拟分片
| 物理分片 | 物理库表 | 虚拟分片ID范围 | 虚拟分片数量 |
|---|---|---|---|
| 物理分片0 | db_0.table_0 | 0-127 | 128个 |
| 物理分片1 | db_0.table_1 | 128-255 | 128个 |
| 物理分片2 | db_0.table_2 | 256-383 | 128个 |
| 物理分片3 | db_0.table_3 | 384-511 | 128个 |
| 物理分片4 | db_1.table_0 | 512-639 | 128个 |
| 物理分片5 | db_1.table_1 | 640-767 | 128个 |
| 物理分片6 | db_1.table_2 | 768-895 | 128个 |
| 物理分片7 | db_1.table_3 | 896-1023 | 128个 |
3.3 🔧 SQL 生成逻辑详解
以第一个物理分片为例(生成 0-127):
SELECT 0 + a + b*10 + c*100 AS n
FROM
(SELECT 0 AS a UNION SELECT 1 ... UNION SELECT 9) t1, -- 个位
(SELECT 0 AS b UNION SELECT 1 ... UNION SELECT 9) t2, -- 十位
(SELECT 0 AS c UNION SELECT 1) t3 -- 百位
WHERE n >= 0 AND n <= 127;
原理:笛卡尔积 + 数学组合
- 生成个位、十位、百位的所有组合
- t1(个位):0, 1, 2, 3, 4, 5, 6, 7, 8, 9
- t2(十位):0, 1, 2, 3, 4, 5, 6, 7, 8, 9
- t3(百位):0, 1
- 笛卡尔积(10 × 10 × 2 = 200 种组合)
| 百位 | 十位 | 个位 | 计算公式 | 结果 | 是否保留 |
|---|---|---|---|---|---|
| 0 | 0 | 0 | 0+0+0 | 0 | ✅ |
| 0 | 0 | 1 | 0+0+1 | 1 | ✅ |
| 0 | 1 | 0 | 0+10+0 | 10 | ✅ |
| ... | ... | ... | ... | ... | ... |
| 1 | 2 | 7 | 100+20+7 | 127 | ✅ |
| 1 | 2 | 8 | 100+20+8 | 128 | ❌ 超出范围 |
| 1 | 2 | 9 | 100+20+9 | 129 | ❌ 超出范围 |
- WHERE 过滤:
WHERE n >= 0 AND n <= 127保留128条记录
3.3.1 ❓ 为什么看起来"有大有小,不是递增的"?
原因:笛卡尔积的执行顺序是不确定的!
数据库在执行笛卡尔积时,可能按以下任意顺序生成:
- 方式1:先固定c、b,遍历a → 0, 1, 2, ..., 9, 10, 11, ...
- 方式2:先固定c、a,遍历b → 0, 10, 20, ..., 1, 11, 21, ...
- 方式3:完全随机顺序 → 5, 23, 1, 67, 34, 12, ...
但是!最终结果是确定的:
- ✅ 一定包含0-127的所有数字(一个不多,一个不少)
- ✅ 只是插入顺序可能不同
- ✅ SELECT 时可以用
ORDER BY logical_shard_id排序查看
验证SQL:
-- 排序查看,结果一定是连续的 0, 1, 2, ..., 127
SELECT * FROM d_sharding_route_mapping
WHERE physical_database_suffix = '0'
AND physical_table_suffix = 0
ORDER BY logical_shard_id;
3.4 🎨 虚拟分片路由表的形象比喻
比喻1:停车场的车位编号
- 停车场有1024个车位,分为8个停车区(A-H区)
- 车位编号(logical_shard_id):
- A区:0-127号车位
- B区:128-255号车位
- ...
- H区:896-1023号车位
- 扩容时:A区拆分为 A1区(0-63号)+ A2区(64-127号)
- 车位号码不变,只是归属区域变了!
比喻2:快递分拣中心
- 1024个包裹格子(logical_shard_id: 0-1023)
- 8个配送站(物理分片)
- 初始分配:配送站1负责格子0-127,配送站2负责128-255,...
- 扩容时:配送站1拆分为站1A(格子0-63)+ 站1B(格子64-127)
- 只需更新"格子归属表",包裹计算逻辑不变!
3.5 📝 完整示例
假设: orderNumber = 1234567890
步骤1:计算虚拟分片ID
calculateLogicalShardId(1234567890)
→ 提取基因位(后3位)→ physicalShardIndex = 2
→ 模128 → virtualOffset = 82
→ logicalShardId = 2 × 128 + 82 = 338
步骤2:查询路由表
SELECT * FROM d_sharding_route_mapping WHERE logical_shard_id = 338;
结果:physical_database_suffix='0', physical_table_suffix=2
步骤3:拼接物理表名
database = 'damai_order_0'
table = 'd_order_2'
最终路由到:damai_order_0.d_order_2
3.6 🚀 扩容时的妙用
初始状态:
物理分片2(damai_order_0.d_order_2)→ 虚拟分片256-383(128个)
扩容后:
虚拟分片256-319(64个)→ 保留在 d_order_2
虚拟分片320-383(64个)→ 迁移到 d_order_6(新表)
只需修改路由表:
UPDATE d_sharding_route_mapping
SET physical_table_suffix = 6
WHERE logical_shard_id >= 320 AND logical_shard_id <= 383;
优势:
- ✅ 不需要修改代码
- ✅ 不需要重启服务
- ✅ 秒级切换
- ✅ 可以回滚