中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Java分布式鎖的三種實現方式是什么

發布時間:2022-08-26 11:24:24 來源:億速云 閱讀:289 作者:iii 欄目:開發技術

這篇“Java分布式鎖的三種實現方式是什么”文章的知識點大部分人都不太理解,所以小編給大家總結了以下內容,內容詳細,步驟清晰,具有一定的借鑒價值,希望大家閱讀完這篇文章能有所收獲,下面我們一起來看看這篇“Java分布式鎖的三種實現方式是什么”文章吧。

    Java中的鎖主要包括synchronized鎖和JUC包中的鎖,這些鎖都是針對單個JVM實例上的鎖,對于分布式環境如果我們需要加鎖就顯得無能為力。

    在單個JVM實例上,鎖的競爭者通常是一些不同的線程,而在分布式環境中,鎖的競爭者通常是一些不同的線程或者進程。如何實現在分布式環境中對一個對象進行加鎖呢?答案就是分布式鎖。

    分布式鎖實現方案

    目前分布式鎖的實現方案主要包括三種:

    • 基于數據庫(唯一索引)

    • 基于緩存(Redis,memcached,tair)

    • 基于Zookeeper

    基于數據庫實現分布式鎖:主要是利用數據庫的唯一索引來實現,唯一索引天然具有排他性,這剛好符合我們對鎖的要求:同一時刻只能允許一個競爭者獲取鎖。加鎖時我們在數據庫中插入一條鎖記錄,利用業務id進行防重。當第一個競爭者加鎖成功后,第二個競爭者再來加鎖就會拋出唯一索引沖突,如果拋出這個異常,我們就判定當前競爭者加鎖失敗。防重業務id需要我們自己來定義,例如我們的鎖對象是一個方法,則我們的業務防重id就是這個方法的名字,如果鎖定的對象是一個類,則業務防重id就是這個類名。

    基于緩存實現分布式鎖:理論上來說使用緩存來實現分布式鎖的效率最高,加鎖速度最快,因為Redis幾乎都是純內存操作,而基于數據庫的方案和基于Zookeeper的方案都會涉及到磁盤文件IO,效率相對低下。一般使用Redis來實現分布式鎖都是利用Redis的SETNX key value這個命令,只有當key不存在時才會執行成功,如果key已經存在則命令執行失敗。

    基于Zookeeper:Zookeeper一般用作配置中心,其實現分布式鎖的原理和Redis類似,我們在Zookeeper中創建瞬時節點,利用節點不能重復創建的特性來保證排他性。

    在實現分布式鎖的時候我們需要考慮一些問題,例如:分布式鎖是否可重入,分布式鎖的釋放時機,分布式鎖服務端是否有單點問題等。

    基于數據庫實現分布式鎖

    上面已經分析了基于數據庫實現分布式鎖的基本原理:通過唯一索引保持排他性,加鎖時插入一條記錄,解鎖是刪除這條記錄。下面我們就簡要實現一下基于數據庫的分布式鎖。

    表設計

    CREATE TABLE `distributed_lock` (
      `id` bigint(20) NOT NULL AUTO_INCREMENT,
      `unique_mutex` varchar(255) NOT NULL COMMENT '業務防重id',
      `holder_id` varchar(255) NOT NULL COMMENT '鎖持有者id',
      `create_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
      PRIMARY KEY (`id`),
      UNIQUE KEY `mutex_index` (`unique_mutex`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

    id字段是數據庫的自增id,unique_mutex字段就是我們的防重id,也就是加鎖的對象,此對象唯一。在這張表上我們加了一個唯一索引,保證unique_mutex唯一性。holder_id代表競爭到鎖的持有者id。

    加鎖

    insert into distributed_lock(unique_mutex, holder_id) values (‘unique_mutex', ‘holder_id');

    如果當前sql執行成功代表加鎖成功,如果拋出唯一索引異常(DuplicatedKeyException)則代表加鎖失敗,當前鎖已經被其他競爭者獲取。

    解鎖

    delete from methodLock where unique_mutex=‘unique_mutex' and holder_id=‘holder_id';

    解鎖很簡單,直接刪除此條記錄即可。

    分析

    是否可重入:就以上的方案來說,我們實現的分布式鎖是不可重入的,即是是同一個競爭者,在獲取鎖后未釋放鎖之前再來加鎖,一樣會加鎖失敗,因此是不可重入的。解決不可重入問題也很簡單:加鎖時判斷記錄中是否存在unique_mutex的記錄,如果存在且holder_id和當前競爭者id相同,則加鎖成功。這樣就可以解決不可重入問題。

    鎖釋放時機:設想如果一個競爭者獲取鎖時候,進程掛了,此時distributed_lock表中的這條記錄就會一直存在,其他競爭者無法加鎖。為了解決這個問題,每次加鎖之前我們先判斷已經存在的記錄的創建時間和當前系統時間之間的差是否已經超過超時時間,如果已經超過則先刪除這條記錄,再插入新的記錄。另外在解鎖時,必須是鎖的持有者來解鎖,其他競爭者無法解鎖。這點可以通過holder_id字段來判定。

    數據庫單點問題:單個數據庫容易產生單點問題:如果數據庫掛了,我們的鎖服務就掛了。對于這個問題,可以考慮實現數據庫的高可用方案,例如MySQL的MHA高可用解決方案。

    基于Zookeeper實現分布式鎖

    前置知識

    Zookeeper的數據存儲結構就像一棵樹,這棵樹由節點組成,這種節點叫做Znode。

    Znode分為四種類型:

    • 持久節點(PERSISTENT):默認的節點類型。創建節點的客戶端與zookeeper斷開連接后,該節點依舊存在 。

    • 持久節點順序節點(PERSISTENT_SEQUENTIAL): 所謂順序節點,就是在創建節點時,Zookeeper根據創建的時間順序給該節點名稱進行編號:

    • 臨時節點(EPHEMERAL) :和持久節點相反,當創建節點的客戶端與zookeeper斷開連接后,臨時節點會被刪除。

    • 臨時順序節點(EPHEMERAL_SEQUENTIAL) :顧名思義,臨時順序節點結合和臨時節點和順序節點的特點:在創建節點時,Zookeeper根據創建的時間順序給該節點名稱進行編號;當創建節點的客戶端與Zookeeper斷開連接后,臨時節點會被刪除。

    Zookeeper分布式鎖恰恰應用了臨時順序節點。具體如何實現呢?讓我們來看一看詳細步驟:

    加鎖和解鎖流程

    獲取鎖

    首先,在Zookeeper當中創建一個持久節點ParentLock。當第一個客戶端想要獲得鎖時,需要在ParentLock這個節點下面創建一個臨時順序節點 Lock1。

    Java分布式鎖的三種實現方式是什么

    之后,Client1查找ParentLock下面所有的臨時順序節點并排序,判斷自己所創建的節點Lock1是不是順序最靠前的一個。如果是第一個節點,則成功獲得鎖。

    Java分布式鎖的三種實現方式是什么

    這時候,如果再有一個客戶端 Client2 前來獲取鎖,則在ParentLock下載再創建一個臨時順序節點Lock2。

    Java分布式鎖的三種實現方式是什么

    Client2查找ParentLock下面所有的臨時順序節點并排序,判斷自己所創建的節點Lock2是不是順序最靠前的一個,結果發現節點Lock2并不是最小的。

    于是,Client2向排序僅比它靠前的節點Lock1注冊Watcher,用于監聽Lock1節點是否存在。這意味著Client2搶鎖失敗,進入了等待狀態。

    Java分布式鎖的三種實現方式是什么

    這時候,如果又有一個客戶端Client3前來獲取鎖,則在ParentLock下載再創建一個臨時順序節點Lock3。

    Java分布式鎖的三種實現方式是什么

    Client3查找ParentLock下面所有的臨時順序節點并排序,判斷自己所創建的節點Lock3是不是順序最靠前的一個,結果同樣發現節點Lock3并不是最小的。

    于是,Client3向排序僅比它靠前的節點Lock2注冊Watcher,用于監聽Lock2節點是否存在。這意味著Client3同樣搶鎖失敗,進入了等待狀態。

    Java分布式鎖的三種實現方式是什么

    這樣一來,Client1得到了鎖,Client2監聽了Lock1,Client3監聽了Lock2。這恰恰形成了一個等待隊列,很像是Java當中ReentrantLock(可重入鎖)所依賴的AQS(AbstractQueuedSynchronizer)。

    獲得鎖的過程大致就是這樣,那么Zookeeper如何釋放鎖呢?

    釋放鎖的過程很簡單,只需要釋放對應的子節點就好。

    釋放鎖

    釋放鎖分為兩種情況:

    1.任務完成,客戶端顯示釋放

    當任務完成時,Client1會顯示調用刪除節點Lock1的指令。

    Java分布式鎖的三種實現方式是什么

    2.任務執行過程中,客戶端崩潰

    獲得鎖的Client1在任務執行過程中,如果Duang的一聲崩潰,則會斷開與Zookeeper服務端的鏈接。根據臨時節點的特性,相關聯的節點Lock1會隨之自動刪除。

    Java分布式鎖的三種實現方式是什么

    由于Client2一直監聽著Lock1的存在狀態,當Lock1節點被刪除,Client2會立刻收到通知。這時候Client2會再次查詢ParentLock下面的所有節點,確認自己創建的節點Lock2是不是目前最小的節點。如果是最小,則Client2順理成章獲得了鎖。

    Java分布式鎖的三種實現方式是什么

    同理,如果Client2也因為任務完成或者節點崩潰而刪除了節點Lock2,那么Client3就會接到通知。

    Java分布式鎖的三種實現方式是什么

    最終,Client3成功得到了鎖。

    Java分布式鎖的三種實現方式是什么

    使用Zookeeper實現分布式鎖的大致流程就是這樣。

    分析

    解決不可重入:客戶端加鎖時將主機和線程信息寫入鎖中,下一次再來加鎖時直接和序列最小的節點對比,如果相同,則加鎖成功,鎖重入。

    鎖釋放時機:由于我們創建的節點是順序臨時節點,當客戶端獲取鎖成功之后突然session會話斷開,ZK會自動刪除這個臨時節點。

    單點問題:ZK是集群部署的,主要一半以上的機器存活,就可以保證服務可用性。

    利用curator實現

    Zookeeper第三方客戶端curator中已經實現了基于Zookeeper的分布式鎖。利用curator加鎖和解鎖的代碼如下:

    @Autowired
    private CuratorFramework curatorFramework;
    // 加鎖,支持超時,可重入
    public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
        //
        InterProcessMutex interProcessMutex= new InterProcessMutex(curatorFramework, "/ParenLock");
        try {
            return interProcessMutex.acquire(timeout, unit);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return true;
    }
    // 解鎖
    public boolean unlock() {
    InterProcessMutex interProcessMutex= new InterProcessMutex(curatorFramework,  "/ParenLock");
        try {
            interProcessMutex.release();
        } catch (Throwable e) {
            log.error(e.getMessage(), e);
        } finally {
            executorService.schedule(new Cleaner(client, path), delayTimeForClean, TimeUnit.MILLISECONDS);
        }
        return true;
    }

    最常用的鎖:

    • InterProcessMutex:分布式可重入排它鎖

    • InterProcessSemaphoreMutex:分布式排它鎖

    • InterProcessReadWriteLock:分布式讀寫鎖

    基于緩存實現分布式鎖,以Redis為例

    加鎖

    public class RedisTool {
        private static final String LOCK_SUCCESS = "OK";
        private static final String SET_IF_NOT_EXIST = "NX";
        private static final String SET_WITH_EXPIRE_TIME = "PX";
        /**
         * 加鎖
         * @param stringRedisTemplate Redis客戶端
         * @param lockKey 鎖的key
         * @param requestId 競爭者id
         * @param expireTime 鎖超時時間,超時之后鎖自動釋放
         * @return 
         */
        public static boolean getDistributedLock(StringRedisTemplate stringRedisTemplate, String lockKey, String requestId, int expireTime) {
            return stringRedisTemplate.opsForValue().setIfAbsent(lockKey, requestId, 30, TimeUnit.SECONDS);
        }
    }

    可以看到,我們加鎖就一行代碼:

    stringRedisTemplate.opsForValue().setIfAbsent(lockKey, requestId, 30, TimeUnit.SECONDS);

    這個setIfAbsent()方法一共五個形參:

    • 第一個為key,我們使用key來當鎖,因為key是唯一的。

    • 第二個為value,這里寫的是鎖競爭者的id,在解鎖時,我們需要判斷當前解鎖的競爭者id是否為鎖持有者。

    • 第三個為expx,這個參數我們傳的是PX,意思是我們要給這個key加一個過期時間的設置,具體時間由第五個參數決定;

    • 第四個參數為time,與第四個參數相呼應,代表key的過期時間。

    總的來說,執行上面的setIfAbsent()方法就只會導致兩種結果:

    • 1.當前沒有鎖(key不存在),那么就進行加鎖操作,并對鎖設置一個有效期,同時value表示加鎖的客戶端。

    • 2.已經有鎖存在,不做任何操作。上述解鎖請求中,緩存超時機制保證了即使一個競爭者加鎖之后掛了,也不會產生死鎖問題:超時之后其他競爭者依然可以獲取鎖。通過設置value為競爭者的id,保證了只有鎖的持有者才能來解鎖,否則任何競爭者都能解鎖,那豈不是亂套了。

    解鎖

    public class RedisTool {
        private static final Long RELEASE_SUCCESS = 1L;
        /**
         * 釋放分布式鎖
         * @param stringRedisTemplate Redis客戶端
         * @param lockKey 鎖
         * @param requestId 鎖持有者id
         * @return 是否釋放成功
         */
        public static boolean releaseDistributedLock(StringRedisTemplate stringRedisTemplate, String lockKey, String requestId) {
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
            Long result = stringRedisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Collections.singletonList(lockKey), requestId);
            return RELEASE_SUCCESS.equals(result);
        }
    }

    解鎖的步驟

    • 1、判斷當前解鎖的競爭者id是否為鎖的持有者,如果不是直接返回失敗,如果是則進入第2步。

    • 2、刪除key,如果刪除成功,返回解鎖成功,否則解鎖失敗。

    注意到這里解鎖其實是分為2個步驟,涉及到解鎖操作的一個原子性操作問題。這也是為什么我們解鎖的時候用Lua腳本來實現,因為Lua腳本可以保證操作的原子性。那么這里為什么需要保證這兩個步驟的操作是原子操作呢?

    設想:假設當前鎖的持有者是競爭者1,競爭者1來解鎖,成功執行第1步,判斷自己就是鎖持有者,這是還未執行第2步。這是鎖過期了,然后競爭者2對這個key進行了加鎖。加鎖完成后,競爭者1又來執行第2步,此時錯誤產生了:競爭者1解鎖了不屬于自己持有的鎖。可能會有人問為什么競爭者1執行完第1步之后突然停止了呢?這個問題其實很好回答,例如競爭者1所在的JVM發生了GC停頓,導致競爭者1的線程停頓。這樣的情況發生的概率很低,但是請記住即使只有萬分之一的概率,在線上環境中完全可能發生。因此必須保證這兩個步驟的操作是原子操作。

    分析

    • 是否可重入:以上實現的鎖是不可重入的,如果需要實現可重入,在SET_IF_NOT_EXIST之后,再判斷key對應的value是否為當前競爭者id,如果是返回加鎖成功,否則失敗。

    • 鎖釋放時機:加鎖時我們設置了key的超時,當超時后,如果還未解鎖,則自動刪除key達到解鎖的目的。如果一個競爭者獲取鎖之后掛了,我們的鎖服務最多也就在超時時間的這段時間之內不可用。

    • Redis單點問題:如果需要保證鎖服務的高可用,可以對Redis做高可用方案:Redis集群+主從切換。目前都有比較成熟的解決方案。

    redis分布式鎖,更詳細的可以參考:分布式鎖(Redisson)原理分析

    三種方案比較

    方案理解難易程度實現的復雜度性能可靠性優點缺點
    基于數據庫容易復雜不可靠  
    基于緩存(Redis)一般一般可靠Set和Del指令性能較高1.實現復雜,需要考慮超時,原子性,誤刪等情形。2.沒有等待鎖的隊列,只能在客戶端自旋來等待,效率低下。(但是現在有Redisson這兩缺點就相當于沒有了)
    基于Zookeeper簡單一般一般1.有封裝好的框架,容易實現2.有等待鎖的隊列,大大提升搶鎖效率。添加和刪除節點性能較低

    以上就是關于“Java分布式鎖的三種實現方式是什么”這篇文章的內容,相信大家都有了一定的了解,希望小編分享的內容對大家有幫助,若想了解更多相關的知識內容,請關注億速云行業資訊頻道。

    向AI問一下細節

    免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

    AI

    巴楚县| 苏尼特左旗| 道孚县| 宾川县| 乌鲁木齐县| 江门市| 彭山县| 广南县| 大新县| 雷州市| 穆棱市| 五台县| 广河县| 巴塘县| 石楼县| 上饶市| 肃南| 登封市| 游戏| 南安市| 璧山县| 巴马| 柳林县| 长宁县| 区。| 安顺市| 永兴县| 柳江县| 无为县| 和林格尔县| 南靖县| 清苑县| 略阳县| 仙居县| 巴马| 金堂县| 绥滨县| 疏勒县| 巩义市| 和政县| 大丰市|