分布式订单系统:订单号编码设计实战
引言:订单号的那些坑
之前公司的订单系统因为订单号设计不合理导致了一系列问题:
- 订单号重复:两个用户竟然收到了相同的订单号,客服接到投诉电话打爆
- 订单号泄露信息:用户通过订单号推算出当天的订单量,竞争对手知道了我们的销售数据
- 订单号过长:用户截图分享时订单号占了一整行,影响用户体验
- 分库分表困难:订单号无法作为分片键,导致数据迁移成本极高
订单号是电商系统的核心标识,看似简单,实则暗藏玄机。本文将带你深入理解分布式订单号设计,并提供多种实战方案。
一、订单号设计原则
1.1 核心要求
┌─────────────────────────────────────────────────────────────┐
│ 订单号设计的核心要求 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 全局唯一 │
│ └─> 绝对不能重复,这是底线 │
│ │
│ 2. 趋势递增 │
│ └─> 便于索引,提升查询性能 │
│ │
│ 3. 信息不泄露 │
│ └─> 不能暴露业务量、时间等敏感信息 │
│ │
│ 4. 长度合理 │
│ └─> 便于用户记忆和传输,通常 16-32 位 │
│ │
│ 5. 高性能 │
│ └─> 生成速度要快,不能成为性能瓶颈 │
│ │
│ 6. 可扩展性 │
│ └─> 支持分库分表,便于水平扩展 │
│ │
└─────────────────────────────────────────────────────────────┘
1.2 常见错误
| 错误类型 | 示例 | 问题 |
|---|---|---|
| 自增ID | 1, 2, 3, ... | 泄露业务量,不支持分布式 |
| 时间戳 | 202403081200001 | 同一毫秒内可能重复 |
| UUID | 550e8400-e29b-41d4-a716-446655440000 | 无序,过长,不易记忆 |
| 随机数 | 1234567890123456 | 可能重复,无法排序 |
二、订单号设计方案对比
2.1 方案一:数据库自增
CREATE TABLE `order_seq` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`biz_type` varchar(32) NOT NULL COMMENT '业务类型',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_biz_type` (`biz_type`)
) ENGINE=InnoDB;
优点:
- 简单易用,数据库原生支持
- 绝对唯一,不会重复
- 趋势递增,便于索引
缺点:
- 性能瓶颈,数据库压力大
- 不支持分布式,单点故障
- 泄露业务量
适用场景:单机系统,低并发场景
2.2 方案二:Redis 自增
Long orderId = redisTemplate.opsForValue().increment("order:seq:20240308");
优点:
- 性能高,Redis 单机可达 10万 QPS
- 支持分布式
- 可以按日期分段
缺点:
- Redis 单点故障风险
- 需要持久化保证可靠性
- 需要处理 Redis 宕机情况
适用场景:中高并发场景,已有 Redis 基础设施
2.3 方案三:雪花算法
0 | 0001100 10100010 10111110 10001001 01011100 00 | 10001 | 1 1010 0101 0000 0000 0000 0000 0000 0000 0000
└─┬─┘ └──────────────────────────────────────────────┬─┘ └──┬───┘ └─────────────────────────────────────────┬─┘
│ 时间戳(41位) │ 工作ID(5位) 序列号(12位)
│ 数据中心ID(5位)
符号位(1位,固定为0)
优点:
- 性能极高,单机可达 400万 QPS
- 趋势递增,按时间排序
- 不依赖第三方组件
- 支持分布式
缺点:
- 时钟回拨问题
- 工作ID 需要管理
- 长度较长(19位)
适用场景:高并发场景,分布式系统
2.4 方案四:业务编码 + 序列号
订单号格式:业务类型(2位) + 时间(8位) + 序列号(8位) + 校验位(2位)
示例:OD2024030800000001AB
优点:
- 包含业务信息,便于识别
- 可以按业务类型分表
- 长度可控
- 支持校验
缺点:
- 生成逻辑复杂
- 需要管理序列号
- 可能泄露业务信息
适用场景:需要业务识别的场景
2.5 方案对比总结
| 方案 | 性能 | 唯一性 | 分布式 | 复杂度 | 适用场景 |
|---|---|---|---|---|---|
| 数据库自增 | 低 | ✅ | ❌ | 低 | 单机低并发 |
| Redis 自增 | 高 | ✅ | ✅ | 中 | 中高并发 |
| 雪花算法 | 极高 | ✅ | ✅ | 中 | 高并发分布式 |
| 业务编码 | 中 | ✅ | ✅ | 高 | 需要业务识别 |
三、雪花算法深度解析
3.1 算法原理
┌─────────────────────────────────────────────────────────────┐
│ 雪花算法位结构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 0 | 0001100 10100010 10111110 10001001 01011100 00 | 10001 | 1 1010 0101 0000 0000 0000 0000 0000 0000 0000
│ └─┬─┘ └──────────────────────────────────────────────┬─┘ └──┬───┘ └─────────────────────────────────────────┬─┘
│ │ 时间戳(41位) │ 工作ID(5位) 序列号(12位)
│ │ 数据中心ID(5位)
│ 符号位(1位,固定为0)
│ │
│ 总长度:64位 │
│ 时间戳:41位(毫秒级,可用69年) │
│ 数据中心ID:5位(32个数据中心) │
│ 工作ID:5位(32个工作节点) │
│ 序列号:12位(每毫秒4096个ID) │
│ │
└─────────────────────────────────────────────────────────────┘
3.2 核心代码
public class SnowflakeIdGenerator {
// 起始时间戳(2020-01-01 00:00:00)
private static final long START_TIMESTAMP = 1577808000000L;
// 各部分位数
private static final long SEQUENCE_BIT = 12L; // 序列号占12位
private static final long WORKER_BIT = 5L; // 工作ID占5位
private static final long DATACENTER_BIT = 5L; // 数据中心ID占5位
private static final long TIMESTAMP_BIT = 41L; // 时间戳占41位
// 各部分最大值
private static final long MAX_SEQUENCE = ~(-1L << SEQUENCE_BIT);
private static final long MAX_WORKER = ~(-1L << WORKER_BIT);
private static final long MAX_DATACENTER = ~(-1L << DATACENTER_BIT);
// 各部分位移
private static final long WORKER_SHIFT = SEQUENCE_BIT;
private static final long DATACENTER_SHIFT = SEQUENCE_BIT + WORKER_BIT;
private static final long TIMESTAMP_SHIFT = SEQUENCE_BIT + WORKER_BIT + DATACENTER_BIT;
private final long workerId; // 工作ID
private final long datacenterId; // 数据中心ID
private long sequence = 0L; // 序列号
private long lastTimestamp = -1L; // 上次生成ID的时间戳
public SnowflakeIdGenerator(long workerId, long datacenterId) {
if (workerId > MAX_WORKER || workerId < 0) {
throw new IllegalArgumentException("工作ID 超出范围");
}
if (datacenterId > MAX_DATACENTER || datacenterId < 0) {
throw new IllegalArgumentException("数据中心ID 超出范围");
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
public synchronized long nextId() {
long currentTimestamp = getCurrentTimestamp();
// 时钟回拨处理
if (currentTimestamp < lastTimestamp) {
throw new RuntimeException("时钟回拨,拒绝生成ID");
}
// 同一毫秒内,序列号递增
if (currentTimestamp == lastTimestamp) {
sequence = (sequence + 1) & MAX_SEQUENCE;
// 序列号溢出,等待下一毫秒
if (sequence == 0) {
currentTimestamp = waitNextMillis(lastTimestamp);
}
} else {
// 新的毫秒,序列号重置
sequence = 0L;
}
lastTimestamp = currentTimestamp;
// 组装ID
return ((currentTimestamp - START_TIMESTAMP) << TIMESTAMP_SHIFT)
| (datacenterId << DATACENTER_SHIFT)
| (workerId << WORKER_SHIFT)
| sequence;
}
private long waitNextMillis(long lastTimestamp) {
long currentTimestamp = getCurrentTimestamp();
while (currentTimestamp <= lastTimestamp) {
currentTimestamp = getCurrentTimestamp();
}
return currentTimestamp;
}
private long getCurrentTimestamp() {
return System.currentTimeMillis();
}
}
3.3 时钟回拨问题
问题原因:
- 服务器时钟不准确
- NTP 时间同步导致时钟回拨
- 虚拟机迁移导致时钟变化
解决方案:
方案一:拒绝服务
if (currentTimestamp < lastTimestamp) {
throw new RuntimeException("时钟回拨,拒绝生成ID");
}
方案二:等待时钟追上
if (currentTimestamp < lastTimestamp) {
long offset = lastTimestamp - currentTimestamp;
if (offset <= 5) { // 5ms 以内,等待
Thread.sleep(offset * 2);
currentTimestamp = getCurrentTimestamp();
} else {
throw new RuntimeException("时钟回拨超过阈值");
}
}
方案三:使用备用时间戳
if (currentTimestamp < lastTimestamp) {
currentTimestamp = lastTimestamp; // 使用上次时间戳
sequence = (sequence + 1) & MAX_SEQUENCE;
}
四、Spring Boot 实战
4.1 项目依赖
<dependencies>
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Starter Data Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Spring Boot Starter Data JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- MySQL Driver -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
4.2 雪花算法生成器
@Component
@Slf4j
public class SnowflakeIdGenerator {
@Value("${snowflake.worker-id:1}")
private long workerId;
@Value("${snowflake.datacenter-id:1}")
private long datacenterId;
private long sequence = 0L;
private long lastTimestamp = -1L;
@PostConstruct
public void init() {
log.info("雪花算法初始化:workerId={}, datacenterId={}", workerId, datacenterId);
}
public synchronized long nextId() {
long currentTimestamp = System.currentTimeMillis();
if (currentTimestamp < lastTimestamp) {
throw new RuntimeException("时钟回拨,拒绝生成ID");
}
if (currentTimestamp == lastTimestamp) {
sequence = (sequence + 1) & 4095L;
if (sequence == 0) {
currentTimestamp = waitNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = currentTimestamp;
return ((currentTimestamp - 1577808000000L) << 22)
| (datacenterId << 17)
| (workerId << 12)
| sequence;
}
private long waitNextMillis(long lastTimestamp) {
long currentTimestamp = System.currentTimeMillis();
while (currentTimestamp <= lastTimestamp) {
currentTimestamp = System.currentTimeMillis();
}
return currentTimestamp;
}
}
4.3 Redis 订单号生成器
@Service
@Slf4j
public class RedisOrderIdGenerator {
@Autowired
private RedisTemplate<String, Long> redisTemplate;
private static final String ORDER_SEQ_KEY = "order:seq:";
/**
* 生成订单号(按日期分段)
*/
public String generateOrderId() {
String dateKey = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
String key = ORDER_SEQ_KEY + dateKey;
Long seq = redisTemplate.opsForValue().increment(key);
// 设置过期时间(第二天凌晨)
long ttl = LocalDateTime.now().until(LocalDateTime.now().plusDays(1).withHour(0).withMinute(0).withSecond(0),
ChronoUnit.SECONDS);
redisTemplate.expire(key, ttl, TimeUnit.SECONDS);
// 格式化订单号:日期(8位) + 序列号(8位)
return dateKey + String.format("%08d", seq);
}
/**
* 生成业务订单号
*/
public String generateBusinessOrderId(String bizType) {
String dateKey = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
String key = ORDER_SEQ_KEY + bizType + ":" + dateKey;
Long seq = redisTemplate.opsForValue().increment(key);
// 设置过期时间
long ttl = LocalDateTime.now().until(LocalDateTime.now().plusDays(1).withHour(0).withMinute(0).withSecond(0),
ChronoUnit.SECONDS);
redisTemplate.expire(key, ttl, TimeUnit.SECONDS);
// 格式化订单号:业务类型(2位) + 日期(8位) + 序列号(8位)
return bizType + dateKey + String.format("%08d", seq);
}
}
4.4 数据库序列生成器
@Service
@Slf4j
public class DatabaseOrderIdGenerator {
@Autowired
private OrderSeqRepository orderSeqRepository;
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 生成订单号(使用数据库序列)
*/
@Transactional
public String generateOrderId() {
String dateKey = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
// 使用 SELECT FOR UPDATE 获取序列号
String sql = "INSERT INTO order_seq (biz_type) VALUES (?) ON DUPLICATE KEY UPDATE id = LAST_INSERT_ID(id + 1)";
Long seq = jdbcTemplate.queryForObject(sql, Long.class, dateKey);
// 格式化订单号:日期(8位) + 序列号(8位)
return dateKey + String.format("%08d", seq);
}
/**
* 批量生成订单号
*/
@Transactional
public List<String> generateOrderIds(int count) {
String dateKey = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
String sql = "INSERT INTO order_seq (biz_type) VALUES (?) ON DUPLICATE KEY UPDATE id = LAST_INSERT_ID(id + ?)";
jdbcTemplate.update(sql, dateKey, count);
Long startSeq = jdbcTemplate.queryForObject("SELECT LAST_INSERT_ID()", Long.class);
List<String> orderIds = new ArrayList<>();
for (int i = 0; i < count; i++) {
orderIds.add(dateKey + String.format("%08d", startSeq - count + i + 1));
}
return orderIds;
}
}
4.5 业务编码生成器
@Service
@Slf4j
public class BusinessOrderIdGenerator {
@Autowired
private RedisOrderIdGenerator redisOrderIdGenerator;
@Autowired
private SnowflakeIdGenerator snowflakeIdGenerator;
/**
* 生成普通订单号
*/
public String generateNormalOrder() {
return "OD" + redisOrderIdGenerator.generateOrderId();
}
/**
* 生成秒杀订单号
*/
public String generateSeckillOrder() {
return "SK" + redisOrderIdGenerator.generateBusinessOrderId("SK");
}
/**
* 生成预售订单号
*/
public String generatePresaleOrder() {
return "PS" + redisOrderIdGenerator.generateBusinessOrderId("PS");
}
/**
* 生成退款订单号
*/
public String generateRefundOrder() {
return "RF" + redisOrderIdGenerator.generateBusinessOrderId("RF");
}
/**
* 生成支付流水号
*/
public String generatePaymentNo() {
return "PAY" + String.valueOf(snowflakeIdGenerator.nextId());
}
/**
* 生成物流单号
*/
public String generateLogisticsNo() {
return "LG" + redisOrderIdGenerator.generateBusinessOrderId("LG");
}
}
4.6 订单服务
@Service
@Slf4j
public class OrderService {
@Autowired
private BusinessOrderIdGenerator orderIdGenerator;
@Autowired
private OrderRepository orderRepository;
/**
* 创建订单
*/
@Transactional
public Order createOrder(OrderDTO orderDTO) {
// 生成订单号
String orderId = orderIdGenerator.generateNormalOrder();
Order order = new Order();
order.setOrderId(orderId);
order.setUserId(orderDTO.getUserId());
order.setAmount(orderDTO.getAmount());
order.setStatus(OrderStatus.PENDING);
order.setCreateTime(LocalDateTime.now());
orderRepository.save(order);
log.info("订单创建成功:{}", orderId);
return order;
}
/**
* 创建秒杀订单
*/
@Transactional
public Order createSeckillOrder(OrderDTO orderDTO) {
String orderId = orderIdGenerator.generateSeckillOrder();
Order order = new Order();
order.setOrderId(orderId);
order.setUserId(orderDTO.getUserId());
order.setAmount(orderDTO.getAmount());
order.setStatus(OrderStatus.PENDING);
order.setOrderType(OrderType.SECKILL);
order.setCreateTime(LocalDateTime.now());
orderRepository.save(order);
log.info("秒杀订单创建成功:{}", orderId);
return order;
}
}
五、最佳实践
5.1 订单号设计建议
┌─────────────────────────────────────────────────────────────┐
│ 订单号设计建议 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 根据业务场景选择方案 │
│ └─> 低并发:数据库自增 │
│ └─> 中高并发:Redis 自增 │
│ └─> 高并发分布式:雪花算法 │
│ │
│ 2. 考虑分库分表 │
│ └─> 订单号作为分片键,便于水平扩展 │
│ └─> 使用雪花算法,天然支持分片 │
│ │
│ 3. 避免信息泄露 │
│ └─> 不使用纯时间戳或纯自增ID │
│ └─> 加入随机数或混淆位 │
│ │
│ 4. 控制订单号长度 │
│ └─> 建议 16-32 位 │
│ └─> 便于用户记忆和传输 │
│ │
│ 5. 做好容灾备份 │
│ └─> Redis 集群部署 │
│ └─> 数据库主从复制 │
│ └─> 多机房部署 │
│ │
└─────────────────────────────────────────────────────────────┘
5.2 性能优化
@Service
@Slf4j
public class OptimizedOrderIdGenerator {
// 本地缓存,减少 Redis 访问
private final AtomicLong localSeq = new AtomicLong(0);
private final AtomicLong maxSeq = new AtomicLong(0);
@Autowired
private RedisTemplate<String, Long> redisTemplate;
/**
* 批量预取序列号
*/
@PostConstruct
public void prefetch() {
String dateKey = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
String key = "order:seq:" + dateKey;
// 每次预取 1000 个序列号
long seq = redisTemplate.opsForValue().increment(key, 1000);
localSeq.set(seq - 999);
maxSeq.set(seq);
log.info("预取序列号:{} - {}", localSeq.get(), maxSeq.get());
}
/**
* 生成订单号(使用本地缓存)
*/
public String generateOrderId() {
long currentSeq = localSeq.incrementAndGet();
// 本地缓存用完,重新预取
if (currentSeq > maxSeq.get()) {
synchronized (this) {
if (currentSeq > maxSeq.get()) {
prefetch();
currentSeq = localSeq.incrementAndGet();
}
}
}
String dateKey = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
return dateKey + String.format("%08d", currentSeq);
}
}
5.3 监控告警
@Component
@Slf4j
public class OrderIdMonitor {
@Autowired
private MeterRegistry meterRegistry;
@Autowired
private RedisTemplate<String, Long> redisTemplate;
/**
* 监控订单号生成速率
*/
@Scheduled(fixedRate = 60000)
public void monitorGenerateRate() {
String dateKey = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
String key = "order:seq:" + dateKey;
Long currentSeq = redisTemplate.opsForValue().get(key);
if (currentSeq != null) {
meterRegistry.gauge("order.seq.current", currentSeq);
// 告警:单日订单量超过 100 万
if (currentSeq > 1000000) {
log.warn("单日订单量超过阈值:{}", currentSeq);
}
}
}
/**
* 监控订单号生成延迟
*/
@Scheduled(fixedRate = 60000)
public void monitorGenerateLatency() {
long start = System.currentTimeMillis();
String orderId = generateOrderId();
long latency = System.currentTimeMillis() - start;
meterRegistry.gauge("order.generate.latency", latency);
// 告警:生成延迟超过 10ms
if (latency > 10) {
log.warn("订单号生成延迟过高:{}ms", latency);
}
}
}
六、常见问题与解决方案
6.1 订单号重复
原因:
- 时钟回拨
- Redis 宕机
- 数据库主从同步延迟
解决方案:
- 使用雪花算法,避免时钟回拨
- Redis 集群部署,避免单点故障
- 数据库主从延迟补偿
6.2 订单号泄露信息
原因:
- 使用纯时间戳
- 使用纯自增ID
- 未做混淆处理
解决方案:
- 加入随机数或混淆位
- 使用雪花算法
- 业务编码 + 序列号
6.3 分库分表困难
原因:
- 订单号无法作为分片键
- 订单号不包含分片信息
解决方案:
- 使用雪花算法,天然支持分片
- 订单号中包含分片信息
- 使用一致性哈希
6.4 性能瓶颈
原因:
- 数据库自增性能低
- Redis 单点故障
- 生成逻辑复杂
解决方案:
- 使用雪花算法
- Redis 集群部署
- 本地缓存优化
七、总结
订单号设计看似简单,实则需要考虑全局唯一性、趋势递增、信息不泄露、长度合理、高性能、可扩展性等多个维度。
核心要点:
- 根据业务场景选择合适的方案
- 高并发场景推荐使用雪花算法
- 考虑分库分表,订单号作为分片键
- 做好容灾备份,避免单点故障
- 监控告警,及时发现问题
适用场景:
- 电商订单系统
- 支付流水号
- 物流单号
- 退款单号
- 秒杀订单
如果本文对你有帮助,欢迎关注「服务端技术精选」公众号,获取更多后端技术干货。
互动题:
- 在你的项目中,订单号是如何设计的?遇到过哪些问题?
- 雪花算法的时钟回拨问题你是如何解决的?
- 如何设计一个支持多业务类型的订单号生成系统?
欢迎在评论区分享你的想法和经验,我们一起交流学习!
标题:分布式订单系统:订单号编码设计实战
作者:jiangyi
地址:http://jiangyi.space/articles/2026/03/08/1772943579406.html
公众号:服务端技术精选
- 引言:订单号的那些坑
- 一、订单号设计原则
- 1.1 核心要求
- 1.2 常见错误
- 二、订单号设计方案对比
- 2.1 方案一:数据库自增
- 2.2 方案二:Redis 自增
- 2.3 方案三:雪花算法
- 2.4 方案四:业务编码 + 序列号
- 2.5 方案对比总结
- 三、雪花算法深度解析
- 3.1 算法原理
- 3.2 核心代码
- 3.3 时钟回拨问题
- 方案一:拒绝服务
- 方案二:等待时钟追上
- 方案三:使用备用时间戳
- 四、Spring Boot 实战
- 4.1 项目依赖
- 4.2 雪花算法生成器
- 4.3 Redis 订单号生成器
- 4.4 数据库序列生成器
- 4.5 业务编码生成器
- 4.6 订单服务
- 五、最佳实践
- 5.1 订单号设计建议
- 5.2 性能优化
- 5.3 监控告警
- 六、常见问题与解决方案
- 6.1 订单号重复
- 6.2 订单号泄露信息
- 6.3 分库分表困难
- 6.4 性能瓶颈
- 七、总结
评论
0 评论