Java秒殺系統(tǒng)(十六):基于ZooKeeper的分布式鎖優(yōu)化秒殺邏輯

作者: 修羅debug
版權(quán)聲明:本文為博主原創(chuàng)文章,遵循 CC 4.0 by-sa 版權(quán)協(xié)議,轉(zhuǎn)載請附上原文出處鏈接和本聲明。




摘要:本篇博文是“Java秒殺系統(tǒng)實戰(zhàn)系列文章”的第十六篇,本文我們將繼續(xù)秒殺系統(tǒng)的優(yōu)化之路,采用統(tǒng)一協(xié)調(diào)調(diào)度中心中間件ZooKeeper控制秒殺系統(tǒng)中高并發(fā)多線程對于共享資源~代碼塊的并發(fā)訪問所出現(xiàn)的并發(fā)安全問題,即用ZooKeeper實現(xiàn)一種分布式鎖!

內(nèi)容:ZooKeeper,看到其名字,不由得聯(lián)想至 Zoo + Keeper,即動物園的看管所!這個寓意用以表達(dá)的是一種統(tǒng)一協(xié)調(diào)管理思想,動物園有很多動物,這些動物就類似于分布式系統(tǒng)架構(gòu)時代所部署的不同系統(tǒng)服務(wù)節(jié)點(diǎn),而這些動物~服務(wù)節(jié)點(diǎn)偶爾可能需要打交道,相互之間可能需要進(jìn)行相應(yīng)的問候,這個時候得需要有一個“看管者”,其職責(zé)除了需要管理動物園里的這些動物的行為之外(即這些系統(tǒng)服務(wù)的行為),還需要統(tǒng)一協(xié)調(diào)管理這些動物之間的“問候”、“打交道”(系統(tǒng)服務(wù)之間的調(diào)用)!

ZooKeeper對外會提供一個多層級的節(jié)點(diǎn)命名空間(節(jié)點(diǎn)稱為ZNode),每個節(jié)點(diǎn)都用一個以斜杠(/)分隔的路徑表示,而且每個節(jié)點(diǎn)都有父節(jié)點(diǎn)(根節(jié)點(diǎn)除外)。ZooKeeper的相關(guān)功能特性在實際使用過程中,其底層可能需要動態(tài)的添加、刪減相應(yīng)的節(jié)點(diǎn),此時zk會提供一個Watcher監(jiān)聽器,用以監(jiān)聽那些動態(tài)新增、刪減的節(jié)點(diǎn),即ZooKeeper會在某些業(yè)務(wù)場景對一些節(jié)點(diǎn)使用上Watcher機(jī)制,監(jiān)聽相應(yīng)的節(jié)點(diǎn)的動態(tài)。


我們即將要在下面介紹的“分布式鎖”功能組件即為ZooKeeper提供給開發(fā)者的一大利器,其底層的實現(xiàn)原理正是基于Watcher機(jī)制 + 動態(tài)創(chuàng)建、刪減臨時順序節(jié)點(diǎn) 所實現(xiàn)的,值得一提的是,一個ZNode節(jié)點(diǎn)將代表一個路徑。

以下為ZooKeeper實現(xiàn)(獲?。┓植际芥i的原理:

(1)當(dāng)前線程在獲取分布式鎖的時候,會在ZNode節(jié)點(diǎn)(ZNode節(jié)點(diǎn)是Zookeeper的指定節(jié)點(diǎn))下創(chuàng)建臨時順序節(jié)點(diǎn),釋放鎖的時候?qū)h除該臨時節(jié)點(diǎn)。

(2)客戶端/服務(wù) 調(diào)用createNode方法在 ZNode節(jié)點(diǎn) 下創(chuàng)建臨時順序節(jié)點(diǎn),然后調(diào)用getChildren(“ZNode”)來獲取ZNode下面的所有子節(jié)點(diǎn),注意此時不用設(shè)置任何Watcher。

(3)客戶端/服務(wù)獲取到所有的子節(jié)點(diǎn)path之后,如果發(fā)現(xiàn)自己創(chuàng)建的子節(jié)點(diǎn)序號最小,那么就認(rèn)為該客戶端獲取到了鎖,即當(dāng)前線程獲取到了分布式鎖。

(4)如果發(fā)現(xiàn)自己創(chuàng)建的節(jié)點(diǎn)并非ZNode所有子節(jié)點(diǎn)中最小的,說明自己還沒有獲取到鎖,此時客戶端需要找到比自己小的那個節(jié)點(diǎn),然后對其調(diào)用exist()方法,同時對其注冊事件監(jiān)聽器。

之后,讓這個被關(guān)注的節(jié)點(diǎn)刪除(核心業(yè)務(wù)邏輯執(zhí)行完,釋放鎖的時候,就是刪除該節(jié)點(diǎn)),則客戶端的Watcher會收到相應(yīng)的通知,此時再次判斷自己創(chuàng)建的節(jié)點(diǎn)是否是ZNode子節(jié)點(diǎn)中序號最小的,如果是則獲取到了鎖,如果不是則重復(fù)以上步驟繼續(xù)獲取到比自己小的一個節(jié)點(diǎn)并注冊監(jiān)聽。

以上為ZooKeeper的基本介紹以及關(guān)于其底層實現(xiàn)分布式鎖的原理的介紹,但是,Debug想說的是“理論再好,如果不會轉(zhuǎn)化為實際的代碼或者輸出,那只能稱之為泛泛而談、吹牛逼” !

下面,我們將基于Spring Boot搭建的秒殺系統(tǒng)整合ZooKeeper,并基于ZooKeeper實現(xiàn)一種分布式鎖,以此解決秒殺系統(tǒng)中高并發(fā)多線程并發(fā)產(chǎn)生的諸多問題。

(1)首先,當(dāng)然是引入ZooKeeper的依賴?yán)?,其中zk的版本在這里我們采用3.4.10,zk客戶端操作實例curator的版本為2.12.0

<!-- zookeeper start -->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.10</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
<exclusion>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>2.12.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>2.12.0</version>
</dependency>

緊接著,是在配置文件application.properties中加入ZooKeeper的配置,包括其服務(wù)所在的Host、端口Port、命名空間等等:  

#zookeeper
zk.host=127.0.0.1:2181
zk.namespace=kill

(2)然后,跟Redis、Redisson一樣,我們需要基于Spring Boot自定義注入ZooKeeper的相關(guān)操作Bean組件,即CuratorFramework實例的自定義配置,如下所示:  

//ZooKeeper組件自定義配置
@Configuration
public class ZooKeeperConfig {

@Autowired
private Environment env;

//自定義注入ZooKeeper客戶端操作實例
@Bean
public CuratorFramework curatorFramework(){
CuratorFramework curatorFramework=CuratorFrameworkFactory.builder()
.connectString(env.getProperty("zk.host"))
.namespace(env.getProperty("zk.namespace"))
//重試策略
.retryPolicy(new RetryNTimes(5,1000))
.build();
curatorFramework.start();
return curatorFramework;
}
}

(3)接著,我們就可以拿來使用了,在KillService秒殺服務(wù)類中,我們創(chuàng)建了一個新的秒殺處理方法killItemV5,表示借助ZooKeeper中間件解決高并發(fā)多線程并發(fā)訪問共享資源~共享代碼塊出現(xiàn)的并發(fā)安全問題!  

@Autowired
private CuratorFramework curatorFramework;
//TODO:路徑就相當(dāng)于一個ZNode
private static final String pathPrefix="/kill/zkLock/";

//商品秒殺核心業(yè)務(wù)邏輯的處理-基于ZooKeeper的分布式鎖
@Override
public Boolean killItemV5(Integer killId, Integer userId) throws Exception {
Boolean result=false;
//定義獲取分布式鎖的操作組件實例
InterProcessMutex mutex=new InterProcessMutex(curatorFramework,pathPrefix+killId+userId+"-lock");
try {
//嘗試獲取分布式鎖
if (mutex.acquire(10L,TimeUnit.SECONDS)){

//TODO:核心業(yè)務(wù)邏輯
if (itemKillSuccessMapper.countByKillUserId(killId,userId) <= 0){
ItemKill itemKill=itemKillMapper.selectByIdV2(killId);
if (itemKill!=null && 1==itemKill.getCanKill() && itemKill.getTotal()>0){
int res=itemKillMapper.updateKillItemV2(killId);
if (res>0){
commonRecordKillSuccessInfo(itemKill,userId);
result=true;
}
}
}else{
throw new Exception("zookeeper-您已經(jīng)搶購過該商品了!");
}
}
}catch (Exception e){
throw new Exception("還沒到搶購日期、已過了搶購時間或已被搶購?fù)戤叄?);
}finally {
//釋放分布式鎖
if (mutex!=null){
mutex.release();
}
}
return result;
}

從上述該源代碼中可以看出其核心的處理邏輯在于“定義操作組件實例”、“獲取鎖”以及“釋放鎖”的實現(xiàn)上,如下所示:  

//定義獲取分布式鎖的操作組件實例
InterProcessMutex mutex=new InterProcessMutex(curatorFramework,pathPrefix+killId+userId+"-lock");

//嘗試獲取分布式鎖
mutex.acquire(10L,TimeUnit.SECONDS)

//釋放鎖
mutex.release();

(4)至此,基于統(tǒng)一協(xié)調(diào)調(diào)度中心中間件ZooKeeper實現(xiàn)的分布式鎖的代碼我們已經(jīng)實戰(zhàn)完畢了,下面我們按照慣例,進(jìn)入壓測環(huán)節(jié),數(shù)據(jù)用例以及壓測的線程組的線程數(shù)我們?nèi)耘f跟以前一樣,total=6本書,用戶Id為10040~10049即10個用戶,線程數(shù)為1w。

點(diǎn)擊JMeter的啟動按鈕,即可發(fā)起秒級并發(fā)1w個線程的請求,稍等片刻(因為ZooKeeper需要不斷的在當(dāng)前設(shè)定的節(jié)點(diǎn)創(chuàng)建、刪除臨時節(jié)點(diǎn),故而耗時還是比較長的),觀察控制臺的輸出以及數(shù)據(jù)庫表item_kill、item_kill_success表最終的數(shù)據(jù)記錄結(jié)果,如下圖所示:


對于這樣的結(jié)果,可謂是皆大歡喜吧!至此,本文關(guān)于ZooKeeper的應(yīng)用實戰(zhàn)我們就介紹到這里了! 

補(bǔ)充:

1、目前,這一秒殺系統(tǒng)的整體構(gòu)建與代碼實戰(zhàn)已經(jīng)全部完成了,該秒殺系統(tǒng)對應(yīng)的視頻教程的鏈接地址為:https://www.fightjava.com/web/index/course/detail/6,可以點(diǎn)擊鏈接進(jìn)行試看以及學(xué)習(xí),實戰(zhàn)期間有任何問題都可以留言或者與Debug聯(lián)系、交流!

2、另外,Debug也開源了該秒殺系統(tǒng)對應(yīng)的完整的源代碼以及數(shù)據(jù)庫,其地址可以來這里下載:https://gitee.com/steadyjack/SpringBoot-SecondKill 記得Fork跟Star啊!!!

3、最后,不要忘記了關(guān)注一下Debug的技術(shù)微信公眾號: