業務場景:車輛 VIN 碼入庫前需檢查狀態——不存在或狀態=1(已完成)才允許插入狀態=0(處理中),否則駁回。單線程無問題,高併發下「查詢-插入」非原子導致重複入庫。本文記錄四輪方案演進,最終落地應用層 ConcurrentHashMap 輕量級鎖方案,並分析各方案優劣與適用邊界。
-- 期望原子操作 IF NOT EXISTS (SELECT 1 FROM vin_table WHERE vin = ? AND status = 0) THEN INSERT INTO vin_table (vin, status) VALUES (?, 0); ELSE REJECT; END IF;
ALTER TABLE vin_table ADD UNIQUE INDEX uk_vin_status (vin, status);
阻礙:業務允許同一 VIN 存在多條記錄(歷史記錄),唯一索引衝突
@Transactional
public void insertVin(String vin) {
// 鎖定相關行
List list = jdbc.query("SELECT * FROM vin_table WHERE vin = ? FOR UPDATE", vin);
if (list.isEmpty() || list.get(0).getStatus() == 1) {
jdbc.update("INSERT INTO vin_table (vin, status) VALUES (?, 0)", vin);
} else {
throw new RejectException("處理中");
}
}
阻礙:FOR UPDATE 僅鎖定**已存在**的行,「不存在」無法加鎖,併發插入仍會衝突(幻讀)
// 方案 3a: synchronized
synchronized(this) { checkAndInsert(vin); }
// 方案 3b: ReentrantLock
private static final ReentrantLock lock = new ReentrantLock();
lock.lock();
try { checkAndInsert(vin); } finally { lock.unlock(); }
結果:功能正確,但鎖粒度過粗,全鎖導致吞吐量急劇下降,高併發場景不可接受
private static final ConcurrentHashMapVIN_LOCK_MAP = new ConcurrentHashMap<>(); public void insertVin(String vin) { // 嘗試放入鎖標記,失敗表示有併發請求正在處理 String prev = VIN_LOCK_MAP.putIfAbsent(vin, "LOCKED"); if (prev != null) { throw new RejectException("VIN 正在處理中,請稍後重試"); } try { // 雙重檢查(資料庫層兜底) if (existsProcessingVin(vin)) { throw new RejectException("資料庫已存在處理中記錄"); } insertVinToDb(vin); } finally { VIN_LOCK_MAP.remove(vin); // 釋放鎖 } }
| 方案 | 一致性 | 效能 | 擴展性 | 複雜度 | 適用場景 |
|---|---|---|---|---|---|
| DB 唯一索引 | 強 | 高 | 高 | 低 | 業務天然唯一鍵 |
| DB 事務+FOR UPDATE | 強 | 中 | 中 | 中 | 無幻讀場景 |
| JVM 重量級鎖 | 強(單機) | 低 | 無(單機) | 低 | 低併發單體 |
| ConcurrentHashMap | 最終一致 | 高 | 中(單機) | 低 | 高併發單體/少量實例 |
| 分佈式鎖 | 強 | 中 | 高 | 高 | 微服務叢集 |
當服務擴容為多實例部署,需引入 Redis/Redisson 分佈式鎖:
@Autowired private RedissonClient redisson;
public void insertVin(String vin) {
RLock lock = redisson.getLock("vin:lock:" + vin);
boolean acquired = lock.tryLock(5, 30, TimeUnit.SECONDS);
if (!acquired) throw new RejectException("獲取鎖超時");
try {
if (existsProcessingVin(vin)) throw new RejectException("已存在");
insertVinToDb(vin);
} finally {
if (lock.isHeldByCurrentThread()) lock.unlock();
}
}
vin:lock:),避免誤釋放原文提到:「更新接口會清空 vinMap,導致庫中數據混亂」
修正:更新接口**不應清空全局 Map**,而應僅移除對應 VIN 的鎖標記,或根本不操作 Map(更新屬於狀態流轉,非新增檢查)。正確邏輯:
// 更新接口正確邏輯
public void updateVinStatus(String vin, int newStatus) {
jdbc.update("UPDATE vin_table SET status = ? WHERE vin = ? AND status = 0", newStatus, vin);
// 不操作 VIN_LOCK_MAP,因為更新不涉及「新增檢查」邏輯
}