大家好,我是小米,今年 31 歲。寫這篇文章的時候,我正坐在公司工位上,盯着 禪道 上一個“看似簡單”的 Bug 單子發呆。這個 Bug 的標題只有一句話:
“生產環境:訂單重複扣款,概率出現”
如果你是 Java 工程師,看到這句話,後背基本已經開始冒冷汗了。那一刻,我腦子裏閃過的不是 JVM,不是 GC,也不是 SQL,而是一個老朋友——分佈式鎖。也是後來,我才意識到:
這玩意,幾乎是每個 Java 社招面試都會問的,但真正理解透的人,並不多。
今天我想換一種方式,不從 API 講,不從源碼講,而是給你講一個故事。
一個關於 “鎖” 的故事。
從一個“公共廁所”的故事説起
我先問你一個問題。假設你在一個旅遊景點,這裏只有 一個公共廁所,但排隊的人非常多。這個廁所就是一個共享資源。
為了不出事,門口掛了一塊牌子:“使用中,請勿進入”
誰進去,誰把門鎖上,用完再解鎖。在單機世界裏,這個機制非常簡單:
- synchronized
- ReentrantLock
就像你家裏的衞生間,門就在你眼前,你一把就鎖得住。
但現實是:這是一個“分佈式廁所”
問題來了。有一天,這個景點突然火了,遊客暴增,管理人員一拍腦袋:“不行了,一個廁所扛不住,我們多建幾個入口吧。”
於是廁所不止一個門了。有東門、西門、南門、北門,每個門口都有一個排隊的隊伍。而且每個門口都有一個保安,他們之間不認識、也不交流。
這,就是分佈式系統。
- 多台服務器
- 多個 JVM
- 多個實例
- 多個線程
這時候你發現:
單機鎖,已經不管用了。
面試官的問題,通常從這裏開始
我在一次社招面試中,被問到這樣一個問題:“你説説,Redis 怎麼實現分佈式鎖?”
我當時的第一反應是:“setnx + expire。”
面試官點了點頭,又笑了一下:“那你詳細説説。”這一個“詳細”,往往就是分水嶺。
最樸素的 Redis 分佈式鎖長什麼樣?
我們先回到最簡單的版本。核心目標只有一個:
在分佈式環境中,保證同一時間,只有一個線程能拿到鎖。
用 Redis 實現,思路就是:
- 鎖 = Redis 中的一個 key
- 拿鎖 = 搶這個 key
- 解鎖 = 刪除這個 key
我們先寫一個“原始人版本”的鎖。
- setnx lock_key value
含義很簡單:
- 如果 key 不存在,設置成功,返回 1
- 如果 key 已存在,設置失敗,返回 0
於是:
- 返回 1:我搶到鎖了
- 返回 0:鎖被別人佔了
聽起來沒毛病,對吧?但問題很快就來了。
第一個坑:人突然死在廁所裏
假設有一個用户 A:
- A 成功執行了 setnx
- A 拿到鎖
- A 進入臨界區
- 服務器宕機了
結果呢?
- lock_key 還在
- 但 A 永遠不會再執行 del
廁所門被反鎖了,鑰匙還在屍體口袋裏。後面的人排到天荒地老。
於是,我們想到:給鎖加個“自動解鎖”
很快,大家想到:“那我給鎖加個過期時間不就好了?”
於是代碼變成了:
- setnx lock_key value
- expire lock_key 30
邏輯是:
- A 拿到鎖
- 30 秒後自動釋放
聽起來完美。但面試官如果在這一步打斷你,問題就來了。
第二個坑:鎖設置成功了,但過期時間沒來得及設置
你注意到了嗎?這其實是兩條命令:
- setnx
- expire
它們 不是原子操作。如果發生這種情況:
- setnx 成功
- 網絡抖了一下
- expire 沒執行成功
- 服務器又掛了
結果呢?你以為你加了自動解鎖,其實沒有。廁所再次被永久佔用。
真正成熟方案的第一步:一次性完成所有動作
這時候,Redis 提供了一個“組合拳”:
SET lock_key value NX EX 30
它的含義是:
- NX:key 不存在才能設置
- EX:設置過期時間
- 原子操作
這行命令的出現,直接淘汰了前面 80% 的“偽分佈式鎖”。
如果你在面試中説到這裏,面試官大概率會繼續往下追。
value 為什麼不能隨便寫?
我再給你講一個真實事故。有一次,我們項目裏有個新人,解鎖的時候直接寫了:
DEL lock_key
結果,在併發條件下,出了一個非常隱蔽的問題。場景是這樣的:
- 線程 A 拿到鎖,設置過期 30 秒
- A 執行邏輯非常慢
- 30 秒到了,鎖自動過期
- 線程 B 拿到了新鎖
- A 執行完了,調用 del,把 B 的鎖刪了
你看懂了嗎?A 刪的,已經不是自己的鎖了。
於是,鎖必須“認主”
為了解決這個問題,每個鎖必須有一個唯一身份標識。最常見的就是:
- UUID
- 線程 ID + JVM ID
流程就變成了:
- 獲取鎖時:SET lock_key uuid NX EX 30
- 解鎖時:
- 先判斷 value 是否等於自己的 uuid
- 再刪除
這時,面試官一般會點頭,但馬上再問一句:“那你怎麼保證判斷和刪除是原子性的?”
恭喜你,進入最後一關。
最終形態:Lua 腳本的登場
Redis 是單線程模型,並且支持 Lua 腳本原子執行。於是,解鎖邏輯通常寫成一段 Lua:
- 如果 key 存在
- 並且 value 等於當前線程的 uuid
- 才允許刪除
這一步,才是一個“合格的 Redis 分佈式鎖”的完整形態。
説回面試:面試官真正想考什麼?
講了這麼多,其實我想告訴你一件事:
面試官不是想聽你背命令,而是想看你“有沒有踩過坑”。
他們真正關心的通常是:
- 你是否意識到分佈式環境的不確定性
- 你是否考慮過鎖失效、誤刪、併發邊界
- 你是否知道這個東西 為什麼要這樣設計
Redis 分佈式鎖,到底適不適合你?
我最後説一句非常現實的話。Redis 分佈式鎖不是銀彈。
它適合:
- 對性能敏感
- 錯一次問題不大的業務
- 搶券、秒殺、冪等控制
它不適合:
- 強一致性
- 金融級別事務
- 絕對不能出錯的核心鏈路
在這些場景下,你可能需要:
- Zookeeper
- 數據庫行鎖
- 更高級的協調組件
寫在最後
如果你看到這裏,我想你已經發現了:
Redis 分佈式鎖,本質上不是一個 API 問題,而是一個“邊界問題”。
它考驗的不是你會不會寫代碼,而是你能不能提前看到“最壞的那條路”。
就像那個公共廁所的故事一樣:
- 鎖不是掛出來就完事了
- 你要考慮人會不會死在裏面
- 門會不會被別人撬開
- 鑰匙會不會被誤拿
這些,才是面試真正想聽的。
END
我是小米,一個喜歡分享技術的31歲程序員。如果你喜歡我的文章,歡迎關注我的微信公眾號“軟件求生”,獲取更多技術乾貨!
如果你覺得這篇文章對你有幫助,歡迎點個“在看”,我們下篇見~