以下這段內容是關于 Apache Ignite 的分布式鎖(Distributed Locks) 的介紹。這是一個非常重要的功能,用于在分布式系統中協調多個節點對共享資源的并發訪問。
下面我們來一步步深入理解它。
🎯 一、一句話理解:什么是 Ignite 分布式鎖?
Ignite 分布式鎖是一個跨多個服務器節點的“互斥鎖”,確保同一時間只有一個節點可以操作某個共享數據(比如緩存中的某個 key)。
? 類比:
- 就像一把“全球唯一的鑰匙”:只有拿到這把鑰匙的線程才能修改某個數據。
- 單機環境下用
synchronized
或ReentrantLock
; - 分布式環境下就需要
IgniteCache.lock()
這種跨 JVM 的鎖。
🧩 二、核心概念解析
1?? IgniteCache.lock(key)
—— 獲取一個分布式鎖
Lock lock = cache.lock("keyLock");
- 這個
lock
是一個實現了java.util.concurrent.locks.Lock
接口的對象。 - 它不是本地鎖!它是集群范圍內的分布式鎖。
- 當你在 Node A 上調用
lock.lock()
,Node B 和 Node C 上試圖對同一個 key 加鎖的線程都會阻塞等待,直到 Node A 釋放鎖。
2?? 使用方式:try-finally 確保釋放
lock.lock(); // 阻塞直到獲取鎖
try {// 安全地操作共享資源cache.put("Hello", 11);cache.put("World", 22);
} finally {lock.unlock(); // 必須釋放,否則死鎖!
}
?? 注意:必須放在
finally
塊中釋放,防止異常導致鎖未釋放,造成死鎖或資源饑餓。
3?? lockAll(keys)
—— 批量加鎖
Collection<String> keys = Arrays.asList("key1", "key2", "key3");
Lock lock = cache.lockAll(keys);
lock.lock();
try {// 同時鎖定多個 keycache.put("key1", 1);cache.put("key2", 2);cache.put("key3", 3);
} finally {lock.unlock();
}
- 適用于需要原子性地操作多個 key 的場景。
- 所有 key 的鎖會一起獲取、一起釋放。
- 避免因部分加鎖成功而導致的數據不一致問題。
🔐 三、為什么需要分布式鎖?
在分布式系統中,多個節點可能同時訪問同一份數據。例如:
場景 | 問題 | 解決方案 |
---|---|---|
多個節點同時更新用戶余額 | 超賣、余額錯亂 | 對 userId 加分布式鎖 |
多個節點爭搶執行定時任務 | 重復執行 | 對 "task-refresh" 加鎖 |
緩存雙寫一致性 | 緩存和數據庫不一致 | 更新時對 key 加鎖 |
👉 沒有鎖 → 數據競爭(Race Condition) → 數據錯誤!
?? 四、Atomicity Mode:必須是 TRANSACTIONAL
CacheConfiguration cfg = new CacheConfiguration("myCache");
cfg.setAtomicityMode(CacheAtomicityMode.TRANSACTIONAL); // 必須設置
- Ignite 支持兩種原子性模式:
ATOMIC
:高性能,無事務支持,不能使用顯式鎖。TRANSACTIONAL
:支持事務和顯式分布式鎖。
? 如果你在
ATOMIC
模式下調用cache.lock()
,會拋出異常!
? 所以:要用分布式鎖,緩存必須配置為 TRANSACTIONAL
模式。
🔄 五、Locks vs Transactions:鎖與事務的關系
這是最容易混淆的部分,原文說得很清楚:
“Explicit locks are not transactional and cannot be used from within transactions.”
我們來拆解這句話:
? 情況 1:顯式鎖 ≠ 事務鎖
類型 | 顯式鎖 (cache.lock() ) | 事務中的鎖 |
---|---|---|
是否可嵌套在事務中 | ? 不可以 | ? 可以 |
是否自動提交/回滾 | ? 不支持回滾 | ? 支持 |
如何獲取 | 手動 lock.lock() | 自動由事務管理器獲取 |
使用場景 | 非事務性臨界區 | 事務性數據操作 |
🔴 錯誤寫法(會拋異常):
IgniteTransactions txs = ignite.transactions();
try (Transaction tx = txs.txStart()) {Lock lock = cache.lock("key");lock.lock(); // ? 拋異常!不能在事務中使用顯式鎖cache.put("key", 1);tx.commit();
}
? 情況 2:想要“事務中的顯式鎖”?用 PESSIMISTIC 事務
如果你希望在事務中也能“顯式控制鎖”的行為(比如立即失敗而不是等待),應該使用:
try (Transaction tx = ignite.transactions().txStart(TransactionConcurrency.PESSIMISTIC, // 悲觀并發控制TransactionIsolation.REPEATABLE_READ)) {// 第一次讀/寫就會自動加鎖Integer val = cache.get("key");cache.put("key", val + 1);tx.commit(); // 提交時釋放鎖
}
悲觀事務(PESSIMISTIC)的特點:
- 在
get()
或put()
時立即嘗試獲取分布式鎖。 - 如果鎖被占用,可以選擇超時失敗(避免無限等待)。
- 行為類似于“顯式鎖 + 事務”的組合效果。
🧪 六、完整示例:銀行轉賬(防止并發超支)
IgniteCache<String, Integer> cache = ignite.cache("accounts");// 模擬兩個賬戶
String from = "account-A";
String to = "account-B";// 對兩個賬戶加鎖(避免死鎖:按字母順序加鎖)
List<String> sortedKeys = Arrays.asList(from, to).stream().sorted().collect(Collectors.toList());
Lock lock = cache.lockAll(sortedKeys);lock.lock();
try {Integer balanceA = cache.get(from);Integer balanceB = cache.get(to);if (balanceA >= 100) {cache.put(from, balanceA - 100);cache.put(to, balanceB + 100);System.out.println("轉賬成功");} else {System.out.println("余額不足");}
} finally {lock.unlock(); // 釋放所有鎖
}
? 保證了即使多個節點同時發起轉賬,也不會出現“超賣”。
?? 七、注意事項 & 最佳實踐
項目 | 建議 |
---|---|
🔒 鎖粒度 | 盡量小(比如按用戶 ID 鎖),避免鎖整個緩存 |
?? 鎖持有時間 | 越短越好,不要在鎖內做耗時操作(如網絡請求) |
💥 異常處理 | 一定要 finally unlock() ,建議用 try-with-resources(如果自定義封裝) |
🪢 死鎖風險 | 多 key 加鎖時,按固定順序加鎖(如排序) |
📈 性能影響 | 分布式鎖涉及網絡通信,頻繁使用會影響性能 |
🔄 替代方案 | 考慮使用 EntryProcessor (invoke() )進行原子更新,避免手動加鎖 |
? 總結:一句話掌握精髓
Ignite 的
cache.lock(key)
提供了一種簡單、直觀的跨節點互斥機制,讓你像使用本地ReentrantLock
一樣保護分布式共享資源,但前提是緩存必須是TRANSACTIONAL
模式,并且不能與事務混用。
🔄 對比總結表
功能 | cache.lock() 顯式鎖 | 悲觀事務(PESSIMISTIC) | EntryProcessor (invoke) |
---|---|---|---|
是否跨節點 | ? 是 | ? 是 | ? 是 |
是否支持事務 | ? 否 | ? 是 | ? 是(單 key) |
是否自動加鎖 | ? 手動 | ? 自動 | ? 自動 |
適用場景 | 非事務臨界區 | 多 key 事務操作 | 單 key 原子更新 |
性能 | 中等 | 中等 | 高(推薦) |
如果你想實現高并發下的安全更新,優先考慮 EntryProcessor
;如果邏輯復雜必須加鎖,再用 lock()
或 悲觀事務。
如有具體業務場景(如庫存扣減、計數器、任務調度),歡迎繼續提問,我可以給出更具體的代碼建議!