實現思路
公平鎖:創建有序節點,判斷本節點是不是序號最小的節點(第一個節點),若是,則獲取鎖;若不是,則監聽比該節點小的那個節點的刪除事件。
非公平鎖:直接嘗試在指定path下創建節點,創建成功,則說明該節點搶到鎖了。如果創建失敗,則監聽鎖節點的刪除事件,或者sleep一段時間后再重試。
可重入:使用ThreadLocal記錄加鎖客戶端的唯一標識。重復時先從ThreadLocal獲取,獲取到,這認為加鎖成功,直接返回。
使用瞬時節點創建可重入公平鎖
使用瞬時節點的好處是當Session失效時,該節點將被清理,從而避免使用持久節點加鎖成功后,拋異常或宕機或服務器重啟等原因造成的鎖無法釋放問題。
使用curator實現
// 假設需要加鎖的訂單Id
private static String orderId = "157146671409578219";
// 工程名
private static String appName = "trade_";
// 此次加鎖業務處理邏輯描述
private static String operatorDesc = "updateTrade_";
// 部門,每個部門可以有自己的zk空間目錄
private static String department = "zfpt" ;
// 鎖前綴 應該根據業務 具有唯一性
private static String lockPrefixKey = "/" + appName + operatorDesc ;
/**
* 每個線程 創建自己的Connection ,創建自己的Session
*/
@Test
public void curator() throws Exception {
for (int i = 0 ; i < 100 ; i++) {
Thread currentThread = new Thread(() -> {
// 創建Connection
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("master:2181,slave1:2181,slave2:2181")
.retryPolicy(new RetryOneTime(1000)) //重試策略
.namespace(department) // 可以設置自己部門縮寫
.build();
client.start();
// 模擬對同一個訂單加鎖
InterProcessMutex lock = new InterProcessMutex(client, lockPrefixKey + orderId);
try {
// 一直嘗試加鎖 直到鎖可用。 有點像synchronized
// lock.acquire();
if(lock.acquire(1, TimeUnit.SECONDS)) {
System.out.println(Thread.currentThread().getName() + " 搶到鎖");
} else {
System.out.println(Thread.currentThread().getName() + "超時沒有搶到鎖");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
// 如果當前線程獲得到鎖,則釋放鎖
if(lock.isAcquiredInThisProcess()) {
System.out.println(Thread.currentThread().getName() + " 釋放鎖");
lock.release();
} else {
System.out.println(Thread.currentThread().getName() + " 沒有搶到鎖,故沒有釋放鎖");
}
} catch (Exception e) {
e.printStackTrace();
}
}
});
currentThread.setName("Lock【" + i + "】");
currentThread.start();
}
Thread.sleep(Integer.MAX_VALUE);
}
實現原理:
因為ZK不允許在臨時節點下創建子節點,所以InterProcessMutex
工具類會根據加鎖傳入path的(也就是案例中的lockPrefixKey + orderId)創建一個持久節點;然后在這個持久節點下創建瞬時節點,創建成功后,對該持久節點下的全部子節點進行降序排序,判斷當前節點是否是第一個節點,如果是則獲取鎖,否則對前一個節點加上監聽事件。然后Object.wait()
,當監聽事件被觸發,則會調用notifyAll
方法,對等待線程進行喚醒。再次嘗試獲取鎖。
缺點:
- 傳入的加鎖節點會被創建為永久節點(就是lockPrefixKey + orderId),這樣zk節點數量將會急速遞增。
- 臨時節點不穩定:一個客戶端加鎖成功后,可能會因為網絡抖動等原因導致Session斷開,該客戶端創建的臨時節點被清理,導致另外一節正在監聽的客戶端加鎖成功,同時進行操作。
使用持久節點創建可重入公平鎖
上文提到的臨時節點不穩定,父節點為永久節點無法釋放問題。我的拙見是:
- 使用持久節點代替臨時節點:釋放鎖的時候刪除自己創建的加鎖節點。
- 父節點為永久節點無法釋放:可以在每個客戶端釋放鎖的時候進行判斷,如果自己是最后一個節點,則刪除父節點。
- 但是需要考慮的問題:客戶端加鎖成功后,宕機或重啟或者其他極端異常情況,無法刪除加鎖節點。最后一個加鎖節點同樣異常,也無法刪除父節點。這時,可以給每個鎖加過期時間,過期失效。由於zk沒有提供過期自動清理,所以可以在第二次訪問該節點的時候 先進行判斷,判斷失效先刪除再創建。如果沒有第二次訪問的節點 可以依靠定時任務進行節點清理。