首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫

高併發下 MySQL 插入檢查方案演進:從資料庫鎖到應用層分佈式鎖的最佳實踐

     編輯:MySQL 資料庫相關知識

業務場景:車輛 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 存在多條記錄(歷史記錄),唯一索引衝突

方案二:資料庫事務 + SELECT ... FOR UPDATE(失敗)

@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 僅鎖定**已存在**的行,「不存在」無法加鎖,併發插入仍會衝突(幻讀)

方案三:JVM 內置鎖 / ReentrantLock(可行但放棄)

// 方案 3a: synchronized
synchronized(this) { checkAndInsert(vin); }

// 方案 3b: ReentrantLock
private static final ReentrantLock lock = new ReentrantLock();
lock.lock();
try { checkAndInsert(vin); } finally { lock.unlock(); }

結果:功能正確,但鎖粒度過粗,全鎖導致吞吐量急劇下降,高併發場景不可接受

方案四:ConcurrentHashMap 細粒度鎖(最終方案)

private static final ConcurrentHashMap VIN_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();
    }
}

關鍵避坑指南

  1. 雙重檢查不可省:應用層鎖只能防併發請求,無法防止異常重啟導致鎖丟失,資料庫層唯一約束/狀態檢查是最後防線
  2. 鎖 Key 設計:包含業務標識(如 vin:lock:),避免誤釋放
  3. 超時設置:鎖超時 > 業務最長執行時間 + 網絡抖動緩衝
  4. 監控指標:鎖等待時間、鎖衝突率、業務拒絕率

方案四的已知限制與修正

原文提到:「更新接口會清空 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,因為更新不涉及「新增檢查」邏輯
}
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved