在高併發遊戲伺服器中,異步持久化、分庫分表、跨服合服等場景,要求在寫入資料庫前生成全局唯一 ID。本文對比四大主流方案並提供生產級改進版 Snowflake 實現。
| 方案 | 優勢 | 劣勢 | 適用場景 |
|---|---|---|---|
| Java UUID | 本地生成、無依賴、水平擴展極強 | 128bit 佔用大、無序、索引效能極差、無時間語義 | 非關鍵路徑、日誌追蹤 ID、臨時標識 |
| Redis INCR | 原子遞增、部署簡單、支援步長、叢集高可用 | 高併發單點瓶頸、網絡開銷、依賴外部服務 | 中小型遊戲、單服併發 < 5萬 QPS |
| Flicker 方案 | 資料庫自增、強一致 | 單點寫入、架構複雜、擴展困難、不推薦 | 極少見,歷史參考 |
| Snowflake | 高效能、低延遲、趨勢遞增、無中心依賴 | 需獨立部署/維護、時鐘回撥風險、機器 ID 分配管理 | 大型分散式遊戲、高併發核心業務 |
64-bit Long 結構劃分:
| 1bit 符號位 | 41bit 時間戳(毫秒) | 10bit 機器ID | 12bit 序列號 | | 0 | 41 bits | 10 bits | 12 bits |
public class GameIdGenerator {
// ========== 位分配配置 ==========
private static final long EPOCH = 1609459200000L; // 2021-01-01 00:00:00 GMT
private static final long REGION_ID_BITS = 3L; // 區域/機房:8 個
private static final long WORKER_ID_BITS = 10L; // 機器 ID:1024 個
private static final long SEQUENCE_BITS = 10L; // 序列號:1024/毫秒
private static final long MAX_REGION_ID = ~(-1L << REGION_ID_BITS);
private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS);
private static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);
private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;
private static final long REGION_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + REGION_ID_BITS;
// ========== 運行狀態 ==========
private long lastTimestamp = -1L;
private long sequence = 0L;
private final long workerId;
private final long regionId;
private final SecureRandom random = new SecureRandom();
// ========== 建構子 ==========
public GameIdGenerator(long workerId, long regionId) {
if (workerId < 0 || workerId > MAX_WORKER_ID) {
throw new IllegalArgumentException("workerId 超出範圍 [0, " + MAX_WORKER_ID + "]");
}
if (regionId < 0 || regionId > MAX_REGION_ID) {
throw new IllegalArgumentException("regionId 超出範圍 [0, " + MAX_REGION_ID + "]");
}
this.workerId = workerId;
this.regionId = regionId;
}
// 僅機器 ID,區域預設 0
public GameIdGenerator(long workerId) {
this(workerId, 0);
}
// ========== 核心生成邏輯 ==========
public synchronized long nextId() {
long timestamp = currentTimeMillis();
// 時鐘回撥檢測
if (timestamp < lastTimestamp) {
long offset = lastTimestamp - timestamp;
if (offset > 5) { // 超過 5ms 視為嚴重回撥,拋異常或等待
throw new IllegalStateException("時鐘回撥 " + offset + "ms,拒絕生成 ID");
}
// 輕微回撥:自旋等待
timestamp = waitNextMillis(lastTimestamp);
}
// 同毫秒內序列號遞增
if (timestamp == lastTimestamp) {
sequence = (sequence + 1) & SEQUENCE_MASK;
if (sequence == 0) { // 序列號耗盡
timestamp = waitNextMillis(lastTimestamp);
}
} else {
// 新毫秒:序列號重置為隨機值(0-9),改善尾數分布均勻性
sequence = random.nextInt(10);
}
lastTimestamp = timestamp;
// 組裝 ID
return ((timestamp - EPOCH) << TIMESTAMP_SHIFT)
| (regionId << REGION_ID_SHIFT)
| (workerId << WORKER_ID_SHIFT)
| sequence;
}
// ========== 輔助方法 ==========
private long waitNextMillis(long lastTimestamp) {
long timestamp = currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = currentTimeMillis();
}
return timestamp;
}
protected long currentTimeMillis() {
return System.currentTimeMillis();
}
// ========== 解析方法(除錯/監控用) ==========
public static IdComponents parse(long id) {
long sequence = id & SEQUENCE_MASK;
long workerId = (id >> WORKER_ID_SHIFT) & MAX_WORKER_ID;
long regionId = (id >> REGION_ID_SHIFT) & MAX_REGION_ID;
long timestamp = (id >> TIMESTAMP_SHIFT) + EPOCH;
return new IdComponents(timestamp, regionId, workerId, sequence);
}
public record IdComponents(long timestamp, long regionId, long workerId, long sequence) {}
}
| 改進點 | 問題 | 解決方案 |
|---|---|---|
| 區域/機房位 | 跨機房合服、多區域部署 ID 隔離 | 增加 3bit regionId,支援 8 個機房 |
| 時鐘回撥保護 | NTP 同步導致時間倒退,產生重複 ID | 檢測回撥 > 5ms 拋異常,<=5ms 自旋等待 |
| 序列號隨機化 | 每毫秒序列號歸 0,導致 ID 尾數分布不均(取模分表不均) | 跨毫秒重置為 0-9 隨機數,改善分布 |
| SecureRandom | 預設 Random 線程安全但效能低 | ThreadLocalRandom 或 SecureRandom 實例化一次 |