寫在前面的話
不知道你有沒有過這種經歷:在本地開發測試時一切順風順水,邏輯嚴絲合縫。可一旦代碼部署到線上,面對高併發的真實流量,各種匪夷所思的數據異常就開始冒頭了。
我最早遇到的"庫存超賣"就是這樣一個典型案例。從最初相信 Java 自帶的鎖,到後來手寫 Redis 鎖,再到最後折騰出穩定方案,這個過程其實就是對"併發"二字理解不斷加深的過程。
今天想聊聊這塊內容,不堆砌概念,只講講這條路是怎麼一步步走過來的。
一、一切的起點:synchronized 的舒適區
剛開始寫代碼時,思維往往停留在"單機"模式。遇到需要控制併發的地方,直覺反應就是加個 synchronized 關鍵字。
1. 曾經寫過的代碼
// 簡單的庫存扣減
public synchronized void deductStock(String productId) {
// 1. 查詢庫存
Product product = stockMapper.selectById(productId);
// 2. 判斷並扣減
if (product.getStock() > 0) {
product.setStock(product.getStock() - 1);
stockMapper.updateById(product);
}
}
2. 這個方案能用嗎?
能用,但有前提。
如果你的系統是一個簡單的後台管理系統,或者是一個單節點部署的內部工具,併發量極低,那麼 synchronized 完全足夠。它簡單、高效,且無需引入外部依賴,是解決單機併發問題的"如意金箍棒"。
3. 為什麼後來不行了?
問題的關鍵在於”跨進程“。
當業務發展,服務需要部署兩台甚至更多服務器時,每台服務器都有一個獨立的 JVM。
- 服務器 A 的
synchronized鎖住了它自己的線程。 - 服務器 B 的
synchronized鎖住了它自己的線程。 - 結果:A 和 B 同時放行了一個請求,扣減了同一件商品。庫存立刻變負數。
這時候我們意識到:我們需要一把能管得住所有服務器的"大鎖"。
二、初嘗分佈式鎖:Redis SETNX 的嘗試
既然 JVM 內部的鎖不管用了,那自然要找一個所有服務器都能訪問到的第三方組件來存這把鎖。Redis 因為其高性能和簡單的 API,成了首選。
1. 最直觀的寫法
Redis 有個命令叫 SETNX (SET if Not Exists)。這名字聽起來就天生是為了搶佔資源設計的。
# 誰先執行成功,誰就搶到了鎖
SETNX lock:product:101 1
邏輯很簡單:
- 多個服務器同時發
SETNX命令。 - 只有一個能返回
1(成功),其他的返回0(失敗)。 - 搶到鎖的執行業務,做完之後
DEL刪除鎖。
2. 現實中的意外
這個方案最大的隱患在於“刪鎖”這步。
如果代碼在執行業務邏輯時,服務器突然斷電了,或者進程崩潰了,導致 DEL 命令沒來得及發出。
後果:這把鎖就像"幽靈"一樣永遠存在於 Redis 裏。後續所有針對這個商品的請求,都會因為拿不到鎖而被死死卡住。
改進方案:必須加過期時間。
SETNX lock:product:101 1
EXPIRE lock:product:101 10 # 10秒後自動過期
3. 還是不夠完美
SETNX 和 EXPIRE 是兩條命令,不是原子操作。如果在第一句和第二句之間由於網絡抖動或者服務重啓斷開了,鎖依然會變成"死鎖"。
適用場景:
這種簡單的 SETNX 方案,在很早期的 Redis 版本或者一些非核心業務(比如簡單的定時任務去重)中還可以見到,但在對於數據準確性要求極高的交易核心鏈路,它顯然過於脆弱了。
三、進階:原子性與"鎖不住"的尷尬
吸取了死鎖的教訓,後來 Redis 官方推出了原子命令,或者我們通用 Lua 腳本來保證操作原子性。
1. 修復死鎖問題
# 一條命令搞定加鎖和過期時間
SET lock:product:101 uuid NX PX 10000
這就解決了原子性問題。只要鎖加上了,由於有過期時間,哪怕服務器爆炸,鎖最終也會自動消失,系統能自動恢復。
2. 引入了新問題:鎖因為超時提前釋放了
假設我們將鎖的過期時間設為 10秒。
但那天的數據庫特別卡,業務邏輯執行了 15秒。
這就出現了一個嚴重的邏輯漏洞:
- T0秒:線程 A 加鎖成功。
- T10秒:鎖自動過期釋放。
- T11秒:線程 B 進場,發現沒鎖,加鎖成功。
- T15秒:線程 A 終於執行完了,發起
DEL刪除鎖。- 關鍵點:此時 A 刪掉的,其實是 B 的鎖!
這就導致了連鎖崩潰:鎖失效 -> A 刪 B 的鎖 -> B 裸奔 -> B 刪 C 的鎖...
適用場景:
這種方案適用於業務執行時間非常短且穩定的場景。但只要涉及網絡調用(如第三方支付、跨服務調用),執行時間不可控,這種固定過期時間的方案就始終懸着一把劍。
四、最終方案:Redisson 的守候
為了解決"鎖過期時間不好估算"的痛點,Redisson 帶着它的看門狗(WatchDog) 機制出現了。這也許是目前 Java 生態中最成熟的分佈式鎖方案。
1. 什麼是看門狗?
其實原理很樸素:既然我不知道業務要跑多久,那我能不能搞個"助理"在後台盯着?
簡單來説就是:
- 只要業務線程還在跑,"看門狗"會每隔一會兒就去 Redis 喊一聲:"大哥,還沒完呢,給我續個杯!"
- Redis 收到通知,就把過期時間重新填滿。
- 如果業務線程掛了,看門狗也沒了,沒人續杯,鎖自然就過期了。
2. 使用起來的感受
代碼變得異常清爽,彷彿回到了單機鎖的時代:
// 1. 獲取鎖對象
RLock lock = redisson.getLock("lock:product:101");
try {
// 2. 加鎖(開啓看門狗,默認30秒過期,每10秒續期一次)
lock.lock();
// 3. 執行業務(哪怕跑了1分鐘,鎖也不會丟)
complexBusinessLogic();
} finally {
// 4. 釋放鎖(只有當鎖存在,且是當前線程加的鎖時,才釋放)
if (lock.isLocked() && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
3. 穩在哪兒?
Redisson 幫我們把最難處理的幾個點屏蔽了:
- 自動續期:不用糾結
expire設置多少秒合適。 - 防止誤刪:解鎖時會校驗線程 ID,不會刪掉別人的鎖。
- 可重入:和
synchronized一樣,同一個線程可以多次獲取同一把鎖。
適用場景:
幾乎涵蓋了所有需要強一致性的分佈式併發場景。無論是秒殺扣庫存、金融賬户扣款,還是定時任務的分發執行,Redisson 都是目前最穩健的選擇。
五、集羣下的隱憂:Redlock 是救世主嗎?
講到這裏,很多細心的朋友可能會問:
"如果 Redis 是主從集羣(Cluster),主節點掛了,鎖還沒同步到從節點,從節點升級為主,鎖不就丟了嗎?"
這一針見血。
為了解決這個問題,Redis 之父 Antirez 提出了 Redlock 算法:讓客户端向 N 個獨立的 Redis 節點同時申請鎖,只要超過半數(N/2+1)申請成功,就認為獲取了鎖。
1. 為什麼我不推薦 Redlock?
在實際工程落地中,Redlock 的投入產出比(ROI)並不高:
- 部署成本高:你需要至少 3 個(最好 5 個)完全獨立的 Redis 實例,而不是主從集羣。
- 性能折損:客户端要順序去多個節點加鎖,網絡開銷成倍增加。
- 並非絕對安全:分佈式系統的時鐘跳躍(Clock Drift)或者長 GC 依然可能打破 Redlock 的安全性(這也是著名的 Martin Kleppmann 與 Antirez 辯論的焦點)。
2. 更有性價比的選擇
如果你的業務真的無法容忍哪怕百萬分之一的"主從切換丟鎖"風險,我的建議是:
- 方案一:獨立部署
專門部署一個單機版 Redis 實例(不做集羣),只用來存鎖。哪怕它掛了,整個業務熔斷,也好過併發亂了。簡單粗暴,但極其有效。 - 方案二:擁抱強一致性(CP)
如果鎖的一致性比可用性更重要(比如涉及資金轉賬),請轉身擁抱 ZooKeeper 或 Etcd。它們天生就是為 CP(強一致性)設計的,不要勉強 AP(高可用)的 Redis 做它不擅長的事。 - 方案三:更通用的選擇
在 99.9% 的業務場景下,接受 Redis 主從切換可能帶來的極短暫鎖丟失風險。
想一想,主節點宕機的概率是多少?正好在宕機那幾毫秒持有鎖的概率是多少?為了解決這微乎其微的概率,引入複雜的 Redlock,往往得不償失。
六、最後的一點心得
技術方案的演進,本質上是在做取捨。
- Synchronized 勝在簡單,敗在擴展。
- Redis SETNX 勝在性能,敗在極端情況的可靠性。
- Redisson 勝在可靠和完備,但在集羣極端場景下依然有軟肋。
- Zookeeper 勝在強一致,但性能和維護成本是硬傷。
在實際工作中,我們不必言必稱 Redlock,也不必因為一點點極端風險就焦慮。軟件工程沒有銀彈,只有最適合當下的選擇。
很多時候,我們從簡單方案過渡到複雜方案,並不是因為想炫技,而是在無數次"掉坑"之後,對代碼、對線上的敬畏。但同樣,在面對過度設計時,也要有敢於説"不"的底氣:如果單實例夠用,就別搞集羣;如果 Redis 夠用,就別上 Redlock。
願你的代碼,既能跑得快,又能扛得住;願你的架構,既有深度,又有温度。
文章的最後,想和你多聊兩句。
技術之路,常常是熱鬧與孤獨並存。那些深夜的調試、靈光一閃的方案、還有踩坑爬起後的頓悟,如果能有人一起聊聊,該多好。
為此,我建了一個小花園——我的微信公眾號「[努力的小鄭]」。
這裏沒有高深莫測的理論堆砌,只有我對後端開發、系統設計和工程實踐的持續思考與沉澱。它更像我的數字筆記本,記錄着那些值得被記住的解決方案和思維火花。
如果你覺得今天的文章還有一點啓發,或者單純想找一個同行者偶爾聊聊技術、談談思考,那麼,歡迎你來坐坐。
願你前行路上,總有代碼可寫,有夢可追,也有燈火可親。
