分散式鎖:從 SETNX 到 Redlock,與那場著名的爭議
· tech
📑 目錄
多個行程、多台機器要搶同一個資源(同一時間只准一個人扣庫存、跑一個排程),就需要一把分散式鎖。Redis 因為快又原子,常被拿來當這把鎖。但這是個「看起來三行就能寫完、其實坑深到見底」的題目——一路踩下去,還會撞上一場分散式系統界著名的學術爭議。這篇從最天真的寫法,一步步補到 Redlock,再誠實面對那個「Redis 鎖到底安不安全」的問題。
一把正確的單機鎖:SET NX PX + Lua 釋放
先看最常見、單一 Redis 節點上「大致正確」的寫法。它的每個零件,都在補一個天真寫法會踩的洞:
落成命令就這兩段:
# 取得:NX(互斥)+ PX(TTL)一次完成,token 用 UUID 這種隨機值
SET lock:order:42 3f9a...e1 NX PX 30000
-- 釋放:GET 比對 token,相符才 DEL(整段用 EVAL 跑,保證原子)
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
早年還有人用 SETNX + 另一句 EXPIRE 兩步設鎖——千萬別:兩句之間若當掉,鎖就沒了 TTL、變成永久死鎖。SET ... NX PX 把「設鎖」和「設過期」合成一個原子命令,正是為了堵這個洞。
但這樣就安全了嗎?TTL 鎖的致命假設
到這裡看起來很完整了。但 Martin Kleppmann(《DDIA》作者)提出一個殺傷力極大的質疑:只要有 TTL,鎖就無法保證真正的互斥。 問題出在一個誰都躲不掉的東西——行程暫停(GC 的 stop-the-world、OS 排程、甚至機器被 suspend):
這個 fencing token 的洞見很重要:鎖只能「盡量」互斥,真正的正確性得靠資源端拒絕過期的寫入。 而 fencing token 要單調遞增,本身就需要一個可靠的計數器——那又回到了真共識的地盤。
Redlock,與那場爭議
antirez(Redis 作者)為了讓 Redis 鎖更可靠,提出了 Redlock:別只靠一台 Redis,而是準備 N 台獨立的 master(通常 5 台),取鎖時去跟過半數(≥3 台)在限定時間內都搶到,才算成功;釋放時對全部節點解鎖。用「過半」來抵抗少數節點掛掉——精神上跟 Cluster 的過半故障轉移、共識演算法是同一路。
然後就吵起來了。Kleppmann 批評:Redlock 依賴「各節點時鐘走得夠準、行程不會長暫停」這類時序假設,而這些假設在真實系統裡不成立(GC pause、時鐘飄移),所以 Redlock 給人一種它其實給不了的安全感;要正確性,就得用 fencing token + 真共識系統。antirez 反駁:Kleppmann 的攻擊模型太嚴苛,多數實務場景 Redlock 已足夠,且 fencing token 那套一樣有它的假設。這場辯論沒有「誰贏」,但它逼出了一個至今最實用的分野。
那個分野:你要的是「效率鎖」還是「正確性鎖」
- 效率鎖(efficiency):鎖只是為了避免重複做白工——同一個快取重算兩次、同一封信寄兩遍,偶爾破防只是浪費一點資源,不會出大事。這種鎖,單機 Redis 的
SET NX PX綽綽有餘,連 Redlock 都未必需要。 - 正確性鎖(correctness):絕對不能有兩個人同時做——扣款、開發票、轉帳。這種場景下,TTL 鎖的時序假設不夠格,你需要 fencing token + 真共識(ZooKeeper、etcd),或乾脆把互斥交給資料庫的交易與唯一約束。
反思
「看起來三行就能寫完」的東西,往往最深
分散式鎖是我心中「魔鬼藏在細節」的頭號教材。從 SETNX 一路補到 Redlock,每一步都在堵一個你一開始根本沒想到的洞:忘了 TTL 會死鎖、忘了驗 token 會誤刪、忘了 GC pause 會破互斥……每個洞單獨看都「顯而易見」,但沒踩過就是想不到。這給我的長期習慣是:遇到「這不是很簡單嗎」的念頭時,反而要更警覺——越是被當成三行小事的東西,越可能在你沒看的角落埋著坑。真正的資深,不是會寫那三行,而是知道那三行「還漏了什麼」。
效率鎖 vs 正確性鎖,是我做技術決策最常用的一把尺
Kleppmann 那個分野,價值遠超出鎖本身。它教我在用任何「盡力而為」的機制前,先問一句:萬一它偶爾失效,是『浪費一點』還是『出人命』? 是前者(效率),就大方接受簡單方案的偶爾破防,別過度工程;是後者(正確性),就別自欺欺人地用時序假設換安全感,老實上真共識或資料庫交易。這條線讓我不再糾結「Redis 鎖到底安不安全」這種沒有絕對答案的問題,而是先定位「我這把鎖屬於哪一類」——問題分類對了,工具的選擇就不再兩難。
那場爭議教我的:工程沒有銀彈,只有假設
antirez 和 Kleppmann 誰對?我的答案是:他們吵的根本不是對錯,是假設。Kleppmann 假設一個會 GC pause、時鐘會飄的殘酷世界;antirez 假設一個大致正常的實務環境。假設不同,結論自然不同——而兩個假設在各自的場景裡都成立。這件事徹底改變了我看技術爭論的方式:與其問「這個方案好不好」,不如問「它建立在什麼假設上、那些假設在我的場景成不成立」。任何號稱安全、可靠、高效的方案,底下都躺著一組假設;把那組假設攤開來看,比聽誰的結論有用一百倍。這是分散式鎖這個小題目,教給我最大的一課。