訂單號是電商、支付、物流系統的核心標識,需滿足唯一性、可追溯、無敏感信息洩露、長度適中、高併發不重複等要求。本文對比多種生成策略,給出生產級推薦方案。
| 方案 | 核心邏輯 | 長度 | 唯一性保障 | 併發性能 | 信息攜帶 | 缺點 |
|---|---|---|---|---|---|---|
| 時間戳+隨機數 | date('YmdHis') + rand(1000,9999) | 18-20位 | 極弱(同秒重複) | 高 | 時間 | 高併發必重 |
| 微秒時間戳 | microtime(true) * 10000 | 17-18位 | 弱(同微秒重複) | 高 | 時間 | 單機單進程同微秒衝突 |
| UUID v1/v4 | uniqid()/random_bytes() | 36位 | 極強(統計唯一) | 中 | 無 | 太長、無業務含義 |
| 雪花算法 | 41bit時間+10bit機器+12bit序列 | 19-20位 | 極強(分佈式唯一) | 極高 | 時間、機器 | 依賴時鐘同步、機器ID分配 |
| 資料庫自增ID | INSERT 獲取 lastInsertId | 遞增 | 極強(DB保證) | 低(寫入瓶頸) | 遞增序號 | 暴露業務量、單點寫入 |
| Redis INCR | Redis 原子自增 + 前綴 | 可控 | 極強 | 高 | 時間、序號 | 依賴 Redis 可用性 |
function generateOrderSnV1(): string {
// 年份字母編碼:2024=A, 2025=B... 支持 26 年
$yearCode = chr(ord('A') + (int)date('y') - 24); // 2024年起
// 月份十六進制:1-9, A-C
$monthHex = dechex((int)date('m'));
// 日期
$day = date('d');
// 秒級時間戳後 5 位
$secSuffix = substr((string)time(), -5);
// 微秒後 5 位
$microSuffix = substr((string)(microtime(true) * 10000), -5);
// 2 位隨機數
$randSuffix = sprintf('%02d', random_int(0, 99));
return "{$yearCode}{$monthHex}{$day}{$secSuffix}{$microSuffix}{$randSuffix}";
// 示例:B422694333616096(16位)
}
function generateOrderSnV2(): string {
$datePrefix = date('Ymd'); // 20240623
$uniqid = uniqid('', true); // 如 5f3a8b2c1d4e6.12345678
$suffix = substr(str_replace('.', '', $uniqid), 7, 13); // 取中間 13 位
return $datePrefix . substr($suffix, 0, 8); // 共 16 位
// 示例:2024062356100485
}
class SnowflakeIdGenerator {
private int $workerId;
private int $datacenterId;
private int $sequence = 0;
private int $lastTimestamp = -1;
// 位分配:1bit符號 | 41bit毫秒時間戳 | 5bit數據中心 | 5bit機器 | 12bit序列
private const WORKER_ID_BITS = 5;
private const DATACENTER_ID_BITS = 5;
private const SEQUENCE_BITS = 12;
private const MAX_WORKER_ID = -1 ^ (-1 << self::WORKER_ID_BITS); // 31
private const MAX_DATACENTER_ID = -1 ^ (-1 << self::DATACENTER_ID_BITS); // 31
private const SEQUENCE_MASK = -1 ^ (-1 << self::SEQUENCE_BITS); // 4095
private const WORKER_ID_SHIFT = self::SEQUENCE_BITS; // 12
private const DATACENTER_ID_SHIFT = self::SEQUENCE_BITS + self::WORKER_ID_BITS; // 17
private const TIMESTAMP_LEFT_SHIFT = self::SEQUENCE_BITS + self::WORKER_ID_BITS + self::DATACENTER_ID_BITS; // 22
// 自定義起始時間戳(2024-01-01 00:00:00),延長可用年限
private const EPOCH = 1704038400000; // 毫秒
public function __construct(int $workerId, int $datacenterId) {
if ($workerId > self::MAX_WORKER_ID || $workerId < 0) {
throw new \InvalidArgumentException("WorkerId 超出範圍 [0, " . self::MAX_WORKER_ID . "]");
}
if ($datacenterId > self::MAX_DATACENTER_ID || $datacenterId < 0) {
throw new \InvalidArgumentException("DatacenterId 超出範圍 [0, " . self::MAX_DATACENTER_ID . "]");
}
$this->workerId = $workerId;
$this->datacenterId = $datacenterId;
}
public function nextId(): int {
$timestamp = $this->timeGen();
if ($timestamp < $this->lastTimestamp) {
throw new \RuntimeException("時鐘回撥: 拒絕生成 ID 等待 " . ($this->lastTimestamp - $timestamp) . "ms");
}
if ($timestamp === $this->lastTimestamp) {
$this->sequence = ($this->sequence + 1) & self::SEQUENCE_MASK;
if ($this->sequence === 0) {
$timestamp = $this->tilNextMillis($this->lastTimestamp);
}
} else {
$this->sequence = 0;
}
$this->lastTimestamp = $timestamp;
return (($timestamp - self::EPOCH) << self::TIMESTAMP_LEFT_SHIFT)
| ($this->datacenterId << self::DATACENTER_ID_SHIFT)
| ($this->workerId << self::WORKER_ID_SHIFT)
| $this->sequence;
}
private function tilNextMillis(int $lastTimestamp): int {
$timestamp = $this->timeGen();
while ($timestamp <= $lastTimestamp) {
$timestamp = $this->timeGen();
}
return $timestamp;
}
private function timeGen(): int {
return (int)(microtime(true) * 1000);
}
}
// 使用示例
$generator = new SnowflakeIdGenerator(workerId: 1, datacenterId: 1);
$orderId = $generator->nextId(); // 如 1234567890123456789
$orderSn = 'ORD' . $orderId; // 添加業務前綴
function generateOrderSnRedis(\Redis $redis, string $prefix = 'ORD'): string {
// KEY 格式:order:sn:20240623 → 每日重置,長度可控
$dateKey = $prefix . ':sn:' . date('Ymd');
$seq = $redis->incr($dateKey);
// 設置 25 小時過期,自動清理
if ($seq === 1) $redis->expire($dateKey, 90000);
return sprintf('%s%s%06d', $prefix, date('Ymd'), $seq);
// 示例:ORD20240623000001
}
// 訂單號池表
CREATE TABLE order_sn_pool (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
sn VARCHAR(32) NOT NULL UNIQUE,
used TINYINT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;
// 批量預生成(定時任務/腳本)
INSERT INTO order_sn_pool (sn)
SELECT CONCAT('ORD', DATE_FORMAT(NOW(), '%Y%m%d'), LPAD(id, 6, '0'))
FROM (SELECT @rownum:=@rownum+1 AS id FROM information_schema.columns a, information_schema.columns b, (SELECT @rownum:=0) r LIMIT 10000) t;
// 業務取號(原子操作)
function getOrderSnFromPool(\PDO $pdo): string {
$pdo->beginTransaction();
try {
$stmt = $pdo->prepare("UPDATE order_sn_pool SET used=1 WHERE id=(SELECT id FROM order_sn_pool WHERE used=0 ORDER BY id LIMIT 1 FOR UPDATE)");
$stmt->execute();
$sn = $pdo->query("SELECT sn FROM order_sn_pool WHERE used=1 ORDER BY id DESC LIMIT 1")->fetchColumn();
$pdo->commit();
return $sn;
} catch (\Throwable $e) {
$pdo->rollBack();
throw $e;
}
}
┌─ 是否分佈式部署?──否──► 單機:Redis INCR / 微秒+隨機 / 預生成池 │ 是 │ ├─ 是否極致性能(>10萬 QPS)?──是──► 雪花算法(本地生成,零網絡開銷) │ 否 │ ├─ 是否強一致性/審計要求?──是──► 數據庫預生成池 / 單表自增ID │ 否 │ └─ 推薦:Redis INCR(每日重置)或 雪花算法(擴展性最佳)
| 檢查項 | 驗證方法 | 風險等級 |
|---|---|---|
| 唯一性壓測 | 併發 10萬 次生成,檢查重複率 | 🔴 致命 |
| 時鐘回撥測試 | 手動調慢系統時間,驗證雪花算法拋異常 | 🟠 高 |
| 機器 ID 衝突 | 多實例部署驗證 WorkerId 分配機制 | 🟠 高 |
| 長度溢出 | 極端時間/序列值測試字符串長度 | 🟡 中 |
| 可讀性/可追溯 | 從訂單號反解出時間、機器、業務類型 | 🟢 低 |
| 敏感信息洩露 | 訂單號不包含用戶ID、金額、庫存等敏感數據 | 🔴 致命 |
選擇訂單號生成方案的核心原則:業務屬性決定技術選型。單機中小規模用 Redis INCR 最簡單;分佈式高併發首選雪花算法;強審計合規用數據庫預生成池。無論哪種方案,壓測驗證唯一性是上線前必經環節。