首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP 生成唯一訂單號的多種演算法對比與高併發實戰方案

     編輯:PHP 相關知識

PHP 生成唯一訂單號的多種演算法對比與高併發實戰方案

訂單號是電商、支付、物流系統的核心標識,需滿足唯一性、可追溯、無敏感信息洩露、長度適中、高併發不重複等要求。本文對比多種生成策略,給出生產級推薦方案。

常見演算法對比

方案核心邏輯長度唯一性保障併發性能信息攜帶缺點
時間戳+隨機數date('YmdHis') + rand(1000,9999)18-20位極弱(同秒重複)高時間高併發必重
微秒時間戳microtime(true) * 1000017-18位弱(同微秒重複)高時間單機單進程同微秒衝突
UUID v1/v4uniqid()/random_bytes()36位極強(統計唯一)中無太長、無業務含義
雪花算法41bit時間+10bit機器+12bit序列19-20位極強(分佈式唯一)極高時間、機器依賴時鐘同步、機器ID分配
資料庫自增IDINSERT 獲取 lastInsertId遞增極強(DB保證)低(寫入瓶頸)遞增序號暴露業務量、單點寫入
Redis INCRRedis 原子自增 + 前綴可控極強高時間、序號依賴 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位)
}

方案二:日期 + uniqid() 截取(原文方案二改進)

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; // 添加業務前綴

方案四:Redis 原子自增(單機/集群中小規模)

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 最簡單;分佈式高併發首選雪花算法;強審計合規用數據庫預生成池。無論哪種方案,壓測驗證唯一性是上線前必經環節。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved