大家好,我是小米,今年 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:

  1. A 成功執行了 setnx
  2. A 拿到鎖
  3. A 進入臨界區
  4. 服務器宕機了

結果呢?

  • lock_key 還在
  • 但 A 永遠不會再執行 del

廁所門被反鎖了,鑰匙還在屍體口袋裏。後面的人排到天荒地老。

於是,我們想到:給鎖加個“自動解鎖”

很快,大家想到:“那我給鎖加個過期時間不就好了?”

於是代碼變成了:

  1. setnx lock_key value
  2. expire lock_key 30

邏輯是:

  • A 拿到鎖
  • 30 秒後自動釋放

聽起來完美。但面試官如果在這一步打斷你,問題就來了。

第二個坑:鎖設置成功了,但過期時間沒來得及設置

你注意到了嗎?這其實是兩條命令

  1. setnx
  2. expire

它們 不是原子操作。如果發生這種情況:

  • setnx 成功
  • 網絡抖了一下
  • expire 沒執行成功
  • 服務器又掛了

結果呢?你以為你加了自動解鎖,其實沒有。廁所再次被永久佔用。

真正成熟方案的第一步:一次性完成所有動作

這時候,Redis 提供了一個“組合拳”:

SET lock_key value NX EX 30

它的含義是:

  • NX:key 不存在才能設置
  • EX:設置過期時間
  • 原子操作

這行命令的出現,直接淘汰了前面 80% 的“偽分佈式鎖”。

如果你在面試中説到這裏,面試官大概率會繼續往下追。

value 為什麼不能隨便寫?

我再給你講一個真實事故。有一次,我們項目裏有個新人,解鎖的時候直接寫了:

DEL lock_key

結果,在併發條件下,出了一個非常隱蔽的問題。場景是這樣的:

  1. 線程 A 拿到鎖,設置過期 30 秒
  2. A 執行邏輯非常慢
  3. 30 秒到了,鎖自動過期
  4. 線程 B 拿到了新鎖
  5. 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 分佈式鎖,到底適不適合你?

我最後説一句非常現實的話。Redis 分佈式鎖不是銀彈。

它適合:

  • 對性能敏感
  • 錯一次問題不大的業務
  • 搶券、秒殺、冪等控制

它不適合:

  • 強一致性
  • 金融級別事務
  • 絕對不能出錯的核心鏈路

在這些場景下,你可能需要:

  • Zookeeper
  • 數據庫行鎖
  • 更高級的協調組件

寫在最後

如果你看到這裏,我想你已經發現了:

Redis 分佈式鎖,本質上不是一個 API 問題,而是一個“邊界問題”。

它考驗的不是你會不會寫代碼,而是你能不能提前看到“最壞的那條路”。

就像那個公共廁所的故事一樣:

  • 鎖不是掛出來就完事了
  • 你要考慮人會不會死在裏面
  • 門會不會被別人撬開
  • 鑰匙會不會被誤拿

這些,才是面試真正想聽的。

END

我是小米,一個喜歡分享技術的31歲程序員。如果你喜歡我的文章,歡迎關注我的微信公眾號“軟件求生”,獲取更多技術乾貨!

如果你覺得這篇文章對你有幫助,歡迎點個“在看”,我們下篇見~