Skip to main content

Command Palette

Search for a command to run...

從 Martin Kleppmann 的批判到 Redisson 實戰:徹底搞懂分散式鎖與「時間」的陷阱

Published
4 min read

導言:你也許並不擁有「現在」

在面試中,當被問到「如何實現分散式鎖?」時,90% 的候選人會自信地回答:「用 Redis 的 SETNX 或者 Redisson。」

但如果面試官追問:「如果你的 JVM 發生了 Full GC,導致鎖過期了,但你的程式還在執行,會發生什麼?」這時候,才是區分「碼農」與「工程師」的關鍵時刻。

今天,我不只是教你怎麼用 API,而是要帶你穿越 Martin Kleppmann 與 Redis 作者 Antirez 的那場世紀論戰,從哲學的高度看工程的實踐,手把手帶你寫出能防禦「時鐘漂移」與「GC 暫停」的防禦性代碼。


第一章:時間的幻象與 GC 的致命暫停

在分散式系統中,我們最大的敵人不是網路斷線,而是**「時間」的不確定性**。

1.1 致命的場景:GC 導致的腦裂

想像一下,你的服務拿到了鎖,準備修改訂單。突然,JVM 觸發了 Stop-the-World (STW) 的垃圾回收。

  • 你的服務:以為時間只過了 1ms,實際上世界已經過了 30 秒。

  • Redis:鎖過期了,把鎖給了別的請求。

  • 結果:兩個執行緒同時操作同一筆數據,數據損壞(Data Corruption)。

讓我們用這張 Mermaid 時序圖 來還原這個災難現場:

程式碼片段

1.2 Kleppmann 的批判

Martin Kleppmann(《Designing Data-Intensive Applications》作者)指出:RedLock 依賴於「系統時鐘」的同步,這在物理上是不可靠的。

如果你的業務只追求效率(如重複發送郵件),RedLock 沒問題;但如果涉及正確性(如金流、庫存),這種依賴時間的鎖是危險的。


第二章:Fencing Tokens — 守護數據的絕對防線

既然我們無法阻止鎖過期,也無法阻止 GC,那我們該如何保護數據?

答案是:讓儲存層(資料庫)變聰明。我們引入 Fencing Token(柵欄權杖) 1

2.1 機制原理

  1. 鎖服務:每次授予鎖時,生成一個單調遞增的數字(Token)。

  2. 客戶端:拿著這個 Token 去資料庫更新。

  3. 資料庫:檢查這次的 Token 是否比上次處理的大。如果小於等於上次的,直接拒絕。

這就像給資料庫裝了一個「單向棘輪」,時間只能往前走,不能回頭。

程式碼片段


第三章:Redisson 實戰 — 你可能用錯了鎖

很多資深工程師以為用了 Redisson 就萬事大吉,但魔鬼藏在細節裡。

3.1 RedissonMultiLock 不是 RedLock

  • 誤區:很多人以為 RedissonMultiLock 就是 RedLock 算法。

  • 真相:它只是一個容器,把多個鎖打包而已。它不具備 Fencing Token 機制。

3.2 正確的工具:RFencedLock

Redisson 聽到了社群的聲音,引入了 RFencedLock。它會在鎖獲取成功時,返回一個 Token。

面試必考點:

為什麼要用 RFencedLock?

答:因為普通的 RLock 無法區分「鎖過期」和「GC暫停」,而 RFencedLock 提供了單調遞增的 Token,配合 DB 的 CAS 機制可以保證強一致性。


第四章:手把手寫代碼 — Spring Boot + 函數式編程

光說不練假把式。我們要寫出架構師級別的代碼:優雅、防禦性強、易於測試

我們將使用「函數式編程 (Functional Programming)」風格,把複雜的鎖邏輯封裝起來,讓業務代碼保持乾淨。

4.1 定義鎖管理器 (Lock Manager)

這個組件負責處理鎖的獲取、Token 的傳遞、異常處理和鎖的釋放。

Java

import org.redisson.api.RFencedLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

@Component
public class DistributedLockManager {

    private final RedissonClient redisson;

    public DistributedLockManager(RedissonClient redisson) {
        this.redisson = redisson;
    }

    // 定義一個函數介面,強制業務邏輯必須接收 Token
    @FunctionalInterface
    public interface FencedOperation<R> {
        R apply(Long token) throws Exception;
    }

    /**
     * 架構師的封裝:自動處理鎖的獲取、釋放與 Token 傳遞
     */
    public <R> R executeFenced(String lockKey, FencedOperation<R> operation) {
        // 使用 RFencedLock,這是關鍵!
        RFencedLock lock = redisson.getFencedLock(lockKey);
        Long token = null;

        try {
            // 嘗試獲取鎖,等待 5s,鎖 10s 後自動過期
            // 注意:這裡返回了 Token
            token = lock.tryLockAndGetToken(5, 10, TimeUnit.SECONDS);

            if (token == null) {
                throw new IllegalStateException("無法獲取鎖,請稍後再試: " + lockKey);
            }

            // 執行業務邏輯,並將 Token 傳遞進去
            return operation.apply(token);

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("鎖獲取被中斷", e);
        } catch (Exception e) {
            throw new RuntimeException("業務邏輯執行失敗", e);
        } finally {
            // 防禦性編程:只有當前執行緒持有鎖時才釋放
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

4.2 業務層 (Service) 與 資料庫 (Repository)

在 Service 層,我們看不到任何 lock.lock()unlock() 的樣板代碼,只有純粹的業務邏輯。

Service 層:

Java

@Service
public class InventoryService {

    // ... 注入 lockManager ...

    public void decreaseStock(String productId, int quantity) {
        String lockKey = "lock:product:" + productId;

        // 使用 Lambda 表達式,優雅地執行鎖內邏輯
        lockManager.executeFenced(lockKey, (token) -> {

            // 1. 查詢商品
            InventoryItem item = repo.findById(productId);

            // 2. 庫存檢查
            if (item.getStock() < quantity) throw new OutOfStockException();

            // 3. 【核心】帶柵欄的更新
            // 將 token 傳到底層,讓資料庫做最後的守門員
            int rows = repo.updateStockWithFence(productId, quantity, token);

            if (rows == 0) {
                // 這意味著我們是殭屍進程!資料庫拒絕了我們
                throw new ConcurrentModificationException("寫入被拒絕:檢測到過期的鎖持有者");
            }
            return null;
        });
    }
}

SQL (MyBatis/JPA):

SQL

UPDATE inventory 
SET 
    stock = stock - :quantity,
    last_lock_token = :token    -- 更新 Token
WHERE 
    product_id = :productId 
    AND last_lock_token < :token; -- 【柵欄檢查】只有 Token 變大才允許寫入

結論:不確定性中的確定性

拿到 OFFER 的關鍵,不在於背誦 RedLock 的步驟,而在於理解為什麼我們要這麼做。

架構師的決策矩陣 (Takeaway)

你的需求

推薦方案

原因

效率優先 (如發送通知)

普通 RLock

允許偶爾失敗,性能最好。

正確性優先 (如扣庫存)

RFencedLock + DB Check

必須防止 GC 造成的腦裂。

極致強一致性

ZooKeeper / etcd

CP 系統,天生比 Redis (AP) 更適合做鎖。

給讀者的最後建議:

最好的鎖,是沒有鎖。如果能設計成冪等 (Idempotent) 的操作(例如資料庫的唯一鍵約束、樂觀鎖版本號),往往比引入複雜的分散式鎖更可靠。


(本文內容基於 Martin Kleppmann 的理論與 Redisson 官方文檔整理)這是一份為你精心整理的技術網誌,採用資深架構師的視角,結合Mermaid 圖表實戰代碼,旨在幫助讀者深入理解分散式鎖的本質,並在面試中脫穎而出。

More from this blog

G1垃圾回收器深度解析:從底層原理到zgc的演進之路

引言:G1 GC 的時代意義 在 Java 記憶體管理的演進歷程中,G1 (Garbage-First) 垃圾回收器的出現無疑是一個重要的里程碑。它專為應對現代應用程式中常見的大堆積(Large Heap)記憶體與多核心處理器的場景而設計,其核心目標是在延遲(Latency)與吞吐量(Throughput)這兩個經常相互衝突的效能指標之間,取得一個卓越的平衡點。自 JDK 9 起,G1 已成為預設的垃圾回收器,足見其在通用場景下的高效與穩定性。 本文將作為一份深度解析報告,從 G1 的底層運作原...

Jan 5, 20262 min read

Zgc:深入解析底層運作原理

1.0 ZGC 簡介:為低延遲而生的垃圾收集器 ZGC (Z Garbage Collector) 是 Java 生態系統中相對較新的垃圾收集器,其設計初衷是為了解決現代應用程式對超低延遲(ultra-low latency)和高可擴展性(high scalability)的嚴苛要求。傳統的垃圾收集器帶來了一個痛苦的權衡:隨著堆積大小的增長,不可預測的「Stop-The-World」(STW) 暫停時間也隨之增加——這對於現代延遲敏感型服務是不可接受的特性。ZGC 的誕生,正是為了從根本上打破這...

Jan 5, 20262 min read

Java 垃圾回收器演進深度解析:從 Parallel GC 到 ZGC

引言:Java 記憶體管理的演進之路 在 Java 虛擬機(JVM)內部,垃圾回收(Garbage Collection, GC)並非僅僅一項功能,而是驅動引擎心跳的核心機制,它決定了應用程式吞吐量(Throughput)與互動延遲(Latency)之間最根本的權衡。前者關乎單位時間內的總工作量,後者則聚焦於單次操作的回應速度。Java GC 的演進史,正是一部為應對不斷變革的硬體能力(從單核到眾核,從 GB 到 TB 級記憶體)與應用程式需求(從離線批次處理到超低延遲的即時服務)而持續創新的技...

Jan 5, 20262 min read

深度解析:jvm 性能優化指南

1.0 性能優化的哲學與診斷方法論 在深入探討具體的代碼或 JVM 層級的優化技術之前,我們必須先建立一套嚴謹、可重複的診斷方法論。這是性能工程的基石。現代 Java 的性能表現,是其程式碼、JVM 行為與底層硬體(多核心 CPU、深層記憶體階層)交互作用的複雜結果。盲目地修改代碼或調整參數不僅徒勞無功,甚至可能引入新的問題。因此,策略性地確認性能指標、精準定位瓶頸,並基於經驗證據做出決策,是專業架構師與初學者之間最顯著的區別。本章節將闡述如何建立這樣一套方法論,確保每一次優化都有的放矢。 1....

Jan 5, 20263 min read

Ron’s Cabin

21 posts