跳到主要内容

虚拟分片路由是什么

前提概要

🎯 基因法的初衷

在订单表分库分表场景中,采用了基因法来解决数据路由问题。

核心思路很简单:在生成订单编号时,将库表的路由信息(基因)嵌入其中。这样无论使用订单编号还是用户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位物理分片
1001001001db_0.table_1
2010010010db_0.table_2
3011011011db_0.table_3
4100100100db_1.table_0
5101101101db_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范围虚拟分片数量
物理分片0db_0.table_00-127128个
物理分片1db_0.table_1128-255128个
物理分片2db_0.table_2256-383128个
物理分片3db_0.table_3384-511128个
物理分片4db_1.table_0512-639128个
物理分片5db_1.table_1640-767128个
物理分片6db_1.table_2768-895128个
物理分片7db_1.table_3896-1023128个

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;

原理:笛卡尔积 + 数学组合

  1. 生成个位、十位、百位的所有组合
    • 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
  2. 笛卡尔积(10 × 10 × 2 = 200 种组合)
百位十位个位计算公式结果是否保留
0000+0+00
0010+0+11
0100+10+010
..................
127100+20+7127
128100+20+8128❌ 超出范围
129100+20+9129❌ 超出范围
  1. 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;

优势:

  • ✅ 不需要修改代码
  • ✅ 不需要重启服务
  • ✅ 秒级切换
  • ✅ 可以回滚