在分布式订单系统中,当订单量达到亿级规模 ,原有的40位订单号面临容量瓶颈。本文分享一种将订单ID从40位升级到64位的方案,核心思路是将路由信息编码到订单号中,实现零查库的快速路由。
方案涵盖:位段分配设计、编解码实现、降级容错机制、SDK封装等完整内容,适用于需要进行分库分表、跨地域部署的分布式系统。
💡 本文基于真实生产环境设计经验,业务场景已做抽象处理,读者可根据实际需求调整方案。
术语与概念定义
本文涉及以下关键术语,为便于理解,在此统一说明:
核心业务术语
| 术语 | 全称 | 定义 | 示例 |
|---|---|---|---|
| RegionCode | Regional Code | 用户数据所属的国家与地区,使用区域码+品牌格式 | DC-D(Brand-A 地域D用户) DC-F(Brand-C 地域F用户) |
| RequestFrom | - | 业务渠道标识,标识请求来自哪个渠道 | BRAND_A、BRAND_A_BIZ、BRAND_B、BRAND_C、BRAND_D |
| UID | User ID | 用户唯一标识 | - |
RegionCode 详细说明:
- 定义:标识用户数据所属区域的业务字符串标识符
- 作用:用于分布式系统中的数据路由和定位
- 传递:通过请求上下文传递(如 HTTP Header)
- 编码范围 :
- 起始值:513(0x201)
- 最大值:4095(0xFFF,12位编码上限)
- 可用编码数:3583 种组合
路由上下文参数 :RequestFrom + UID + RegionCode
- 这三个参数组合用于请求路由和数据定位
- 通过 HTTP Header 传递:
X-Request-From、X-Region-Code、X-User-Id(示例 Header 名称)
系统与服务
| 术语 | 全称 | 定义 | 作用 |
|---|---|---|---|
| RouteService | Route Control Service | 路由控制服务 | 提供 RegionCode 编码/解码服务: - 编码:RegionCode → 12位整数编码值 - 解码:12位编码值 → RegionCode + RequestFrom |
| OIS | Order Index Service | 订单索引服务 | 提供订单索引和查询能力: - 索引同步:整合多业务单元订单数据 - 查询服务:提供多维度订单查询接口 |
| ConfigCenter | - | 配置中心框架 | - 定义业务系统运行时参数配置 - 动态实时推送配置变更到 SDK 客户端 |
| DataSync | Data Synchronization Service | 数据复制中心 | 用于数据双向或多向复制的数据中间件 |
技术术语
| 术语 | 定义 | 说明 |
|---|---|---|
| 号段模式 | 分布式ID生成方式 | 服务端预先分配一段ID范围(如1000-2000),客户端在本地递增分配,号段用完再向服务端申请下一段 |
| 数据库自增 | 传统ID生成方式 | 每次生成ID都需要访问数据库(通过AUTO_INCREMENT或插入种子表) |
| SEQ | Sequence,序列号 | 订单号的核心部分,保证唯一性(占用40位) |
| UNKNOWN编码 | - | RegionCode 无法编码时的默认值:0b001000000000(512),低于有效编码起始值(513) |
号段模式 vs 数据库自增对比:
| 对比项 | 数据库自增 | 号段模式 |
|---|---|---|
| 访问频率 | 每生成1个ID访问1次数据库 | 每1000个ID访问1次服务端 |
| 性能 | 1000-5000 ID/秒 | 百万级 ID/秒 |
| ID浪费 | 无 | 客户端重启时剩余ID浪费 |
| 实现复杂度 | 简单 | 中等(需要客户端+服务端配合) |
号段模式原理:
markdown
1. 客户端向服务端请求号段:fetchIdPool()
2. 服务端从数据库分配号段:[1000, 2000)
3. 客户端本地递增:1000, 1001, 1002...1999
4. 号段用完后,再次请求下一段:[2000, 3000)
扩展阅读:号段模式的并发安全与性能优化(双 Buffer 预取、动态步长调整等)可参考分布式 ID 生成相关技术文档。
第一章:背景与问题
1.1 业务背景
业务挑战:
在分布式订单系统中,为了支持海量数据和多地域部署,系统需要采用分库分表 + 单元化架构:
- 分库分表:订单数据按业务维度(如业务渠道:BRAND_A、BRAND_B、BRAND_C)分片到不同的数据库
- 单元化部署:不同业务单元部署在不同地域(如 Region-A、Region-B、Region-C),实现故障隔离和就近访问
- 路由要求:每个订单请求需要快速路由到正确的数据库分片
技术问题:
在这种架构下,订单号的设计成为关键问题。
如果订单号不包含业务信息(如业务渠道、地域),在实际场景中会遇到路由困难:
1.2 核心问题
问题场景:
在分布式系统中,以下场景的请求往往仅包含订单ID,缺少路由所需的上下文信息:
| 场景 | 请求内容 | 缺失信息 |
|---|---|---|
| 跨系统查询 | 订单ID:123456789 | 无法确定品牌(BRAND_A/BRAND_B) |
| 外部链接访问 | URL: /order/123456789 | 无法确定地域(Region-A/Region-B/Region-C) |
| 批处理任务 | 异步任务处理订单 123456789 | 缺少业务渠道与地域信息 |
路由困境:
1. 业务渠道(BRAND_A/BRAND_B/BRAND_C)
2. 地域(Region-A/Region-B/Region-C)"] Missing --> Question["❓ 不知道应该查询哪个地域的哪个分片?"] Question -.-> RegionA["Region-A(DC-A)"] Question -.-> RegionB["Region-B(DC-B)"] Question -.-> RegionC["Region-C(DC-C)"] subgraph RegionA_DB["Region-A 数据中心"] RegionA_B1["BRAND_A
分片 0-3"] RegionA_B2["BRAND_B
分片 4-7"] end subgraph RegionB_DB["Region-B 数据中心"] RegionB_B1["BRAND_A
分片 0-3"] RegionB_B2["BRAND_B
分片 4-7"] end subgraph RegionC_DB["Region-C 数据中心"] RegionC_B3["BRAND_C
分片 1000"] end RegionA --> RegionA_DB RegionB --> RegionB_DB RegionC --> RegionC_DB style OrderId fill:#fff3cd style Missing fill:#f8d7da style Question fill:#f8d7da
具体问题:
-
业务渠道不明确
- 订单号 123456789 属于 BRAND_A(分片 0-3)、BRAND_B(分片 4-7)还是 BRAND_C(分片 1000)?
-
地域不明确
- 即使知道是 BRAND_A,也不知道应该查 Region-A 的 BRAND_A 分片还是 Region-B 的 BRAND_A 分片
1.3 技术方案选型
面对上述路由问题,系统对比了两种主要方案:
方案1:集中式索引表
实现方式:
- 建立全量索引表,存储所有订单的路由信息(订单ID → 业务渠道 + 地域)
- 索引表部署在所有地域,通过跨地域同步保持一致
- 每个请求查询索引表获取路由信息
优点:
- ✅ 订单ID格式不变,前端页面无需调整
缺点:
- ❌ 性能问题:所有请求都需要查询索引表
- ❌ 可用性问题:依赖跨地域同步,新增订单需要写入所有地域的索引表
方案2:ID内嵌编码(本文方案)
实现方式:
- 在订单ID中编码业务渠道和地域信息
- 仅为存量订单(升位前)建立索引表,部署在各地域,无需同步
- 新订单:直接从订单ID解码路由信息(纯内存计算)
- 存量订单:查询本地索引表(固定数据,不再增长)
优点:
- ✅ 性能好:新订单零查库,纯位运算解码(<1ms)
- ✅ 可用性好:存量索引表固定,无需跨地域同步
缺点:
- ⚠️ 订单ID长度增加(40位 → 64位),部分页面样式需要调整
1.4 设计思路
基于方案2(ID内嵌编码),核心设计思路如下:
1.4.1 订单号现状与升位空间
订单号现状:
- 当前最大值:百亿级(11位十进制,约35位二进制)
- 年增长:数十亿级/年
- 存储类型 :
- 后端:Java Long(64位有符号整数)
- 前端:JavaScript Number(浮点类型,正整数安全范围53位)
升位空间分析:
arduino
当前订单号:~35 bit(约数百亿)
前端限制: ≤53 bit(JavaScript Number安全整数范围)
后端存储: 64 bit(Java Long)
可用编码空间:53 - 35 = 18 bit
64位订单号位段分配:
scss
位分布(从低位到高位):
+----------------+-----+--------------------+--------+-----------+---------+------+
| 订单SEQ | 业务| 品牌+RegionCode | 扩展 | SEQ扩展 | 版本位 | 符号 |
| (40 bits) | 位 | (12 bits) | (2) | (6) | (2) | (1) |
+----------------+-----+--------------------+--------+-----------+---------+------+
| Bits: 39-0 | 40 | Bits: 52-41 | 54-53 | 60-55 | 62-61 | 63 |
+----------------+-----+--------------------+--------+-----------+---------+------+
✅ 使用中 ✅ ✅ 使用中 ⏸️暂不启用 ⏸️暂不启用 ⏸️暂不启用 🔒固定
位段详细说明:
| 编号 | 位段 | 位数 | 当前状态 | 说明 | 扩展后容量 |
|---|---|---|---|---|---|
| (1) | 订单SEQ | 40 bits (0-39) | ✅ 使用中 | 订单序列号,保证唯一性 | 1.099万亿 |
| (2) | 业务位 | 1 bit (40) | ✅ 使用中 | 业务类型标识,用于DataSync 同步 | 2种类型 |
| (3) | 品牌+RegionCode编码 | 12 bits (41-52) | ✅ 使用中 | 业务渠道+地域编码 不能全0 (区分新旧订单号) 编码范围:513~4095 | 理论4095组合 可用3583组合 (513~4095) |
| (4) | 品牌+RegionCode扩展 | 2 bits (53-54) | ⏸️ 暂不启用 | 扩充品牌+RegionCode组合 | +4096组合 →共16383组合 |
| (5) | SEQ扩展位 | 6 bits (55-60) | ⏸️ 暂不启用 | 扩充SEQ(高位溢出) | +64倍 →70万亿 |
| (6) | 版本位 | 2 bits (61-62) | ⏸️ 暂不启用 | 订单格式版本,当前相当于00 | 4个版本 |
| 符号位 | 符号位 | 1 bit (63) | 🔒 固定为0 | Java Long符号位 不可用于扩展 | - |
空间统计:
- 总位数:64 bits(40 + 1 + 12 + 2 + 6 + 2 + 1)
- 当前使用:53 bits(位段 1 + 2 + 3)
- 预留扩展:10 bits(位段 4 + 5 + 6,bits 53-62)
- 符号位:1 bit(固定为0,不可用)
- 订单号长度:固定 16 位十进制数字
关键设计约束:
-
定长16位十进制数字:
- 为保证订单号固定16位,bits 50-52 至少有一个为1
- RegionCode 编码从513(0x201 = 0b001000000001)开始,天然满足此约束
- 可用编码范围:513~4095,共 3583种组合
-
新旧订单号区分:
- 新订单号:品牌+RegionCode编码 ≠ 0(编码值 ≥ 513)
- 旧订单号:品牌+RegionCode编码 = 0(即bits 41-52 全为0)
- UNKNOWN编码:512(0x200),低于有效编码起始值
-
编码示例(12位二进制):
scss001000000001 (513):起始编码值 001000000010 (514):第二个编码值 ... 01XXXXXXXXXX (10XX):示例编码值 ... 111111111111 (4095):最大编码值 -
SEQ获取限制:
- 为保证正确性,不支持用户自己传入SEQ
- 必须从订单号池获取
1.4.2 降级与扩展设计
降级策略:
为保证系统可用性,设计了多层降级机制:
-
编码降级:
- 兜底:编码失败时,由 RouteService完成兜底编码
- 开关降级:可降级为只有SEQ模式(品牌+RegionCode编码全为0)
- SDK提供开关控制
-
解码降级:
markdown解码流程(带降级): 1. 判断订单号类型 - 品牌+RegionCode编码 = 0 → 旧订单号 - 品牌+RegionCode编码 ≠ 0 → 新订单号 2. 新订单号解码 - 从 RouteService获取品牌+RegionCode → 成功 - RouteService 失败 → 返回null(由 RouteService路由决定) 3. 旧订单号查询 - 查存量订单索引服务 → 成功 - 索引服务失败/不存在 → 返回null - 索引服务可降级/关闭
扩展点设计:
-
订单池可配置:
- 兼容业务自有的缓存订单池
- 支持不同品牌从不同订单号池获取SEQ
- 降低SEQ增长速度
-
路由编码表动态扩展:
- 编码表可扩充、动态配置和下发
- 考虑品牌不存在时的处理方式
- 支持 RegionCode兜底
- 支持内嵌到客户端
-
多位段组合扩展(未来):
- 位段(4)+位段(5):共8 bits可进一步扩展编码表和SEQ
- 位段(4)启用:品牌+RegionCode组合 → 16383种
- 位段(5)启用:SEQ扩展 → 70万亿
1.4.3 编解码流程
编码流程(生成订单号):
markdown
1. 获取SEQ(40位)
├─ 从配置的订单号池获取
├─ 不支持用户自己传入
└─ 不同品牌可从不同池获取
2. 获取品牌+RegionCode信息
├─ 从请求参数或上下文获取
└─ 调用 RouteService获取编码值(12位)
3. 设置业务位(1位)
└─ 用于DataSync 同步标识
4. 位拼接(从低到高)
├─ bits 0-39: SEQ (40位)
├─ bit 40: 业务位 (1位)
├─ bits 41-52: 品牌+RegionCode编码 (12位)
├─ bits 53-62: 扩展位暂不设置(保持为0)
└─ bit 63: 符号位(恒为0,保证正数)
5. 编码失败兜底
├─ 路由编码失败 → RouteService 完成兜底编码
└─ 开关降级 → 返回只有SEQ的订单号(品牌+RegionCode=0)
6. 返回64位订单号(固定16位十进制)
解码流程(查询品牌+RegionCode):
markdown
1. 提取品牌+RegionCode编码(bits 41-52)
2. 判断订单号类型
├─ 编码 = 0 → 旧订单号(goto 步骤4)
└─ 编码 ≠ 0 → 新订单号(goto 步骤3)
3. 新订单号解码
├─ 调用RouteService 解码服务
├─ RouteService 返回品牌+RegionCode → 返回结果
├─ RouteService 解码失败 → 返回null
└─ 考虑位段(4)启用后的扩展编码支持
4. 旧订单号查询
├─ 调用存量订单索引服务
├─ 索引服务返回品牌+RegionCode → 返回结果
├─ 索引服务失败/不存在 → 返回null
└─ 索引服务可降级/关闭
5. 监控记录
├─ 记录订单号格式(新/老)
├─ 记录解码失败情况
└─ APM 监控埋点
基本原则:
- ✅ 尽量返回品牌+RegionCode
- ✅ 失败返回null(不污染数据库RegionCode 字段,路由由 RouteService决定)
- ✅ 版本位不是0也返回null(预留未来版本)
1.4.4 过渡期兼容性设计
存量订单索引服务(过渡阶段):
-
实时同步构建:
- 通过订单索引(OIS)实时同步所有订单
- 可能存在延迟,需要容错处理
-
未接入订单兼容:
- 尚未接入创单的订单号 → 返回null
- OIS 不存在的订单号 → 返回 null
- 路由由 RouteService兜底决定
-
服务降级:
- 索引服务可降级
- 索引服务可关闭
- 降级后返回null,不影响主流程
-
退出机制:
- 全部订单生成接入完成
- 关闭实时 OIS 同步到存量订单索引功能
- 索引表固定,不再增长
接入阶段并行策略:
| 阶段 | 订单号生成接入 | 品牌+RegionCode查询接入 | 说明 |
|---|---|---|---|
| 阶段1 | ✅ 优先接入 | ⏸️ 暂不接入 | 先生成编码订单号 |
| 阶段2 | ✅ 持续完善 | ✅ 开始接入 | 两个接口并行推进 |
| 阶段3 | ✅ 全部完成 | ✅ 全部完成 | 关闭存量索引同步 |
监控体系:
-
SDK接入监控:
- 两个接口的接入情况
- 查询接口使用情况
- 未接入的订单号识别
-
RouteService 监控:
- 路由编码使用情况
- RouteService 遇到未知编码的情况
- 编码表覆盖率
-
降级监控:
- 编码降级触发次数
- 解码失败率(区分新/老格式)
- 存量索引服务调用情况
第二章:SDK设计与实现
2.1 SDK整体架构
设计目标:
- 订单号编码:将 RegionCode(Regional Code)编码到订单号高位
- 订单号解码:从订单号解码出 RegionCode 和 RequestFrom
- 存量兼容:存量订单(未编码)通过索引服务查询
- 降级容错:编码/解码失败时的降级策略
架构分层:
OrderIdEncoder"] Decoder["OrderIdDecoder"] end subgraph EncoderSide["Generator侧(编码流程)"] RouteServiceEnc["RouteServiceEncoder
(RegionCode 编码)"] Adapter["IdGeneratorAdapter
(生成原始ID)"] CoreGen["CoreGenerator
(位拼接)"] end subgraph DecoderSide["Decoder侧(解码流程)"] CoreDec["CoreDecoder
(提取编码值)"] RouteServiceDec["RouteServiceDecoder
(RouteService 解码)"] OISDec["OISDecoder
(降级查询)"] end Generator --> RouteServiceEnc RouteServiceEnc --> Adapter Adapter --> CoreGen Decoder --> CoreDec CoreDec --> RouteServiceDec RouteServiceDec --> OISDec
服务依赖关系图:
PaymentService
RefundService"] end subgraph SDK["订单号升位SDK"] direction TB SDKAPI["API层
OrderIdGenerator
OrderIdDecoder"] SDKCore["核心层
RouteServiceEncoder / CoreGenerator
RouteServiceDecoder / OISDecoder"] SDKAPI --> SDKCore end subgraph Services["外部依赖服务"] direction TB RouteServiceNode["RouteService 服务
(RegionCode编码/解码)"] OIS["OIS 索引服务
(存量订单查询)"] ConfigCenter["ConfigCenter
(配置管理)"] end subgraph Storage["数据存储"] direction TB SeedDB["订单号种子表"] OrderDB["订单数据库"] end BizApp -->|"生成/解码订单号"| SDKAPI SDKCore -->|"编码/解码RegionCode"| RouteServiceNode SDKCore -->|"查询存量订单"| OIS SDKCore -->|"读取配置"| ConfigCenter SDKCore -->|"生成自增ID"| SeedDB BizApp -->|"读写订单数据"| OrderDB style App fill:#e3f2fd style SDK fill:#fff3e0 style Services fill:#f3e5f5 style Storage fill:#e8f5e9
依赖说明:
| 服务/组件 | 依赖方向 | 作用 | 可用性要求 |
|---|---|---|---|
| RouteService 服务 | SDK → RouteService | RegionCode编码/解码 | 高可用(编码失败返回UNKNOWN) |
| OIS 索引服务 | SDK → OIS | 存量订单查询 | 可降级(查询失败返回 null) |
| ConfigCenter | SDK → ConfigCenter | 开关/灰度配置 | 高可用(使用本地缓存) |
| 订单号种子表 | SDK → DB | 生成自增ID | 高可用(创单依赖) |
| 订单索引表 | OIS → DB | 存量数据存储 | 可降级(过渡期使用) |
| RouteService 编码映射表 | RouteService → DB | 编码规则存储 | 高可用(RouteService 依赖) |
服务交互时序:
编码时序(生成订单号):
解码时序(查询 RegionCode):
2.2 编码流程实现
2.2.1 编码流程概览
- 输出:12位编码值
- 失败兜底:返回 UNKNOWN=0b001000000000"] Step1Detail --> Step2["步骤2: IdGeneratorAdapter"] Step2 --> Step2Detail["- 调用用户提供的RawIdGenerator
- 生成40位原始订单号
- 封装:(原始ID, 路由编码值)"] Step2Detail --> Step3["步骤3: CoreGenerator"] Step3 --> Step3Detail["- 位拼接:
bits 0-39: 原始ID (40位)
bit 40: 业务位 (1位)
bits 41-52: 路由编码 (12位)
bits 53-62: 扩展位 (10位,暂为0)
bit 63: 符号位 (恒为0)
- 返回64位订单号"] Step3Detail --> End["输出:编码后的64位订单号Id'"] style Step1 fill:#e1f5ff style Step2 fill:#e1f5ff style Step3 fill:#e1f5ff
2.2.2 核心代码实现
RouteServiceEncoder实现:
java
public class RouteServiceEncoder implements Generator<String, Long> {
private static final long UNKNOWN_CODE = 0b001000000000L; // 512
@Override
public Long generate(String regionCode, String uid) {
// 1. RegionCode 为空时,根据UID获取 RegionCode
if (StringUtils.isEmpty(regionCode) && StringUtils.isNotEmpty(uid)) {
regionCode = routeService.lookupByUser(uid);
}
// 2. 调用路由编码服务
if (StringUtils.isEmpty(regionCode)) {
return UNKNOWN_CODE; // RegionCode 仍为空,返回UNKNOWN
}
Long code = routeService.encode(regionCode);
// 3. RouteService 未编码的,返回UNKNOWN
return code != null ? code : UNKNOWN_CODE;
}
}
ID生成适配器实现:
java
public class IdGeneratorAdapter {
private RawIdGenerator<Long> rawIdGenerator;
private RouteServiceEncoder routeEncoder;
public EncodingContext generate(String regionCode, String uid) throws Exception {
// 1. 生成原始订单号
Long rawId = rawIdGenerator.generate();
// 2. 获取路由编码
Long routeCode = routeEncoder.generate(regionCode, uid);
// 3. 封装返回
return new EncodingContext(rawId, routeCode);
}
}
class EncodingContext {
private long rawId; // 40位原始ID
private long routeCode; // 12位路由编码
private long userDefined; // 用户自定义位(可选)
}
CoreGenerator实现:
java
public class CoreGenerator {
// 位段定义
private static final int SEQ_BITS = 40;
private static final int BUSINESS_BITS = 1;
private static final int REGION_CODE_BITS = 12;
public long encode(EncodingContext context) {
long rawId = context.getRawId();
long routeCode = context.getRouteCode();
long userDefined = context.getUserDefined();
// 位拼接
long orderId = rawId // bits 0-39
| (userDefined << 40) // bit 40 (业务位)
| (routeCode << 41); // bits 41-52
// bits 53-62: 扩展位,暂为0
// bit 63: 符号位,恒为0(Java Long默认)
return orderId;
}
}
2.2.3 开关与灰度控制
双层开关设计:
java
public class FeatureSwitchControl {
// 全局开关(优先级最高)
private boolean globalEnabled;
// 应用粒度开关
private Map<String, Boolean> appSwitchMap;
// 灰度百分比
private int rolloutPercentage;
public boolean isEnabled(String appId, String key) {
// 1. 全局开关关闭,直接返回false
if (!globalEnabled) {
return false;
}
// 2. 检查应用粒度开关
Boolean appEnabled = appSwitchMap.get(appId);
if (appEnabled == null || !appEnabled) {
return false;
}
// 3. 灰度流量控制
return trafficControl.shouldPass(key, rolloutPercentage);
}
}
灰度策略:
java
public class TrafficRollout {
public boolean shouldPass(String key, int percentage) {
if (percentage <= 0) {
return false;
}
if (percentage >= 100) {
return true;
}
// 基于key哈希的稳定灰度
// 使用 & 0x7FFFFFFF 确保非负(避免 Integer.MIN_VALUE 的边界问题)
int hash = key.hashCode() & 0x7FFFFFFF;
return (hash % 100) < percentage;
}
}
2.2.4 UNKNOWN编码值的处理逻辑
UNKNOWN编码值定义:
java
private static final long UNKNOWN_CODE = 0b001000000000L; // 512(十进制)
为什么是512?
- 有效编码范围:513 ~ 4095(共3583个可用编码)
- UNKNOWN值 :512,低于有效编码起始值
- 设计意图:作为哨兵值(Sentinel Value),标识"编码失败但订单有效"的特殊状态
UNKNOWN编码的产生场景:
UNKNOWN编码的解码与路由:
当SDK对携带UNKNOWN编码(512)的订单号进行解码时,会触发特殊的路由逻辑:
(513-4095)"] Check -->|是| RouteServiceFail["RouteService 解码返回
Status=FAIL"] RouteServiceFail --> Fallback["触发3层兜底路由"] Fallback --> Layer1["第1层:SDK decode
返回 requestFrom=null"] Layer1 --> Layer2["第2层:从 TraceContext 读取
requestFrom/regionCode"] Layer2 --> Layer3["第3层:RegionCode 推断 + DB查询
查询 order_index 表"] Layer3 --> Success["获取完整路由信息"] NormalDecode --> NormalReturn["返回 RegionCode + RequestFrom"] style Check fill:#e1f5ff style RouteServiceFail fill:#fff3cd style Fallback fill:#ffe5e5 style Success fill:#d4edda style NormalReturn fill:#d4edda
核心代码逻辑:
java
public class RouteServiceDecoder implements Decoder {
@Override
public DecodingResult decode(long orderId, long routeCode) {
// 1. 调用RouteService 解码服务
RegionDecodedResult result = routeService.decode(routeCode);
// 2. 对于UNKNOWN编码(512),RouteService 返回FAIL状态
if (result.getStatus() == RegionDecodedStatus.FAIL) {
value.setRawOrderId(orderId); // 设置回原始orderId
return null; // requestFrom = null,触发业务层兜底
}
// 3. 正常解码,返回 RegionCode 和 RequestFrom
value.setRegionCode(result.getRegionCode());
value.setRequestFrom(result.getRequestFrom());
return value;
}
}
业务层3层兜底机制:
java
// 第1层:SDK解码
DecodingResult result = orderIdDecoder.decode(orderId);
String requestFrom = result != null ? result.getRequestFrom() : null;
// 第2层:从TraceContext读取
if (StringUtils.isEmpty(requestFrom)) {
requestFrom = TraceContext.getRequestFrom();
regionCode = TraceContext.getRegionCode();
}
// 第3层:RegionCode 推断 + DB查询
if (StringUtils.isEmpty(requestFrom)) {
// 调用RegionCode 推断服务
regionCode = regionInferenceService.infer(uid);
// 查询order_index表获取历史订单信息
OrderIndexEntity entity = orderIndexMapper.selectByOrderId(orderId);
if (entity != null) {
requestFrom = entity.getRequestFrom();
regionCode = entity.getRegionCode();
}
}
UNKNOWN编码的设计价值:
| 方面 | 说明 |
|---|---|
| 系统容错性 | RouteService 服务异常时,订单仍可正常创建(不阻塞业务) |
| 兜底路由 | 通过TraceContext、RegionCode 推断、DB查询实现多层兜底 |
| 可识别性 | 512低于有效范围,RouteService 解码返回FAIL,便于监控告警 |
| 历史兼容 | 与编码值=0(旧订单号)区分开,避免混淆 |
监控指标:
- UNKNOWN编码产生率:监控路由编码失败比例
- 兜底路由成功率:监控3层兜底机制的有效性
- 512编码订单数量:定期统计并分析根因
2.3 解码流程实现
2.3.1 解码流程概览
- 判断:编码值=0 → 旧订单号
编码值≠0 → 新订单号"] CoreDecDetail --> Judge{"编码值 ≠ 0?"} Judge -->|Yes 新订单号| RouteServiceDecoder["RouteServiceDecoder
(RouteService 解码)"] Judge -->|No 旧订单号| OISDecoder["OISDecoder
(查询索引表)"] RouteServiceDecoder --> RouteServiceSuccess{"SUCCESS?"} OISDecoder --> OISSuccess{"SUCCESS?"} RouteServiceSuccess -->|Yes| Return["返回:RegionCode, RequestFrom, 原始ID"] OISSuccess -->|Yes| Return RouteServiceSuccess -->|No| Fail["返回null"] OISSuccess -->|No| Fail style Step1 fill:#e1f5ff style RouteServiceDecoder fill:#e1f5ff style OISDecoder fill:#e1f5ff style Return fill:#d4edda style Fail fill:#f8d7da
2.3.2 核心代码实现
CoreDecoder实现:
java
public class CoreDecoder {
public DecodingContext decode(long orderId) {
// 1. 提取各位段
long rawId = orderId & 0xFFFFFFFFFFL; // bits 0-39
long userDefined = (orderId >> 40) & 0x1L; // bit 40
long routeCode = (orderId >> 41) & 0xFFFL; // bits 41-52
long reserved = (orderId >> 53) & 0x3FFL; // bits 53-62 (扩展位)
// 2. 封装返回
DecodingContext context = new DecodingContext();
context.setRawId(rawId);
context.setUserDefined(userDefined);
context.setRouteCode(routeCode);
context.setReserved(reserved);
return context;
}
}
class DecodingContext {
private long rawId;
private long userDefined;
private long routeCode;
private long reserved;
public boolean isNewFormat() {
return routeCode != 0;
}
}
RouteServiceDecoder实现:
java
public class RouteServiceDecoder implements Decoder<OrderIdContext, Long> {
@Override
public OrderIdContext decode(long orderId) {
// 1. 先调用CoreDecoder提取编码值
DecodingContext context = coreDecoder.decode(orderId);
// 2. 判断是否为新订单号
if (!context.isNewFormat()) {
return null; // 旧订单号,返回null由下游处理
}
// 3. 调用RouteService 解码服务
RouteDecodeResult result = routeService.decode(context.getRouteCode());
// 4. 只有SUCCESS状态才认为解码成功
if (result.getStatus() != DecodeStatus.SUCCESS) {
return null;
}
// 5. 封装返回
OrderIdContext orderContext = new OrderIdContext();
orderContext.setRawOrderId(context.getRawId());
orderContext.setRegionCode(result.getRegionCode());
orderContext.setRequestFrom(result.getRequestFrom());
orderContext.setUserDefined(context.getUserDefined());
return orderContext;
}
}
OISDecoder 实现(降级):
java
public class OISDecoder implements Decoder<OrderIdContext, Long> {
private OrderIndexQueryService oiService;
private boolean degradeEnabled; // 降级开关
@Override
public OrderIdContext decode(long orderId) {
// 1. 检查降级开关
if (!degradeEnabled) {
return null;
}
try {
// 2. 查询存量订单索引服务
OrderIndexResult result = oiService.query(orderId);
if (result == null) {
return null; // 索引不存在
}
// 3. 封装返回
OrderIdContext context = new OrderIdContext();
context.setRawOrderId(orderId); // 旧订单号,rawId就是orderId
context.setRegionCode(result.getRegionCode());
context.setRequestFrom(result.getRequestFrom());
return context;
} catch (Exception e) {
// 4. 查询失败,返回null
logger.warn("OIS decoder failed for orderId: {}", orderId, e);
return null;
}
}
}
解码器责任链:
java
public class DecoderChain {
private List<Decoder<OrderIdContext, Long>> decoders;
public DecoderChain() {
this.decoders = Arrays.asList(
new RouteServiceDecoder(), // 第一优先级:RouteService 解码
new OISDecoder() // 第二优先级:OIS 降级查询
);
}
public OrderIdContext decode(long orderId) {
for (Decoder<OrderIdContext, Long> decoder : decoders) {
OrderIdContext result = decoder.decode(orderId);
if (result != null) {
return result; // 解码成功,直接返回
}
}
return null; // 所有解码器都失败
}
}
2.4 降级与容错机制
2.4.1 编码降级策略
降级场景:
| 场景 | 降级策略 | 结果 |
|---|---|---|
| 全局开关关闭 | 直接返回原始ID | 不编码 |
| 路由编码失败 | 返回UNKNOWN编码(512) | 订单号有效,但路由信息不完整 |
| 原始ID生成失败 | 抛出异常 | 订单创建失败 |
编码兜底逻辑:
java
public class OrderIdGeneratorImpl implements OrderIdGenerator {
@Override
public long generateOrderId(String regionCode, String uid) {
try {
// 1. 检查全局开关
if (!featureSwitch.isEnabled(appId, regionCode)) {
// 降级:只返回原始ID
return rawIdGenerator.generate();
}
// 2. 正常编码流程
EncodingContext context = adapter.generate(regionCode, uid);
return coreGenerator.encode(context);
} catch (Exception e) {
logger.error("Generate orderId failed", e);
// 3. 兜底:返回原始ID(如果原始ID也失败,则抛异常)
return rawIdGenerator.generate();
}
}
}
2.4.2 解码降级策略
降级流程:
容错原则:
- 解码失败返回null:不污染数据库RegionCode 字段
- 路由由 RouteService兜底:应用层解码失败后,路由逻辑交给 RouteService 决定
- 索引服务可降级:OIS 服务故障不影响主流程
2.5 使用示例
2.5.1 场景1:订单ID与业务数据分离
适用场景:订单号可提前生成,创建订单时绑定
java
// 1. 用户提供原始ID生成器
RawIdGenerator<Long> rawIdGenerator = () -> {
// 调用已有的 ID 生成服务(如某业务线封装好的订单 ID 获取方式)
return existingIdService.generateId();
};
// 2. 获取OrderIdGenerator(通过SDK工厂类)
OrderIdGenerator orderIdGenerator =
IdGeneratorFactory.getInstance(rawIdGenerator);
// 3. 生成订单号
String regionCode = "DC-D"; // 用户 RegionCode:地域 D
String uid = "12345"; // 用户ID
long orderId = orderIdGenerator.generateOrderId(regionCode, uid);
// 4. 创建订单(绑定订单号)
createOrder(orderId, orderData);
2.5.2 订单号解码
java
// 1. 获取解码器(通过SDK工厂类)
OrderIdDecoder decoder = IdDecoderFactory.getInstance();
// 2. 解码单个订单号
long orderId = 1111222233334444L;
OrderIdContext context = decoder.decode(orderId);
if (context != null) {
String regionCode = context.getRegionCode(); // "DC-D"
String requestFrom = context.getRequestFrom(); // "BRAND_A"
long rawId = context.getRawOrderId(); // 原始ID
}
// 3. 批量解码
List<Long> orderIds = Arrays.asList(111L, 222L, 333L);
List<? extends OrderIdContext> results = decoder.batchDecode(orderIds);
2.5.3 扩展接口:支持userDefined
适用场景:需要使用业务位(如DataSync 同步标识)
java
// 1. 检查配置(避免冲突)
// 注意:使用扩展接口时,需确保相关配置开关已正确设置
// 2. 获取扩展生成器(仅供特定业务使用)
ExtendedOrderIdGenerator generator =
IdGeneratorFactory.getExtendedInstance(rawIdGenerator);
// 3. 生成订单号(指定userDefined)
String regionCode = "DC-E"; // 用户 RegionCode:地域 E
String uid = "12345";
long userDefined = 1L; // 业务位=1
long orderId = generator.generateOrderId(regionCode, uid, userDefined);
// 4. 解码时可获取userDefined
OrderIdDecoder decoder = IdDecoderFactory.getInstance();
OrderIdContext context = decoder.decode(orderId);
Long userDefined = context.getUserDefined(); // 1L
2.6 监控与可观测性
关键监控指标:
- 编码侧:编码成功率、UNKNOWN编码产生率、降级触发次数
- 解码侧:解码成功率(区分新/旧格式)、OIS降级调用率
- 延迟监控:RouteService 编解码延迟、OIS 查询延迟
日志关注点:
- ERROR 级别:编码失败(需立即关注)
- WARN 级别:UNKNOWN编码产生、解码失败
- INFO 级别:OIS降级查询、灰度过滤记录
监控设计建议:
- 实时告警:编码/解码失败率超过阈值时触发告警
- 趋势分析:监控UNKNOWN编码产生率,分析原因并优化
- 降级观测:跟踪OIS降级调用比例,评估服务依赖健康度
- 性能追踪:关注P99延迟,确保编解码操作在1ms内完成
参考资料
分布式ID生成:
分库分表与分布式系统:
- ShardingSphere官方文档
- Designing Data-Intensive Applications - Martin Kleppmann