浅谈分布式锁
概述
為了防止分布式系統(tǒng)中的多個進程之間相互干擾,我們需要一種分布式協(xié)調(diào)技術(shù)來對這些進程進行調(diào)度。而這個分布式協(xié)調(diào)技術(shù)的核心就是來實現(xiàn)這個分布式鎖。
為什么要使用分布式鎖
成員變量 A 存在 JVM1、JVM2、JVM3 三個 JVM 內(nèi)存中
成員變量 A 同時都會在 JVM 分配一塊內(nèi)存,三個請求發(fā)過來同時對這個變量操作,顯然結(jié)果是不對的
不是同時發(fā)過來,三個請求分別操作三個不同 JVM 內(nèi)存區(qū)域的數(shù)據(jù),變量 A 之間不存在共享,也不具有可見性,處理的結(jié)果也是不對的
注:該成員變量 A 是一個有狀態(tài)的對象
如果我們業(yè)務(wù)中確實存在這個場景的話,我們就需要一種方法解決這個問題,這就是分布式鎖要解決的問題
分布式鎖應(yīng)該具有哪些特點
在分布式系統(tǒng)環(huán)境下,一個方法在同一時間只能被一個機器的一個線程執(zhí)行;
高可用的獲取鎖與釋放鎖;
高性能的獲取鎖與釋放鎖;
具備可重入特性;
具備鎖失效機制,防止死鎖;
具備非阻塞鎖特性,即沒有獲取到鎖將直接返回獲取鎖失敗。
實現(xiàn)分布式鎖的N種方式
接下來,介紹三種常用的方式
1.基于Mysql實現(xiàn)分布式鎖
表結(jié)構(gòu)
-- auto-generated definition create table distributed_lock (id int auto_incrementprimary key,method_name varchar(100) not null comment '獲取鎖的方法名',remark varchar(100) null comment '備注信息',status int not null comment '分配狀態(tài): 1-未分配,2-已分配',update_time datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP,version int not null comment '版本號',constraint uidx_method_nameunique (method_name) )comment '分布式鎖表';先獲取鎖的信息
select id, method_name, status,version from distributed_lock where status=1 and method_name='methodName';```### 占有鎖update t_resoure set status=2, version=2, update_time=now() where method_name=‘methodName’ and status=1 and version=2;```
如果沒有更新影響到一行數(shù)據(jù),則說明這個資源已經(jīng)被別人占位了。
以上也是cap理論的悲觀鎖,在數(shù)據(jù)庫上面的應(yīng)用。
缺點:
1、這把鎖強依賴數(shù)據(jù)庫的可用性,數(shù)據(jù)庫是一個單點,一旦數(shù)據(jù)庫掛掉,會導(dǎo)致業(yè)務(wù)系統(tǒng)不可用。
2、這把鎖沒有失效時間,一旦解鎖操作失敗,就會導(dǎo)致鎖記錄一直在數(shù)據(jù)庫中,其他線程無法再獲得到鎖。
3、這把鎖只能是非阻塞的,因為數(shù)據(jù)的insert操作,一旦插入失敗就會直接報錯。沒有獲得鎖的線程并不會進入排隊隊列,要想再次獲得鎖就要再次觸發(fā)獲得鎖操作。
4、這把鎖是非重入的,同一個線程在沒有釋放鎖之前無法再次獲得該鎖。因為數(shù)據(jù)中數(shù)據(jù)已經(jīng)存在了。
解決方案:
數(shù)據(jù)庫是單點?搞兩個數(shù)據(jù)庫,數(shù)據(jù)之前雙向同步。一旦掛掉快速切換到備庫上。
沒有失效時間?只要做一個定時任務(wù),每隔一定時間把數(shù)據(jù)庫中的超時數(shù)據(jù)清理一遍。
非阻塞的?搞一個while循環(huán),直到insert成功再返回成功。
非重入的?在數(shù)據(jù)庫表中加個字段,記錄當前獲得鎖的機器的主機信息和線程信息,那么下次再獲取鎖的時候先查詢數(shù)據(jù)庫,如果當前機器的主機信息和線程信息在數(shù)據(jù)庫可以查到的話,直接把鎖分配給他就可以了。
2.基于Redis實現(xiàn)分布式鎖
首先要弄清楚幾個redis命令的概念
setnx()
setnx 的含義就是 SET if Not Exists,其主要有兩個參數(shù) setnx(key, value)。該方法是原子的,如果 key 不存在,則設(shè)置當前 key 成功,返回 1;如果當前 key 已經(jīng)存在,則設(shè)置當前 key 失敗,返回 0。
expire()
expire 設(shè)置過期時間,要注意的是 setnx 命令不能設(shè)置 key 的超時時間,只能通過 expire() 來對 key 設(shè)置。
getset()
這個命令主要有兩個參數(shù) getset(key,newValue)。該方法是原子的,對 key 設(shè)置 newValue 這個值,并且返回 key 原來的舊值。假設(shè) key 原來是不存在的,那么多次執(zhí)行這個命令,會出現(xiàn)下邊的效果:
getset(key, “value1”) 返回 null 此時 key 的值會被設(shè)置為 value1
getset(key, “value2”) 返回 value1 此時 key 的值會被設(shè)置為 value2
依次類推!
使用步驟:
1.setnx(lockkey, 當前時間 過期超時時間),如果返回 1,則獲取鎖成功;如果返回 0 則沒有獲取到鎖,轉(zhuǎn)向 2。
2.get(lockkey) 獲取值 oldExpireTime ,并將這個 value 值與當前的系統(tǒng)時間進行比較,如果小于當前系統(tǒng)時間,則認為這個鎖已經(jīng)超時,可以允許別的請求重新獲取,轉(zhuǎn)向 3。
3.計算 newExpireTime = 當前時間 過期超時時間,然后 getset(lockkey, newExpireTime) 會返回當前 lockkey 的值currentExpireTime。
4.判斷 currentExpireTime 與 oldExpireTime 是否相等,如果相等,說明當前 getset 設(shè)置成功,獲取到了鎖。如果不相等,說明這個鎖又被別的請求獲取走了,那么當前請求可以直接返回失敗,或者繼續(xù)重試。
5.在獲取到鎖之后,當前線程可以開始自己的業(yè)務(wù)處理,當處理完畢后,比較自己的處理時間和對于鎖設(shè)置的超時時間,如果小于鎖設(shè)置的超時時間,則直接執(zhí)行 delete 釋放鎖;如果大于鎖設(shè)置的超時時間,則不需要再鎖進行處理。
話不多說,上代碼:
public final class RedisLockUtil { ?private static final int defaultExpire = 60; ?private RedisLockUtil() {//} ?/*** 加鎖* @param key redis key* @param expire 過期時間,單位秒* @return true:加鎖成功,false,加鎖失敗*/public static boolean lock(String key, int expire) { ?RedisService redisService = SpringUtils.getBean(RedisService.class);long status = redisService.setnx(key, "1"); ?if(status == 1) {redisService.expire(key, expire);return true;} ?return false;} ?public static boolean lock(String key) {return lock2(key, defaultExpire);} ?/*** 加鎖* @param key redis key* @param expire 過期時間,單位秒* @return true:加鎖成功,false,加鎖失敗*/public static boolean lock2(String key, int expire) { ?RedisService redisService = SpringUtils.getBean(RedisService.class); ?long value = System.currentTimeMillis() expire;long status = redisService.setnx(key, String.valueOf(value)); ?if(status == 1) {return true;}long oldExpireTime = Long.parseLong(redisService.get(key, "0"));if(oldExpireTime < System.currentTimeMillis()) {//超時long newExpireTime = System.currentTimeMillis() expire;long currentExpireTime = Long.parseLong(redisService.getSet(key, String.valueOf(newExpireTime)));if(currentExpireTime == oldExpireTime) {return true;}}return false;} ?public static void unLock1(String key) {RedisService redisService = SpringUtils.getBean(RedisService.class);redisService.del(key);} ?public static void unLock2(String key) { RedisService redisService = SpringUtils.getBean(RedisService.class); long oldExpireTime = Long.parseLong(redisService.get(key, "0")); if(oldExpireTime > System.currentTimeMillis()) { redisService.del(key); }} }3.基于Zookeeper實現(xiàn)分布式鎖
首先,我們先來看看zookeeper的相關(guān)知識。
zk 一般由多個節(jié)點構(gòu)成(單數(shù)),采用 zab 一致性協(xié)議。因此可以將 zk 看成一個單點結(jié)構(gòu),對其修改數(shù)據(jù)其內(nèi)部自動將所有節(jié)點數(shù)據(jù)進行修改而后才提供查詢服務(wù)。
zk 的數(shù)據(jù)以目錄樹的形式,每個目錄稱為 znode, znode 中可存儲數(shù)據(jù)(一般不超過 1M),還可以在其中增加子節(jié)點。
子節(jié)點有三種類型。序列化節(jié)點,每在該節(jié)點下增加一個節(jié)點自動給該節(jié)點的名稱上自增。臨時節(jié)點,一旦創(chuàng)建這個 znode 的客戶端與服務(wù)器失去聯(lián)系,這個 znode 也將自動刪除。最后就是普通節(jié)點。
Watch 機制,client 可以監(jiān)控每個節(jié)點的變化,當產(chǎn)生變化會給 client 產(chǎn)生一個事件。
zookeeper基本鎖:
**原理:**利用臨時節(jié)點與 watch 機制。每個鎖占用一個普通節(jié)點 /lock,當需要獲取鎖時在 /lock 目錄下創(chuàng)建一個臨時節(jié)點,創(chuàng)建成功則表示獲取鎖成功,失敗則 watch/lock 節(jié)點,有刪除操作后再去爭鎖。臨時節(jié)點好處在于當進程掛掉后能自動上鎖的節(jié)點自動刪除即取消鎖。
**缺點:**所有取鎖失敗的進程都監(jiān)聽父節(jié)點,很容易發(fā)生羊群效應(yīng),即當釋放鎖后所有等待進程一起來創(chuàng)建節(jié)點,并發(fā)量很大。
zookeeper分布式鎖實現(xiàn):
**原理:**上鎖改為創(chuàng)建臨時有序節(jié)點,每個上鎖的節(jié)點均能創(chuàng)建節(jié)點成功,只是其序號不同。只有序號最小的可以擁有鎖,如果這個節(jié)點序號不是最小的則 watch 序號比本身小的前一個節(jié)點 (公平鎖)。
步驟:
在 /lock 節(jié)點下創(chuàng)建一個有序臨時節(jié)點 (EPHEMERAL_SEQUENTIAL)。
判斷創(chuàng)建的節(jié)點序號是否最小,如果是最小則獲取鎖成功。不是則取鎖失敗,然后 watch 序號比本身小的前一個節(jié)點。
當取鎖失敗,設(shè)置 watch 后則等待 watch 事件到來后,再次判斷是否序號最小。
取鎖成功則執(zhí)行代碼,最后釋放鎖(刪除該節(jié)點)。
代碼如下:
public final class RedisLockUtil { ?private static final int defaultExpire = 60; ?private RedisLockUtil() {//} ?/*** 加鎖* @param key redis key* @param expire 過期時間,單位秒* @return true:加鎖成功,false,加鎖失敗*/public static boolean lock(String key, int expire) { ?RedisService redisService = SpringUtils.getBean(RedisService.class);long status = redisService.setnx(key, "1"); ?if(status == 1) {redisService.expire(key, expire);return true;} ?return false;} ?public static boolean lock(String key) {return lock2(key, defaultExpire);} ?/*** 加鎖* @param key redis key* @param expire 過期時間,單位秒* @return true:加鎖成功,false,加鎖失敗*/public static boolean lock2(String key, int expire) { ?RedisService redisService = SpringUtils.getBean(RedisService.class); ?long value = System.currentTimeMillis() expire;long status = redisService.setnx(key, String.valueOf(value)); ?if(status == 1) {return true;}long oldExpireTime = Long.parseLong(redisService.get(key, "0"));if(oldExpireTime < System.currentTimeMillis()) {//超時long newExpireTime = System.currentTimeMillis() expire;long currentExpireTime = Long.parseLong(redisService.getSet(key, String.valueOf(newExpireTime)));if(currentExpireTime == oldExpireTime) {return true;}}return false;} ?public static void unLock1(String key) {RedisService redisService = SpringUtils.getBean(RedisService.class);redisService.del(key);} ?public static void unLock2(String key) { RedisService redisService = SpringUtils.getBean(RedisService.class); long oldExpireTime = Long.parseLong(redisService.get(key, "0")); if(oldExpireTime > System.currentTimeMillis()) { redisService.del(key); }} }優(yōu)缺點:
優(yōu)點:
有效的解決單點問題,不可重入問題,非阻塞問題以及鎖無法釋放的問題。實現(xiàn)起來較為簡單。
缺點:
性能上可能并沒有緩存服務(wù)那么高,因為每次在創(chuàng)建鎖和釋放鎖的過程中,都要動態(tài)創(chuàng)建、銷毀臨時節(jié)點來實現(xiàn)鎖功能。ZK 中創(chuàng)建和刪除節(jié)點只能通過 Leader 服務(wù)器來執(zhí)行,然后將數(shù)據(jù)同步到所有的 Follower 機器上。還需要對 ZK的原理有所了解。
三種方案的比較
上面幾種方式,哪種方式都無法做到完美。就像CAP一樣,在復(fù)雜性、可靠性、性能等方面無法同時滿足,所以,根據(jù)不同的應(yīng)用場景選擇最適合自己的才是王道。
從理解的難易程度角度(從低到高)
數(shù)據(jù)庫 > 緩存 > Zookeeper
從實現(xiàn)的復(fù)雜性角度(從低到高)
Zookeeper >= 緩存 > 數(shù)據(jù)庫
從性能角度(從高到低)
緩存 > Zookeeper >= 數(shù)據(jù)庫
從可靠性角度(從高到低)
Zookeeper > 緩存 > 數(shù)據(jù)庫
獲取更多互聯(lián)網(wǎng)知識請關(guān)注公眾號:
總結(jié)
- 上一篇: 面试必备:CAS无锁机制
- 下一篇: 鸡啄米MFC教程笔记之七:对话框:为控件