業務場景:車輛 VIN 碼接入檢查,若數據庫無記錄或狀態=1(已完成),則插入狀態=0(處理中)並觸發人工處理;若狀態=0 則駁回。單線程無問題,高併發下「查詢不存在 → 並發插入」導致臟數據。本文對比四種方案優劣,給出生產級選型建議。
ALTER TABLE vin_table ADD UNIQUE INDEX uk_vin_status (vin, status);
START TRANSACTION; SELECT status FROM vin_table WHERE vin=? FOR UPDATE; -- 行鎖 IF NOT EXISTS OR status=1 THEN INSERT ...; END IF; COMMIT;
SELECT ... FOR UPDATE 鎖住的是「已存在的行」,對「不存在的行」無鎖定能力(間隙鎖需 REPEATABLE READ 且索引覆蓋),兩並發事務均查不到 → 均插入 → 仍會臟數據。// synchronized 或 ReentrantLock
synchronized(this) {
if (!exists(vin)) insert(vin);
}
private static final ConcurrentHashMap<String, String> PROCESSING_VINS = new ConcurrentHashMap<>();
public boolean tryInsertVin(String vin) {
// 嘗試放入,若已存在說明有並發請求正在處理
String prev = PROCESSING_VINS.putIfAbsent(vin, Thread.currentThread().getName());
if (prev != null) {
return false; // 駁回:已有線程在處理該 VIN
}
try {
if (!dbExists(vin) || dbStatus(vin) == 1) {
dbInsert(vin, 0);
return true;
}
return false;
} finally {
PROCESSING_VINS.remove(vin); // 釋放
}
}
// Redis SET NX EX 實現分佈式鎖
boolean lock = redisTemplate.opsForValue().setIfAbsent("lock:vin:" + vin, requestId, 30, TimeUnit.SECONDS);
if (!lock) return REJECT_CONCURRENT;
try {
// 雙重檢查(防止鎖超時釋放後的極端並發)
if (!dbExists(vin) || dbStatus(vin) == 1) {
dbInsert(vin, 0);
return SUCCESS;
}
return REJECT_DUPLICATE;
} finally {
// Lua 腳本安全釋放鎖(僅刪除自己持有的鎖)
String lua = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
redisTemplate.execute(new DefaultRedisScript<>(lua, Boolean.class), "lock:vin:" + vin, requestId);
}
維度 唯一索引 進程鎖 ConcurrentHashMap Redis 分佈式鎖 ON DUPLICATE KEY 集群支援 ✅ ❌ ❌ ✅ ✅ 業務約束 需唯一鍵 無 無 需 Redis 需唯一鍵 性能 極高 高(單機) 極高 高 極高 實現複雜度 低 低 中 中 低 推薦指數 ⭐⭐⭐ (條件允許) ⭐ (單機) ⭐⭐ (單機+可重啟) ⭐⭐⭐⭐⭐ (標準) ⭐⭐⭐⭐⭐ (條件允許) 最佳實踐建議
- 優先評估能否在業務鍵上建立唯一索引 → 用方案一或六,最簡單可靠。
- 若業務不允許唯一索引 → 引入 Redis 分佈式鎖(方案五),通用性最強。
- 單機部署且重啟可控 → ConcurrentHashMap(方案四)輕量高效。
- 嚴禁使用進程鎖(方案三)應對集群併發,極大隱患。
- 所有方案均應在「插入前」做雙重檢查,防止鎖超時/失效極端情況。
併發控制的核心是將「檢查-插入」原子化,選對工具,事半功倍。