<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
今天跟大家探討一下分散式鎖的設計與實現。
我們的系統都是分散式部署的,日常開發中,秒殺下單、搶購商品等等業務場景,為了防⽌庫存超賣,都需要用到分散式鎖。
分散式鎖其實就是,控制分散式系統不同程序共同存取共用資源的一種鎖的實現。如果不同的系統或同一個系統的不同主機之間共用了某個臨界資源,往往需要互斥來防止彼此干擾,以保證一致性。
業界流行的分散式鎖實現,一般有這3種方式:
可以使用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
那個記錄,如果為空,則可以插入一條記錄,如果已有記錄判斷下狀態和時間,是否已經超時。這裡需要注意一下哈,必須要加事務哈。
除了悲觀鎖,還可以用樂觀鎖實現分散式鎖。樂觀鎖,顧名思義,就是很樂觀,每次更新操作,都覺得不會存在並行衝突,只有更新失敗後,才重試。它是基於CAS思想實現的。我以前的公司,扣減餘額就是用這種方案。
搞個version欄位,每次更新修改,都會自增加一,然後去更新餘額時,把查出來的那個版本號,帶上條件去更新,如果是上次那個版本號,就更新,如果不是,表示別人並行修改過了,就繼續重試。
大概流程如下:
查詢版本號和餘額
select version,balance from account where user_id ='666';
假設查到版本號是oldVersion=1.
邏輯處理,判斷餘額
if(balance<扣減金額){ return; } left_balance = balance - 扣減金額;
進行扣減餘額
update account set balance = #{left_balance} ,version = version+1 where version = #{oldVersion} and balance>= #{left_balance} and user_id ='666';
大家可以看下這個流程圖哈:
這種方式適合並行不高的場景,一般需要設定一下重試的次數
Redis分散式鎖一般有以下這幾種實現方式:
聊到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
掉或者要重啟維護了,那這個鎖就長生不老了,別的執行緒永遠獲取不到鎖啦,所以分散式鎖不能這麼實現!
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()
,最終只能有一個使用者端加鎖成功,但是該使用者端鎖的過期時間,可能被別的使用者端覆蓋。這個命令的幾個引數分別表示什麼意思呢?跟大家複習一下:
SET key value [EX seconds] [PX milliseconds] [NX|XX]
second
秒。millisecond
毫秒。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的鎖給釋放了。
為了解決鎖被別的執行緒誤刪問題。可以在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 0 end;
這種方式比較不錯了,一般情況下,已經可以使用這種實現方式。但是還是存在:鎖過期釋放了,業務還沒執行完的問題。
對於可能存在鎖過期釋放,業務沒執行完的問題。我們可以稍微把鎖過期時間設定長一些,大於正常業務處理時間就好啦。如果你覺得不是很穩,還可以給獲得鎖的執行緒,開啟一個定時守護執行緒,每隔一段時間檢查鎖是否還存在,存在則對鎖的過期時間延長,防止鎖過期提前釋放。
當前開源框架Redisson解決了這個問題。可以看下Redisson底層原理圖:
只要執行緒一加鎖成功,就會啟動一個watch dog
看門狗,它是一個後臺執行緒,會每隔10秒檢查一下,如果執行緒1還持有鎖,那麼就會不斷的延長鎖key的生存時間。因此,Redisson就是使用watch dog
解決了鎖過期釋放,業務沒執行完問題。
前面六種方案都只是基於Redis單機版的分散式鎖討論,還不是很完美。因為Redis一般都是叢集部署的:
如果執行緒一在Redis
的master
節點上拿到了鎖,但是加鎖的key
還沒同步到slave
節點。恰好這時,master
節點發生故障,一個slave
節點就會升級為master
節點。執行緒二就可以順理成章獲取同個key
的鎖啦,但執行緒一也已經拿到鎖了,鎖的安全性就沒了。
為了解決這個問題,Redis作者antirez提出一種高階的分散式鎖演演算法:Redlock。它的核心思想是這樣的:
部署多個Redis master,以保證它們不會同時宕掉。並且這些master節點是完全相互獨立的,相互之間不存在資料同步。同時,需要確保在這多個master範例上,是與在Redis單範例,使用相同方法來獲取和釋放鎖。
我們假設當前有5個Redis master節點,在5臺伺服器上面執行這些Redis範例。
RedLock的實現步驟:
簡化下步驟就是:
Redisson實現了redLock版本的鎖,有興趣的小夥伴,可以去了解一下哈~
在學習Zookeeper分散式鎖之前,我們複習一下Zookeeper的節點哈。
Zookeeper的節點Znode有四種型別:
Zookeeper分散式鎖實現應用了臨時順序節點。這裡不貼程式碼啦,來講下zk分散式鎖的實現原理吧。
當第一個使用者端請求過來時,Zookeeper使用者端會建立一個持久節點locks
。如果它(Client1)想獲得鎖,需要在locks
節點下建立一個順序節點lock1
.如圖
接著,使用者端Client1會查詢locks
下面的所有臨時順序子節點,判斷自己的節點lock1
是不是排序最小的那一個,如果是,則成功獲得鎖。
這時候如果又來一個使用者端client2前來嘗試獲得鎖,它會在locks下再建立一個臨時節點lock2
使用者端client2一樣也會查詢locks下面的所有臨時順序子節點,判斷自己的節點lock2是不是最小的,此時,發現lock1才是最小的,於是獲取鎖失敗。獲取鎖失敗,它是不會甘心的,client2向它排序靠前的節點lock1註冊Watcher事件,用來監聽lock1是否存在,也就是說client2搶鎖失敗進入等待狀態。
此時,如果再來一個使用者端Client3來嘗試獲取鎖,它會在locks下再建立一個臨時節點lock3
同樣的,client3一樣也會查詢locks下面的所有臨時順序子節點,判斷自己的節點lock3是不是最小的,發現自己不是最小的,就獲取鎖失敗。它也是不會甘心的,它會向在它前面的節點lock2註冊Watcher事件,以監聽lock2節點是否存在。
我們再來看看釋放鎖的流程,Zookeeper的使用者端業務完成或者發生故障,都會刪除臨時節點,釋放鎖。如果是任務完成,Client1會顯式呼叫刪除lock1的指令
如果是使用者端故障了,根據臨時節點得特性,lock1是會自動刪除的
lock1節點被刪除後,Client2可開心了,因為它一直監聽著lock1。lock1節點刪除,Client2立刻收到通知,也會查詢locks下面的所有臨時順序子節點,發下lock2是最小,就獲得鎖。
同理,Client2獲得鎖之後,Client3也對它虎視眈眈,啊哈哈~
優點:
簡單,使用方便,不需要引入Redis、zookeeper
等中介軟體。
缺點:
優點:
缺點:
缺點:
優點:
到此這篇關於SpringCloud 分散式鎖的多種實現的文章就介紹到這了,更多相關SpringCloud 分散式鎖 內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45