從零開始SpringCloud Alibaba電商系統(十五)——互斥鎖的概念、分佈式鎖的實現

零、系列

歡迎來嫖從零開始SpringCloud Alibaba電商系列:

  1. 從零開始SpringCloud Alibaba電商系統(一)——Alibaba與Nacos服務註冊與發現
  2. 從零開始SpringCloud Alibaba電商系統(二)——Nacos配置中心
  3. 從零開始SpringCloud Alibaba電商系統(三)——Sentinel流量防衛兵介紹、流量控制demo
  4. 從零開始SpringCloud Alibaba電商系統(四)——Sentinel的fallback和blockHandler
  5. 從零開始SpringCloud Alibaba電商系統(五)——Feign Demo,Sentinel+Feign實現多節點間熔斷/服務降級
  6. 從零開始SpringCloud Alibaba電商系統(六)——Sentinel規則持久化到Nacos配置中心
  7. 從零開始SpringCloud Alibaba電商系統(七)——Spring Security實現登錄認證、權限控制
  8. 從零開始SpringCloud Alibaba電商系統(八)——用一個好看的Swagger接口文檔
  9. 從零開始SpringCloud Alibaba電商系統(九)——基於Spring Security OAuth2實現SSO-認證服務器(非JWT)
  10. 從零開始SpringCloud Alibaba電商系統(十)——基於Redis Session的認證鑑權
  11. 從零開始SpringCloud Alibaba電商系統(十一)——spring security完善之動態url控制
  12. 從零開始SpringCloud Alibaba電商系統(十二)——spring aop記錄用戶操作日誌
  13. 從零開始SpringCloud Alibaba電商系統(十三)——ElasticSearch介紹、logback寫入ES
  14. 從零開始SpringCloud Alibaba電商系統(十四)——簡單商品模塊需求、使用ElasticSearch構建商品搜索

一、互斥鎖

鎖的範圍很大,真要有人說給你講講鎖,怕不是耍流氓就是準備三天三夜……

筆者這裏拋開其他的概念,從基本的互斥鎖開始,慢慢深入擴展,什麼是互斥鎖?一句話的定義可能不好描述,但是要實現一個互斥鎖需要滿足以下條件?

不準永遠耽擱一個要求進入臨界區域的線程,造成死鎖或是飢餓發生 。
若沒有任何線程處於臨界區域時,任何要求進入臨界區域的線程必須立刻得到允許。
不能對線程的相對速度與處理器的數目做任何假設。
線程只能在臨界區域內停留一有限的時間。
任何時間只允許一個線程在臨界區域運行。
在臨界區域停止運行的線程,不準影響其他線程運行。 ——wiki百科

再來回味一下互斥鎖,我們可能會明白,鎖是爲了讓A線程能夠獨自運行一個程序一段時間的做法,在線程A獨佔的這個時間裏,其他線程想要運行這個程序都得等着。

那麼我們可能就會有一個初步的想法,想要實現一個互斥鎖,可以使用一個所有線程都可以看到的變量i=0,當有線程先將該變量置爲1時,其他線程判斷到該變量爲1就會等待(進入臨界區或不管循環判斷變量是否變回了0),流程如下(不靠譜的時序):
在這裏插入圖片描述
重試加鎖的方式被稱爲自旋重試機制,一般是樂觀鎖的實現方式,悲觀鎖也就是按照上述嚴格要求的實現,這裏應該線程B進行wait,直到線程A解鎖之後,共享變量i被notify,線程B纔會繼續執行上鎖的程序


二、Java中的鎖

synchronized

synchronized是最好的例子,我們來分析一下它是怎麼做到上述互斥鎖的作用的。

我們知道 synchronized 有三種加鎖的方式:普通方法加鎖、 靜態方法加鎖、同步方法塊加鎖。
無論是哪一種方式,實質上都是對實例對象/Class對象加鎖,與我們上述共享變量的方式無甚差別。

舉個例子,同步方法塊中對對象a加鎖:synchorized(a){ doSomthing() }。

  1. 當線程A要執行doSomthing() ,需要先進入synchorized,此時會檢查a對象是否是上鎖狀態,如果沒有,好的,我上。
  2. 此時線程B也想執行doSomething(),檢查a對象是已經被人佔了,好,那我blocking,堵在門口等線程A出來。(在jdk1.6之後,synchorized關鍵字被優化,當線程B發現a對象被佔用時不會直接blocking,而是先採用CAS+自旋重試的操作,與我們之前的共享變量方式類似。而當自旋重試一段時間還等不到鎖的時候,纔會升級爲重量鎖,進入blocking狀態。)

ReentrantLock

Retreenlock是JUC中最常用的互斥鎖,它的實現原理與synchroized有異曲同工之妙。
ReentrantLock核心也是通過一個共享變量實現的,共享變量=0代表沒人持有鎖。

舉個ReentrantLock非公平鎖使用的例子(非公平鎖指的是新來的線程可能插隊獲取鎖,公平鎖則是所有線程按照先後順序獲取鎖):

	  ReentrantLock reentrantLock = new ReentrantLock(false); //tru爲公平鎖
     reentrantLock.lock();
     doSomthing();
     reentrantLock.unlock();
  1. 當線程A要執行doSomthing()方法,會進入reentrantLock.lock,通過CAS嘗試將共享變量置爲1,嘗試成功,加鎖成功,執行doSomthing()。

  2. 此時線程B也想執行doSomthing(),通過CAS嘗試將共享變量置爲1,失敗,於是AQS隊列登場,線程B入隊列、同時線程B調用unsafe.park被阻塞。

     AQS是reentrantLock內部維護的一個隊列,用於存儲所有試圖獲取該鎖但失敗的線程。
     我們知道隊列是FIFO(先進先出)的,所以這個隊列保證了公平性,相對應的,synchronized就不是公平鎖。
    
  3. 當線程A執行結束,unlock,AQS隊列第一個元素出隊,也就是線程B,對B進行unsafe.unpark,於是線程B就可以快了的doSomthing();

    synchronized與ReentrantLock作爲互斥鎖,在非公平鎖的情況下,用法也會有差異。因爲syncronized實際上是通過標識一個對象來區分是否加鎖,而ReentrantLock是標識了一個變量來標識標識,這是很重要的一個差別。


三、分佈式鎖

我們已經實現了互斥鎖,並且簡單觀察了一些實現的案例,接下來讓我們來思考如何實現分佈式鎖,這是相當有必要的,一個分佈式系統中,只存在於單個jvm中的鎖顯然沒辦法滿足多節點公用一個鎖的需求。

1. 超時問題

但是分佈式鎖相對於jvm層面的鎖來說,我們需要注意更多的問題。在jvm層面上我們對加鎖的狀態只需要考慮成功、失敗這兩種狀態。

但是在分佈式系統層面上,我們需要另一個節點鎖服務器客戶端想要獲得鎖需要網絡訪問鎖服務器。如此一來,由於網絡的不確定性,我們不得不引入第三種狀態——超時
於是引出下面的問題:

鎖的持有者的unlock指令在網絡傳輸中丟失了怎麼辦?或者因爲網絡問題,過了幾分鐘甚至幾個小時纔到達怎麼辦?
總不能讓其他節點一直等着吧。

對於這樣佔着茅坑不拉屎的持鎖者,我們需要制裁,於是出現了一下兩種解決方案

  1. 鎖設置時效性,一定時間就失效,其他人就可以獲得鎖了。
  2. 規定鎖的持有規則,只有客戶端鎖服務器 保存連接纔算持有鎖,一旦網絡波動、連接斷開,那麼鎖就自動消除。

第一種方式是redis、mysql爲主的數據雲端持久化類型的實現方案;第二種則是以zookeeper爲主的基於心跳機制的實現方案。

2.鎖過期了,任務沒結束

上面我們解決了持有鎖時間過長的問題,但是在上面的第一種解決方案中,卻會引出另外的問題:

我張三加鎖後,任務還沒執行完,鎖就過期了。李四拿到了鎖,這下操作不就併發衝突了嗎?

解決方案:
對鎖進行續期。客戶端持有鎖之後,定期向鎖服務器發起請求,表示自己還沒用完,比如redis的一個java客戶端(redission)就是採用這種方式實現鎖。實際上,zookeeper本質上也是這種不斷續約的方式來維護服務端和客戶端連接的。

3. 李四解了張三的鎖

除此之外,分佈式鎖還需要考慮一些基本問題,比如節點B解了節點A的鎖,其根源還是受到了網絡的影響,比如:

張三的unlock指令半天沒到`鎖服務器`,我李四拿到了鎖,結果張三的unlock指令來了,把我的鎖解了,你想幹啥?

鎖被別人解了,這在jvm鎖中是不存在的問題,因爲jvm中不存在數據傳輸不可靠的問題。
解決方案鎖服務器記錄請求人的id,比如線程id。zookeeper的一種java客戶端(curator)就是採用這種方式來區分加鎖人。

4. 加鎖解鎖的原子性問題

加鎖解鎖的原子性問題並非分佈式鎖獨有,比如ReentrantLock,它是java代碼級別實現的鎖,那麼在代碼級別它就一定要保證自己的加鎖、解鎖操作是沒有併發衝突的。
同學們追一下源碼就能知道,ReentrantLock的加鎖解鎖操作都是通過unsafe.compareAndSwapInt()來實現的對共享變量的更新操作,也就是我們常說的CAS
CAS是計算機層面的原子操作,不存在併發問題,它的語義是將變量i從值A修改爲值B,若原值不爲A則修改失敗。通過CAS,就可以控制共享變量的值,進而保證鎖的原子性

分佈式鎖如何保證原子性?

  1. zookeeper:zk的操作很簡單,它能夠保證同一時刻只有一個請求能成功註冊同一key;同時解鎖操作只需要我們客戶端斷開與zk服務端的請求,zk就會自動消除鎖。
  2. redis:redis則需要稍稍花一些心思。加鎖不需要操心,setnx+expire的操作目前已經被redis官方做出了參數形式,可以保證原子性。解鎖則需要實現一個簡單的lua腳本(redis的多條命令可以通過寫在一個lua腳本,來實現原子性)。

四、代碼實現

這裏分別用ReentrantLock、Redission、Curator來演示三種鎖的簡單用法。
案例簡述: 共享變量i,是個線程同時對其進行-1操作。現在我們要求上鎖來保證他不超賣。
以下截圖是筆者反覆測試了好久纔出現了一個超賣的例子,截圖留念,兩個資源,三個線程成功獲取到。 在這裏插入圖片描述

1. ReentrantLock

/**
 * com.mall.zk.demo.juc
 *
 * @author: lele
 * @date: 2020-06-05
 */
public class ReentrantRockUtil {
    private final static ReentrantLock reentrantLock;
    static {
        reentrantLock = new ReentrantLock();
    }

    public static ReentrantLock getLock(){
        return reentrantLock;
    }

    // 共享變量
    public static int i=2;
    public static void main(String[] args) throws Exception{
        final CountDownLatch cdl = new CountDownLatch(1);

        for (int i = 0; i < 50; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try{
                        cdl.await();
                        getLock().lock();
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                    if( ReentrantRockUtil.i>0 ){
                        int res = ReentrantRockUtil.i-1;
                        ReentrantRockUtil.i = res;
                        System.out.println(ReentrantRockUtil.i);
                    }
                    try {
                        getLock().unlock();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
        cdl.countDown();
    }
}

2. Curator實現Lock(Zookeeper)

public class CuratorLockUtil {

    public final static String lockRoot="/lock";
    private static CuratorFramework client;
    public static InterProcessMutex lock;

    static{
        client = CuratorFrameworkFactory.builder()
                .connectString("zookeeper ip:2181")
                .retryPolicy( new ExponentialBackoffRetry(1000,3))
                .build();
        client.start();
    }

    /**
     * 注意這裏使用多例,單例沒有意義,無法模擬多節點
     * @return
     */
    public static InterProcessMutex getLock(){
        // 這裏爲了測試自定義了LockInternalsDriver,這個參數不傳也可,一般夠用
        lock=new InterProcessMutex(client, lockRoot);
        return  lock;
    }

    /**
     * 非必須
     * @return
     */
    public static String getLockId(){
        return Thread.currentThread().getName();
    }

    // 共享變量
    public static int i=10;
    public static void main(String[] args) throws Exception{
        final CountDownLatch cdl = new CountDownLatch(1);
        // cdl.await(); // 等待
        // cdl.countDown(); // 代表當前線程已結束,可以放開

        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    InterProcessMutex lock = getLock();
                    try{
                        cdl.await();
                        lock.acquire();
                        //getLock().acquire();
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                    int res = CuratorLockUtil.i-1;
                    CuratorLockUtil.i = res;
                    System.out.println(CuratorLockUtil.i);
                    try {
                        lock.release();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
        cdl.countDown();

    }
}

3. Redission實現lock(redis)

public class RedissonLockUtil {

    private final static RedissonClient redissonClient;
    private final static String lockName="redisLock";
    static {
        Config config = new Config();
        config.useSingleServer()
                .setAddress("redis://reids ip:6379")
                .setPassword("redis密碼");
        redissonClient = Redisson.create(config);
    }

    public static RLock getLock( String name ){
        return redissonClient.getLock(name);
    }
    public static RLock getLock( ){
        return getLock(lockName);
    }
    // 共享變量
    public static int i=10;
    public static void main(String[] args) throws Exception{
        final CountDownLatch cdl = new CountDownLatch(1);

        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    RLock lock = getLock();
                    try{
                        cdl.await();
                        lock.lock(15,TimeUnit.SECONDS);
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                    if( RedissonLockUtil.i>0 ){
                        int res = RedissonLockUtil.i-1;
                        RedissonLockUtil.i = res;
                        System.out.println(RedissonLockUtil.i);
                    }
                    try {
                        lock.unlock();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
        cdl.countDown();
    }
}

五、demo地址

https://github.com/flyChineseBoy/lel-mall/tree/master/mall15

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章