分布式鎖
為什么要用分布式鎖?
在分布式場景下多個客戶端同時獲取一把鎖,為了保證只有一個客戶端能獲取到這把鎖,分布式鎖誕生了,而分布式鎖的誕生就是為了解決數(shù)據(jù)的最終一致性.在分布式系統(tǒng)中有一個著名的原理叫做CAP原理:其中Consistency(一致性)、 Availability(可用性)、Partition tolerance(分區(qū)容錯性),它描述的是在分布式系統(tǒng)中同時最多只能滿足其中兩個條件,如果你想實現(xiàn)強一致性你就需要犧牲高性能.如果服務是單點,那其實本質(zhì)上就不存在分布式鎖這個概念.
本文接下來將探討實現(xiàn)分布式鎖的幾種解決方案,并做下對比
1. 說明
??關(guān)于分布式鎖這塊,其實沒有辦法做到100%的絕對安全性
2. 背景
??在很多秒殺系統(tǒng)中、生成全局唯一遞增id、支付、任務分配等場景都需要用到分布式鎖
3. 數(shù)據(jù)庫樂觀鎖
??基于數(shù)據(jù)庫樂觀鎖實現(xiàn)分布式鎖,講到樂觀鎖順便講一下悲觀鎖
悲觀鎖:悲觀鎖認為所有的操作都是不安全的,所以操作之前它先上鎖,比如行鎖、表鎖都是基于悲觀鎖原理實現(xiàn)的
樂觀鎖:樂觀鎖和悲觀鎖不同的是,只在數(shù)據(jù)更新的做比較,查詢階段不加鎖,所以性能上要快一些
樂觀鎖的實現(xiàn)大多是基于數(shù)據(jù)庫的版本號機制去實現(xiàn)的,簡單的來說就是,即為數(shù)據(jù)增加一個版本標識,在基于數(shù)據(jù)庫表的版本解決方案中,一般是通過為數(shù)據(jù)庫表添加一個 “version”字段來實現(xiàn)讀取出數(shù)據(jù)時,將此版本號一同讀出,之后更新時,對此版本號加1,其中id是唯一建.
假設(shè)數(shù)據(jù)庫有這樣一條記錄:
| id | name | version |
|---|---|---|
| 1 | jack | 100 |
大致的過程就是:
這個時候假設(shè)有兩個線程(c1,c2)同時要對id為1的記錄進行修改
1)先查詢出來id為1的記錄
2)那么兩個線程查詢出來的結(jié)果都是一樣的,c1想把id為1的name修改成tom,c2想把id為1的name修改成robin
3)c1在更新的時候,c2也在更新記錄,假設(shè)c2后更新,更新的時候兩者都帶上最近一次查詢的版本號
4)如果發(fā)現(xiàn)更新失敗,重試,說明此記錄已經(jīng)被修改
這個實現(xiàn)看看就行了
4. 基于Redis分布式鎖實現(xiàn)
??redis分布式鎖是目前很多大公司采取的技術(shù)解決方案,基于Redis單線程模式,采用隊列模式,隊列本是就是先進后出的模型,將并發(fā)訪問轉(zhuǎn)變成串行訪問,這樣就避免了資源的有序性和競爭關(guān)系.
早期的時候我們用的比較多的是setNX命令,做一些簡單的操作,偽代碼如下:
long result= JedisUtil.setnx(key,value);
if(result>0) {
JedisUtil.expire(key,locktime);
return true;
}
long expire=Jedisutil.ttl(key);
if(expire<0){
if(expire==-1{
JedisUtil.remove(key);
....
}
}
這段代碼看似沒有什么毛病,一般情況下也確實問題不大,但是實現(xiàn)上并不是安全的.
1)第一種情況:執(zhí)行expire命令時候發(fā)生了網(wǎng)絡抖動,執(zhí)行超時或者失敗了,如果這把鎖不釋放,它將會成為死鎖.(我的想法比較~發(fā)生的概率比較小)
2)第二種情況:因為現(xiàn)在的操作是非原子性的,那么理論上說你的操作都可以被“打斷”,比如你的redis部署采用哨兵集群模式,簡單了來說就是HA高可用,主從模式,其實嚴格來說就不是集群了,如果主節(jié)點掛了,由于是主從異步復制的,這個時候從還沒同步過去,就導致主有這個key而從沒有key,被提升為主的從節(jié)點會導致在某個時刻,會有多個客戶端同時持有該key.這個在后續(xù)的redlock和redission中都有解決.
3)第三種情況:超時時間如果控制的不但,會出現(xiàn),任務還沒執(zhí)行完,到了超時時間,這個時候鎖自動釋放了,這個就尷尬了,此時你就要解決自動續(xù)租的問題了
4)效率比較低,強依賴于失效時間,需要一直去輪詢鎖是否失效
redis官方推薦我們配合lua腳本去解決原子性問題:
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(fullKey), Collections.singletonList(value));
if (Objects.equals(UNLOCK_SUCCESS, result)) {
flag = true;
}
redission實現(xiàn)分布式鎖
樓主比較推薦: 因為簡單易用,功能強大
相比Jedis而言可伸縮性性更高,jedis使用的是阻塞IO,而Redisson使用非阻塞的I/O和基于Netty框架的事件驅(qū)動的通信層,而且Redisson的API是線程安全的,這個就比較討人喜歡了.
支持更豐富的數(shù)據(jù)結(jié)構(gòu)比如:BitSet, Set, Multimap, SortedSet, Map, List, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, AtomicLong, CountDownLatch, Publish / Subscribe, Bloom filter, Remote service, Spring cache, Executor service, Live Object service, Scheduler service
引入maven依賴:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>2.2.13</version>
</dependency>
代碼實現(xiàn):
RLock lock = redisson.getLock("lockName");
try{
// 嘗試加鎖,最多等待2秒,上鎖以后8秒自動解鎖
boolean res = lock.tryLock(2, 8, TimeUnit.SECONDS);
if(res){ //成功
//處理業(yè)務
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//釋放鎖
lock.unlock();
}
5. 基于zk開源客戶端Curator實現(xiàn)分布式鎖
Curator內(nèi)部是通過InterProcessMutex(可重入鎖)來在zookeeper中創(chuàng)建臨時有序節(jié)點實現(xiàn)的.基于zab協(xié)議保證鎖的數(shù)據(jù)安全性,基于ping的心跳?;顧C制解決死鎖問題,并通過watch機制能及時喚醒被阻塞的狀態(tài)
1). 基于Zk名稱唯一性
利用名稱唯一性,加鎖操作就是建立一個znode節(jié)點,釋放鎖就是刪除這個目錄,操作簡單,ZAB一致性協(xié)議保證了鎖的數(shù)據(jù)安全性。缺點就是會產(chǎn)生著名的“羊群”效應,N個客戶端在等待一把鎖時,鎖釋放時候所有客戶端都被喚醒,而僅僅只有一個客戶端才能得到鎖。
2). 基于臨時有序順序節(jié)點
首先建立一個節(jié)點;
當進程訪問資源時,獲得鎖,創(chuàng)建znode節(jié)點下順序節(jié)點;
對znode節(jié)點下子節(jié)點排序,序號最小的獲得鎖;后面的節(jié)點獲得上一順序節(jié)點,并注冊監(jiān)聽事件,等待監(jiān)聽事件,獲得鎖;資源用完后釋放資源,調(diào)用unlock,去關(guān)掉zk
InterProcessMutex lock = new InterProcessMutex(client, lockPath);
if ( lock.acquire(maxWait, waitUnit) ) {
try
{
} finally {
lock.release();
}
}
基于zk實現(xiàn)的分布式鎖,安全可靠,也不需要考慮沒有設(shè)置失效時間的問題,因為zk會自動刪除znode節(jié)點目錄,但是zk相對redis效率比較低,因為他會頻繁的創(chuàng)建和銷毀節(jié)點,性能上不是很好,所以不太適合并發(fā)比較高的業(yè)務場景.