久久综合九色综合97婷婷-美女视频黄频a免费-精品日本一区二区三区在线观看-日韩中文无码有码免费视频-亚洲中文字幕无码专区-扒开双腿疯狂进出爽爽爽动态照片-国产乱理伦片在线观看夜-高清极品美女毛茸茸-欧美寡妇性猛交XXX-国产亚洲精品99在线播放-日韩美女毛片又爽又大毛片,99久久久无码国产精品9,国产成a人片在线观看视频下载,欧美疯狂xxxx吞精视频

有趣生活

當前位置:首頁>職場>分布式鎖的幾種典型實現(聊聊分布式鎖的多種實現)

分布式鎖的幾種典型實現(聊聊分布式鎖的多種實現)

發布時間:2024-01-24閱讀(15)

導讀前言大家好,我是撿田螺的小男孩。今天跟大家探討一下分布式鎖的設計與實現。希望對大家有幫助,如果有不正確的地方,歡迎指出,一起學習,一起進步哈~分布式鎖概述數....前言

大家好,我是撿田螺的小男孩。今天跟大家探討一下分布式鎖的設計與實現。希望對大家有幫助,如果有不正確的地方,歡迎指出,一起學習,一起進步哈~

  • 分布式鎖概述
  • 數據庫分布式鎖
  • redis分布式鎖
  • Zookeeper分布式鎖
  • 三種分布式鎖對比
1. 分布式鎖概述

我們的系統都是分布式部署的,日常開發中,秒殺下單、搶購商品等等業務場景,為了防?庫存超賣,都需要用到分布式鎖

分布式鎖其實就是,控制分布式系統不同進程共同訪問共享資源的一種鎖的實現。如果不同的系統或同一個系統的不同主機之間共享了某個臨界資源,往往需要互斥來防止彼此干擾,以保證一致性。

業界流行的分布式鎖實現,一般有這3種方式:

  • 基于數據庫實現的分布式鎖
  • 基于Redis實現的分布式鎖
  • 基于Zookeeper實現的分布式鎖
2. 基于數據庫的分布式鎖2.1 數據庫悲觀鎖實現的分布式鎖

可以使用select ... for update 來實現分布式鎖。我們自己的項目,分布式定時任務,就使用類似的實現方案,我給大家來展示個簡單版的哈

表結構如下:

CREATE TABLE `t_resource_lock` ( `key_resource` varchar(45) COLLATE utf8_bin NOT NULL DEFAULT 資源主鍵, `status` char(1) COLLATE utf8_bin NOT NULL DEFAULT COMMENT S,F,P, `lock_flag` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 1是已經鎖 0是未鎖, `begin_time` datetime DEFAULT NULL COMMENT 開始時間, `end_time` datetime DEFAULT NULL COMMENT 結束時間, `client_ip` varchar(45) COLLATE utf8_bin NOT NULL DEFAULT 搶到鎖的IP, `time` int(10) unsigned NOT NULL DEFAULT 60 COMMENT 方法生命周期內只允許一個結點獲取一次鎖,單位:分鐘, PRIMARY KEY (`key_resource`) USING BTREE) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin

加鎖lock方法的偽代碼如下:

@Transcational //一定要加事務public boolean lock(String keyResource,int time){ resourceLock = select * from t_resource_lock where key_resource =#{keySource} for update; try{ if(resourceLock==null){ //插入鎖的數據 resourceLock = new ResourceLock(); resourceLock.setTime(time); resourceLock.setLockFlag(1); //上鎖 resourceLock.setStatus(P); //處理中 resourceLock.setBeginTime(new Date()); int count = "insert into resourceLock"; if(count==1){ //獲取鎖成功 return true; } return false; } }catch(Exception x){ return false; } //沒上鎖并且鎖已經超時,即可以獲取鎖成功 if(resourceLock.getLockFlag==0&&S.equals(resourceLock.getstatus) && new Date()>=resourceLock.addDateTime(resourceLock.getBeginTime(,time)){ resourceLock.setLockFlag(1); //上鎖 resourceLock.setStatus(P); //處理中 resourceLock.setBeginTime(new Date()); //update resourceLock; return true; }else if(new Date()>=resourceLock.addDateTime(resourceLock.getBeginTime(,time)){ //超時未正常執行結束,獲取鎖失敗 return false; }else{ return false; } }

解鎖unlock方法的偽代碼如下:

public void unlock(String v,status){ resourceLock.setLockFlag(0); //解鎖 resourceLock.setStatus(status); S:表示成功,F表示失敗 //update resourceLock; return ;}

整體流程:

try{if(lock(keyResource,time)){ //加鎖 status = process();//你的業務邏輯處理。 }} finally{ unlock(keyResource,status); //釋放鎖}

其實這個悲觀鎖實現的分布式鎖,整體的流程還是比較清晰的。就是先select ... for update 鎖住主鍵key_resource那個記錄,如果為空,則可以插入一條記錄,如果已有記錄判斷下狀態和時間是否已經超時。這里需要注意一下哈,必須要加事務哈。

2.2 數據庫樂觀鎖實現的分布式鎖

除了悲觀鎖,還可以用樂觀鎖實現分布式鎖。樂觀鎖,顧名思義,就是很樂觀,每次更新操作,都覺得不會存在并發沖突,只有更新失敗后,才重試。它是基于CAS思想實現的。我以前的公司,扣減余額就是用這種方案。

搞個version字段,每次更新修改,都會自增加一,然后去更新余額時,把查出來的那個版本號,帶上條件去更新,如果是上次那個版本號,就更新,如果不是,表示別人并發修改過了,就繼續重試。

大概流程如下:

  1. 查詢版本號和余額

select version,balance from account where user_id =666;

假設查到版本號是oldVersion=1.

  1. 邏輯處理,判斷余額

if(balance<扣減金額){ return;}left_balance = balance - 扣減金額;

  1. 進行扣減余額

update account set balance = #{left_balance} ,version = version 1 where version = #{oldVersion} and balance>= #{left_balance} and user_id =666;

大家可以看下這個流程圖哈:

分布式鎖的幾種典型實現(聊聊分布式鎖的多種實現)(1)

這種方式適合并發不高的場景,一般需要設置一下重試的次數

3.基于Redis實現的分布式鎖

Redis分布式鎖一般有以下這幾種實現方式:

  • setnx expire
  • setnx value值是過期時間
  • set的擴展命令(set ex px nx)
  • set ex px nx 校驗唯一隨機值,再刪除
  • Redisson
  • Redisson RedLock
3.1 setnx expire

聊到Redis分布式鎖,很多小伙伴反手就是setnx expire,如下:

if(jedis.setnx(key,lock_value) == 1){ //setnx加鎖 expire(key,100); //設置過期時間 try { do something //業務處理 }catch(){ } finally { jedis.del(key); //釋放鎖 }}

這段代碼是可以加鎖成功,但是你有沒有發現問題,加鎖操作和設置超時時間是分開的。假設在執行完setnx加鎖后,正要執行expire設置過期時間時,進程crash掉或者要重啟維護了,那這個鎖就長生不老了,別的線程永遠獲取不到鎖啦,所以分布式鎖不能這么實現

3.2 setnx value值是過期時間

long expires = System.currentTimeMillis() expireTime; //系統時間 設置的過期時間String expiresStr = String.valueOf(expires);// 如果當前鎖不存在,返回加鎖成功if (jedis.setnx(key, expiresStr) == 1) { return true;} // 如果鎖已經存在,獲取鎖的過期時間String currentValueStr = jedis.get(key);// 如果獲取到的過期時間,小于系統當前時間,表示已經過期if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) { // 鎖已過期,獲取上一個鎖的過期時間,并設置現在鎖的過期時間(不了解redis的getSet命令的小伙伴,可以去官網看下哈) String oldValueStr = jedis.getSet(key, expiresStr); if (oldValueStr != null && oldValueStr.equals(currentValueStr)) { // 考慮多線程并發的情況,只有一個線程的設置值和當前值相同,它才可以加鎖 return true; }} //其他情況,均返回加鎖失敗return false;}

日常開發中,有些小伙伴就是這么實現分布式鎖的,但是會有這些缺點

  • 過期時間是客戶端自己生成的,分布式環境下,每個客戶端的時間必須同步。
  • 沒有保存持有者的唯一標識,可能被別的客戶端釋放/解鎖
  • 鎖過期的時候,并發多個客戶端同時請求過來,都執行了jedis.getSet(),最終只能有一個客戶端加鎖成功,但是該客戶端鎖的過期時間,可能被別的客戶端覆蓋。
3.3 set的擴展命令(set ex px nx)

這個命令的幾個參數分別表示什么意思呢?跟大家復習一下:

SET key value [EX seconds] [PX milliseconds] [NX|XX]

  • EX second :設置鍵的過期時間為second秒。
  • PX millisecond :設置鍵的過期時間為millisecond毫秒。
  • NX :只在鍵不存在時,才對鍵進行設置操作。
  • XX :只在鍵已經存在時,才對鍵進行設置操作。

if(jedis.set(key, lock_value, "NX", "EX", 100s) == 1){ //加鎖 try { do something //業務處理 }catch(){ } finally { jedis.del(key); //釋放鎖 }}

這個方案可能存在這樣的問題:

  • 鎖過期釋放了,業務還沒執行完。
  • 鎖被別的線程誤刪。

有些伙伴可能會有個疑問,就是鎖為什么會被別的線程誤刪呢?假設并發多線程場景下,線程A獲得了鎖,但是它沒釋放鎖的話,線程B是獲取不到鎖的,所以按道理它是執行不到加鎖下面的代碼滴,怎么會導致鎖被別的線程誤刪呢?

假設線程A和B,都想用key加鎖,最后A搶到鎖加鎖成功,但是由于執行業務邏輯的耗時很長,超過了設置的超時時間100s。這時候,Redis就自動釋放了key鎖。這時候線程B就可以加鎖成功了,接下啦,它也執行業務邏輯處理。假設碰巧這時候,A執行完自己的業務邏輯,它就去釋放鎖,但是它就把B的鎖給釋放了。

3.4 set ex px nx 校驗唯一隨機值,再刪除

為了解決鎖被別的線程誤刪問題。可以在set ex px nx的基礎上,加上個校驗的唯一隨機值,如下:

if(jedis.set(key, uni_request_id, "NX", "EX", 100s) == 1){ //加鎖 try { do something //業務處理 }catch(){ } finally { //判斷是不是當前線程加的鎖,是才釋放 if (uni_request_id.equals(jedis.get(key))) { jedis.del(key); //釋放鎖 } }}

在這里,判斷當前線程加的鎖和釋放鎖不是一個原子操作。如果調用jedis.del()釋放鎖的時候,可能這把鎖已經不屬于當前客戶端,會解除他人加的鎖。

一般可以用lua腳本來包一下。lua腳本如下:

if redis.call(get,KEYS[1]) == ARGV[1] then return redis.call(del,KEYS[1]) else return 0end;

這種方式比較不錯了,一般情況下,已經可以使用這種實現方式。但是還是存在:鎖過期釋放了,業務還沒執行完的問題

3.5 Redisson

對于可能存在鎖過期釋放,業務沒執行完的問題。我們可以稍微把鎖過期時間設置長一些,大于正常業務處理時間就好啦。如果你覺得不是很穩,還可以給獲得鎖的線程,開啟一個定時守護線程,每隔一段時間檢查鎖是否還存在,存在則對鎖的過期時間延長,防止鎖過期提前釋放。

當前開源框架Redisson解決了這個問題。可以看下Redisson底層原理圖:

分布式鎖的幾種典型實現(聊聊分布式鎖的多種實現)(2)

只要線程一加鎖成功,就會啟動一個watch dog看門狗,它是一個后臺線程,會每隔10秒檢查一下,如果線程1還持有鎖,那么就會不斷的延長鎖key的生存時間。因此,Redisson就是使用watch dog解決了鎖過期釋放,業務沒執行完問題

3.6 Redisson RedLock

前面六種方案都只是基于Redis單機版的分布式鎖討論,還不是很完美。因為Redis一般都是集群部署的:

分布式鎖的幾種典型實現(聊聊分布式鎖的多種實現)(3)

如果線程一在Redis的master節點上拿到了鎖,但是加鎖的key還沒同步到slave節點。恰好這時,master節點發生故障,一個slave節點就會升級為master節點。線程二就可以順理成章獲取同個key的鎖啦,但線程一也已經拿到鎖了,鎖的安全性就沒了。

為了解決這個問題,Redis作者antirez提出一種高級的分布式鎖算法:Redlock。它的核心思想是這樣的:

部署多個Redis master,以保證它們不會同時宕掉。并且這些master節點是完全相互獨立的,相互之間不存在數據同步。同時,需要確保在這多個master實例上,是與在Redis單實例,使用相同方法來獲取和釋放鎖。

我們假設當前有5個Redis master節點,在5臺服務器上面運行這些Redis實例。

分布式鎖的幾種典型實現(聊聊分布式鎖的多種實現)(4)

RedLock的實現步驟:

  1. 獲取當前時間,以毫秒為單位。
  2. 按順序向5個master節點請求加鎖。客戶端設置網絡連接和響應超時時間,并且超時時間要小于鎖的失效時間。(假設鎖自動失效時間為10秒,則超時時間一般在5-50毫秒之間,我們就假設超時時間是50ms吧)。如果超時,跳過該master節點,盡快去嘗試下一個master節點。
  3. 客戶端使用當前時間減去開始獲取鎖時間(即步驟1記錄的時間),得到獲取鎖使用的時間。當且僅當超過一半(N/2 1,這里是5/2 1=3個節點)的Redis master節點都獲得鎖,并且使用的時間小于鎖失效時間時,鎖才算獲取成功。(如上圖,10s> 30ms 40ms 50ms 4m0s 50ms)
  4. 如果取到了鎖,key的真正有效時間就變啦,需要減去獲取鎖所使用的時間。
  5. 如果獲取鎖失敗(沒有在至少N/2 1個master實例取到鎖,有或者獲取鎖時間已經超過了有效時間),客戶端要在所有的master節點上解鎖(即便有些master節點根本就沒有加鎖成功,也需要解鎖,以防止有些漏網之魚)。

簡化下步驟就是:

  • 按順序向5個master節點請求加鎖
  • 根據設置的超時時間來判斷,是不是要跳過該master節點。
  • 如果大于等于3個節點加鎖成功,并且使用的時間小于鎖的有效期,即可認定加鎖成功啦。
  • 如果獲取鎖失敗,解鎖!

Redisson實現了redLock版本的鎖,有興趣的小伙伴,可以去了解一下哈~

4. Zookeeper分布式鎖

在學習Zookeeper分布式鎖之前,我們復習一下Zookeeper的節點哈。

Zookeeper的節點Znode有四種類型:

  • 持久節點:默認的節點類型。創建節點的客戶端與zookeeper斷開連接后,該節點依舊存在。
  • 持久節點順序節點:所謂順序節點,就是在創建節點時,Zookeeper根據創建的時間順序給該節點名稱進行編號,持久節點順序節點就是有順序的持久節點。
  • 臨時節點:和持久節點相反,當創建節點的客戶端與zookeeper斷開連接后,臨時節點會被刪除。
  • 臨時順序節點:有順序的臨時節點。

Zookeeper分布式鎖實現應用了臨時順序節點。這里不貼代碼啦,來講下zk分布式鎖的實現原理吧。

4.1 zk獲取鎖過程

當第一個客戶端請求過來時,Zookeeper客戶端會創建一個持久節點locks。如果它(Client1)想獲得鎖,需要在locks節點下創建一個順序節點lock1.如圖

分布式鎖的幾種典型實現(聊聊分布式鎖的多種實現)(5)

接著,客戶端Client1會查找locks下面的所有臨時順序子節點,判斷自己的節點lock1是不是排序最小的那一個,如果是,則成功獲得鎖。

分布式鎖的幾種典型實現(聊聊分布式鎖的多種實現)(6)

這時候如果又來一個客戶端client2前來嘗試獲得鎖,它會在locks下再創建一個臨時節點lock2

分布式鎖的幾種典型實現(聊聊分布式鎖的多種實現)(7)

客戶端client2一樣也會查找locks下面的所有臨時順序子節點,判斷自己的節點lock2是不是最小的,此時,發現lock1才是最小的,于是獲取鎖失敗。獲取鎖失敗,它是不會甘心的,client2向它排序靠前的節點lock1注冊Watcher事件,用來監聽lock1是否存在,也就是說client2搶鎖失敗進入等待狀態。

分布式鎖的幾種典型實現(聊聊分布式鎖的多種實現)(8)

此時,如果再來一個客戶端Client3來嘗試獲取鎖,它會在locks下再創建一個臨時節點lock3

分布式鎖的幾種典型實現(聊聊分布式鎖的多種實現)(9)

同樣的,client3一樣也會查找locks下面的所有臨時順序子節點,判斷自己的節點lock3是不是最小的,發現自己不是最小的,就獲取鎖失敗。它也是不會甘心的,它會向在它前面的節點lock2注冊Watcher事件,以監聽lock2節點是否存在。

分布式鎖的幾種典型實現(聊聊分布式鎖的多種實現)(10)

4.2 釋放鎖

我們再來看看釋放鎖的流程,Zookeeper的客戶端業務完成或者發生故障,都會刪除臨時節點,釋放鎖。如果是任務完成,Client1會顯式調用刪除lock1的指令

分布式鎖的幾種典型實現(聊聊分布式鎖的多種實現)(11)

如果是客戶端故障了,根據臨時節點得特性,lock1是會自動刪除的

分布式鎖的幾種典型實現(聊聊分布式鎖的多種實現)(12)

lock1節點被刪除后,Client2可開心了,因為它一直監聽著lock1。lock1節點刪除,Client2立刻收到通知,也會查找locks下面的所有臨時順序子節點,發下lock2是最小,就獲得鎖。

分布式鎖的幾種典型實現(聊聊分布式鎖的多種實現)(13)

同理,Client2獲得鎖之后,Client3也對它虎視眈眈,啊哈哈~

  • Zookeeper設計定位就是分布式協調,簡單易用。如果獲取不到鎖,只需添加一個監聽器即可,很適合做分布式鎖。
  • Zookeeper作為分布式鎖也缺點:如果有很多的客戶端頻繁的申請加鎖、釋放鎖,對于Zookeeper集群的壓力會比較大。
5. 三種分布式鎖對比

5.1 數據庫分布式鎖實現

優點:

  • 簡單,使用方便,不需要引入Redis、zookeeper等中間件。

缺點:

  • 不適合高并發的場景
  • db操作性能較差,有鎖表的風險;
5.2 Redis分布式鎖實現

優點:

  • 性能好,適合高并發場景
  • 較輕量級
  • 有較好的框架支持,如Redisson

缺點:

  • 過期時間不好控制
  • 需要考慮鎖被別的線程誤刪場景
5.3 Zookeeper分布式鎖實現

缺點:

  • 性能不如redis實現的分布式鎖
  • 比較重的分布式鎖。

優點:

  • 有較好的性能和可靠性
  • 有封裝較好的框架,如Curator
5.4 對比匯總
  • 從性能角度(從高到低)Redis > Zookeeper >= 數據庫;
  • 從理解的難易程度角度(從低到高)數據庫 > Redis > Zookeeper;
  • 從實現的復雜性角度(從低到高)Zookeeper > Redis > 數據庫;
  • 從可靠性角度(從高到低)Zookeeper > Redis > 數據庫。
最后(求關注,別白嫖我)

如果這篇文章對您有所幫助,或者有所啟發的話,求一鍵三連:點贊、轉發、在看,您的支持是我堅持寫作最大的動力。

來源:https://mp.weixin.qq.com/s/xQknd6xsVDPBr4TbETTk2A

歡迎分享轉載→http://www.avcorse.com/read-222101.html

Copyright ? 2024 有趣生活 All Rights Reserve吉ICP備19000289號-5 TXT地圖HTML地圖XML地圖