常見問題:
使用zk實現分佈式鎖,,如果主節點宕機:會發生選舉的過程,爲了數據一致性的問題,在zk選舉的過程中整個zk環境無法使用,客戶端獲取鎖的時候,會不斷的重試,重試多次會直接拋出異常。
客戶端已經在監聽的情況下:
zk客戶端事件監聽到當前的leader節點已經宕機,會自動實現重試。
該過程可能在剩餘的節點中選出一個新的leader節點,從新監聽新的leade節點,,如果一直沒有選出leader節點,爲了避免客戶端死鎖的問題,從新獲取鎖的狀態。
使用zk實現分佈式鎖,死鎖的問題:
curator概念:
Curator是Netflix公司開源的一套zookeeper客戶端框架,解決了很多Zookeeper客戶端非常底層的細節開發工作,包括連接重連、反覆註冊Watcher和NodeExistsException異常等等。Patrixck Hunt(Zookeeper)以一句“Guava is to Java that Curator to Zookeeper”給Curator予高度評價。
Curator官網
其中Curator提供分佈式鎖實現方案,解決羊羣效應問題。
核心API:
InterProcessMutex:分佈式可重入排它鎖
InterProcessSemaphoreMutex:分佈式排它鎖
InterProcessReadWriteLock:分佈式讀寫鎖
InterProcessMultiLock:將多個鎖作爲單個實體管理的容器
Curator環境搭建
Maven依賴
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.2.0</version>
</dependency>
核心api配置
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.curator.utils.CloseableUtils;
import org.springframework.stereotype.Component;
/**
* @ClassName CuratorZkLock zk框架實現分佈式鎖
* @Author 螞蟻課堂餘勝軍 QQ644064779 www.mayikt.com
* @Version V1.0
**/
@Component
public class CuratorZkLock {
// ZooKeeper 服務地址, 單機格式爲:(127.0.0.1:2181),
// 集羣格式爲:(127.0.0.1:2181)
private String connectString;
// Curator 客戶端重試策略
private RetryPolicy retry;
// Curator 客戶端對象
private CuratorFramework client;
public CuratorZkLock() throws Exception {
init();
}
public void init() throws Exception {
// 設置 ZooKeeper 服務地址爲本機的 2181 端口
connectString = "127.0.0.1:2181";
// 重試策略
// 初始休眠時間爲 1000ms, 最大重試次數爲 3
retry = new ExponentialBackoffRetry(1000, 3);
// 創建一個客戶端, 60000(ms)爲 session 超時時間, 15000(ms)爲連接超時時間
client = CuratorFrameworkFactory.newClient(connectString, 60000, 15000, retry);
client.start();
}
public void close() {
CloseableUtils.closeQuietly(client);
}
public CuratorFramework getClient() {
return client;
}
}
簡單用法
/**
秒殺lock
-
@return
*/
@RequestMapping("/seckilLock")
public String seckilLock() throws Exception {
InterProcessMutex interProcessMutex = null;
try {
CuratorFramework client = curatorZkLock.getClient();
interProcessMutex = new InterProcessMutex(client, lockPath);
interProcessMutex.acquire();
log.info("<獲取鎖成功....>");
log.info("<執行秒殺扣庫存業務邏輯....>");
CommodityDetails commodityDetails = commodityDetailsMapper.getCommodityDetails(1L);
Long stock = commodityDetails.getStock();
if (stock > 0) {
log.info("<扣庫存成功>");
commodityDetailsMapper.reduceInventory(1l);
return "ok";
}
// 扣庫存失敗
log.info("<扣庫存失敗>");
return "fail";} catch (Exception e) {
e.printStackTrace();
log.info("<秒殺出現錯誤:e:{}>", e);
return "fail";
} finally {
if (interProcessMutex != null)
interProcessMutex.release();
}
}
Curator源碼解讀
- 從緩存中查找是否已經創建分佈式鎖,如果已經創建了分佈式鎖
則直接複用(具有可重入性) - 如果緩存中沒有,則創建一個分佈式鎖
- 實現原理:創建一個臨時節點,獲取當前父節點下子節點,如果是爲最小的節點,則表示獲取鎖成功,否則獲取鎖失敗,阻塞等待,則監聽上一個節點。
- 當上一個節點如果釋放鎖之後,直接進入到獲取鎖的狀態。
喚醒使用wait notify技術
獲取到當前的線程
Thread currentThread = Thread.currentThread();
從集合中查詢當前線程是否有獲取過分佈式鎖;
LockData lockData = threadData.get(currentThread);
if ( lockData != null )
{
// re-entering
如果有獲取過分佈式鎖的情況下,則重入次數就會+1
lockData.lockCount.incrementAndGet();
return true;
}
在集合中沒有查詢到當前線程有獲取過鎖,如果沒有獲取鎖的情況下,則創建一個臨時順序編號節點
String lockPath = internals.attemptLock(time, unit, getLockNodeBytes());
if ( lockPath != null )
{
在將該臨時順序編號節點存放到集合緩存。
Key爲當前線程 value 分佈式鎖
LockData newLockData = new LockData(currentThread, lockPath);
threadData.put(currentThread, newLockData);
return true;
}
創建一個臨時順序編號節點
ourPath = driver.createsTheLock(client, path, localLockNodeBytes);
判斷該臨時順序編號節點是否屬於最小的;如果屬於最小的節點
就表示獲取鎖成功;
hasTheLock = internalLockLoop(startMillis, millisToWait, ourPath);
獲取當前節點對應 index下標位置;
int ourIndex = children.indexOf(sequenceNodeName);
validateOurIndex(sequenceNodeName, ourIndex);
0 maxLeases=1;
getsTheLock =true
getsTheLock =true 表示獲取鎖成功
boolean getsTheLock = ourIndex < maxLeases;
如果getsTheLock =false的情況下,則訂閱我們上一個節點
String pathToWatch = getsTheLock ? null : children.get(ourIndex - maxLeases);