Java秒殺系統實戰系列~基於ZooKeeper的分佈式鎖優化秒殺邏輯

摘要:

本篇博文是「Java秒殺系統實戰系列文章」的第十七篇,本文咱們將繼續秒殺系統的優化之路,採用統一協調調度中心中間件ZooKeeper控制秒殺系統中高併發多線程對於共享資源~代碼塊的併發訪問所出現的併發安全問題,即用ZooKeeper實現一種分佈式鎖!

內容:

ZooKeeper,看到其名字,不禁得聯想至 Zoo + Keeper,即動物園的看管所!這個寓意用以表達的是一種統一協調管理思想,動物園有不少動物,這些動物就相似於分佈式系統架構時代所部署的不一樣系統服務節點,而這些動物~服務節點偶爾可能須要打交道,相互之間可能須要進行相應的問候,這個時候得須要有一個「看管者」,其職責除了須要管理動物園裏的這些動物的行爲以外(即這些系統服務的行爲),還須要統一協調管理這些動物之間的「問候」、「打交道」(系統服務之間的調用)!

ZooKeeper對外會提供一個多層級的節點命名空間(節點稱爲ZNode),每一個節點都用一個以斜槓(/)分隔的路徑表示,並且每一個節點都有父節點(根節點除外)。ZooKeeper的相關功能特性在實際使用過程當中,其底層可能須要動態的添加、刪減相應的節點,此時zk會提供一個Watcher監聽器,用以監聽那些動態新增、刪減的節點,即ZooKeeper會在某些業務場景對一些節點使用上Watcher機制,監聽相應的節點的動態。git


咱們即將要在下面介紹的「分佈式鎖」功能組件即爲ZooKeeper提供給開發者的一大利器,其底層的實現原理正是基於Watcher機制 + 動態建立、刪減臨時順序節點 所實現的,值得一提的是,一個ZNode節點將表明一個路徑。數據庫

如下爲ZooKeeper實現(獲取)分佈式鎖的原理:apache

(1)當前線程在獲取分佈式鎖的時候,會在ZNode節點(ZNode節點是Zookeeper的指定節點)下建立臨時順序節點,釋放鎖的時候將刪除該臨時節點。安全

(2)客戶端/服務 調用createNode方法在 ZNode節點 下建立臨時順序節點,而後調用getChildren(「ZNode」)來獲取ZNode下面的全部子節點,注意此時不用設置任何Watcher。bash

(3)客戶端/服務獲取到全部的子節點path以後,若是發現本身建立的子節點序號最小,那麼就認爲該客戶端獲取到了鎖,即當前線程獲取到了分佈式鎖。微信

(4)若是發現本身建立的節點並不是ZNode全部子節點中最小的,說明本身尚未獲取到鎖,此時客戶端須要找到比本身小的那個節點,而後對其調用exist()方法,同時對其註冊事件監聽器。多線程

以後,讓這個被關注的節點刪除(核心業務邏輯執行完,釋放鎖的時候,就是刪除該節點),則客戶端的Watcher會收到相應的通知,此時再次判斷本身建立的節點是不是ZNode子節點中序號最小的,若是是則獲取到了鎖,若是不是則重複以上步驟繼續獲取到比本身小的一個節點並註冊監聽。架構

以上爲ZooKeeper的基本介紹以及關於其底層實現分佈式鎖的原理的介紹,可是,Debug想說的是「理論再好,若是不會轉化爲實際的代碼或者輸出,那隻能稱之爲泛泛而談、吹牛逼」 !併發

下面,咱們將基於Spring Boot搭建的秒殺系統整合ZooKeeper,並基於ZooKeeper實現一種分佈式鎖,以此解決秒殺系統中高併發多線程併發產生的諸多問題。app

(1)首先,固然是引入ZooKeeper的依賴啦,其中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的配置,包括其服務所在的Host、端口Port、命名空間等等:

#zookeeper
zk.host=127.0.0.1:2181
zk.namespace=kill複製代碼

(2)而後,跟Redis、Redisson同樣,咱們須要基於Spring Boot自定義注入ZooKeeper的相關操做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秒殺服務類中,咱們建立了一個新的秒殺處理方法killItemV5,表示藉助ZooKeeper中間件解決高併發多線程併發訪問共享資源~共享代碼塊出現的併發安全問題!

@Autowired
private CuratorFramework curatorFramework;
//TODO:路徑就至關於一個ZNode
private static final String pathPrefix="/kill/zkLock/";
 
//商品秒殺核心業務邏輯的處理-基於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:核心業務邏輯
            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-您已經搶購過該商品了!");
            }
        }
    }catch (Exception e){
        throw new Exception("還沒到搶購日期、已過了搶購時間或已被搶購完畢!");
    }finally {
        //釋放分佈式鎖
        if (mutex!=null){
            mutex.release();
        }
    }
    return result;
}複製代碼

從上述該源代碼中能夠看出其核心的處理邏輯在於「定義操做組件實例」、「獲取鎖」以及「釋放鎖」的實現上,以下所示:

//定義獲取分佈式鎖的操做組件實例
InterProcessMutex mutex=new InterProcessMutex(curatorFramework,pathPrefix+killId+userId+"-lock");
 
//嘗試獲取分佈式鎖
mutex.acquire(10L,TimeUnit.SECONDS)
 
//釋放鎖
mutex.release();複製代碼

(4)至此,基於統一協調調度中心中間件ZooKeeper實現的分佈式鎖的代碼咱們已經實戰完畢了,下面咱們按照慣例,進入壓測環節,數據用例以及壓測的線程組的線程數咱們仍舊跟之前同樣,total=6本書,用戶Id爲10040~10049即10個用戶,線程數爲1w。

點擊JMeter的啓動按鈕,便可發起秒級併發1w個線程的請求,稍等片刻(由於ZooKeeper須要不斷的在當前設定的節點建立、刪除臨時節點,故而耗時仍是比較長的),觀察控制檯的輸出以及數據庫表item_kill、item_kill_success表最終的數據記錄結果,以下圖所示:


補充:

一、目前,這一秒殺系統的總體構建與代碼實戰已經所有完成了,完整的源代碼數據庫地址能夠來這裏下載:gitee.com/steadyjack/… 記得Fork跟Star啊!!

二、最後,不要忘記了關注一下Debug的技術微信公衆號:

相關文章
相關標籤/搜索