帶你輕松掌握Redis分布式鎖
目前很多大型網(wǎng)站及應(yīng)用都是分布式部署的,分布式場景中的數(shù)據(jù)一致性問題一直是一個比較重要的話題。
基于 CAP理論,任何一個分布式系統(tǒng)都無法同時滿足一致性(Consistency)、可用性(Availability)和分區(qū)容錯性(Partition tolerance),最多只能同時滿足兩項。
我們?yōu)榱吮WC數(shù)據(jù)的最終一致性,需要很多的技術(shù)方案來支持,比如分布式事務(wù)、分布式鎖等。通常大家都會采redis做分布式鎖,但這樣就可以高枕無憂了嗎?
1. 什么是分布式鎖
分布式與單機(jī)情況下最大的不同在于其不是多線程而是多進(jìn)程,而數(shù)據(jù)只有一份(或有限制),也就是說單機(jī)的共享內(nèi)存已解決不了一致性寫問題,此時需要利用鎖的技術(shù)控制某一時刻修改數(shù)據(jù)的進(jìn)程數(shù)。
當(dāng)在分布式模型下,分布式鎖還是可以將標(biāo)記存在內(nèi)存,只是該內(nèi)存不是某個進(jìn)程分配的內(nèi)存而是公共內(nèi)存(Redis、Memcache)。至于利用數(shù)據(jù)庫、文件等做鎖與單機(jī)的實現(xiàn)是一樣的,只要保證標(biāo)記能互斥就行。
2. 分布式鎖該具備的特性
- 最好是可重入鎖(避免死鎖)
- 最好是一把阻塞鎖(根據(jù)業(yè)務(wù)需求決定)
- 最好是一把公平鎖(根據(jù)業(yè)務(wù)需求決定)
- 有高可用、高性能的獲取鎖和釋放鎖功能
3. 基于數(shù)據(jù)庫做分布式鎖
- 基于樂觀鎖,CAS,但如果是insert的情況采用主鍵沖突防重,在大并發(fā)情況下有可能會造成鎖表現(xiàn)象
- 基于悲觀鎖,也就是排他鎖,會有各種各樣的問題(操作數(shù)據(jù)庫需要一定的開銷,使用數(shù)據(jù)庫的行級鎖并不一定靠譜,性能不靠譜)
如果按分布式該具備的特性來逐條匹配,特別是高可用(存在單點(diǎn))、高性能是硬傷
4. 基于Redis做分布式鎖
一般都使用 setnx(set if not exists) 指令,只允許被一個客戶端占有,先來先得, 用完后再通過 del 指令釋放。
如果中間邏輯執(zhí)行時發(fā)生異常,可能會導(dǎo)致 del 指令沒有被執(zhí)行,這樣就會陷入死鎖,怎么破?
對,給鎖加個過期時間(即使出現(xiàn)異常也可以保證幾秒之后鎖會自動釋放)!
但setnx 和 expire 之間redis服務(wù)器突然掛掉,怎么破?
其實該問題的根源就在于 setnx 和 expire 是兩條指令而不是原子指令。為了解決這個疑難,Redis 開源社區(qū)涌現(xiàn)了一堆分布式鎖的 解決方案。為了治理這個亂象,Redis 2.8 版本中加入了 set 指令的擴(kuò)展參數(shù),使得 setnx 和 expire 指令可以一起執(zhí)行,徹底解決了分布式鎖的亂象。
總之,setnx 和 expire 組合就是分布式鎖的奧義所在。
4.1 超時問題
如果在加鎖和釋放鎖之間的邏輯執(zhí)行的太長,超出了超時限制,怎么破?
也就是說第一個線程持有的鎖過期了但臨界區(qū)的邏輯還沒有執(zhí)行完,這個時候第二個線程就提前重新持有了這把鎖,導(dǎo)致每個請求執(zhí)行臨界區(qū)代碼時不能嚴(yán)格的串行執(zhí)行。
Redis 的分布式鎖不能解決超時問題,建議分布式鎖不要用于較長時間的任務(wù)。
稍微安全一點(diǎn)的方案是為 set 指令的 value 參數(shù)設(shè)置為一個隨機(jī)數(shù),釋放鎖時先匹配隨機(jī)數(shù)是否一致,一致的話再刪除 key,這是可以確保當(dāng)前線程占有的鎖不會被其它線程釋放,但是并不能解決鎖被redis服務(wù)器自動釋放的。
int tag = random.nextint()//隨機(jī)數(shù)
boolean nx=true;
int ex=5;
if(redis.set(key, tag, nx, ex)){
do_something()
redis.delifequals(key, tag)//不存在這樣的命令
}
但是匹配 value 和刪除 key 不是一個原子操作,怎么破?
需要使用 Lua 腳本來處理了,因為 Lua 腳本可以保證連續(xù)多個指令的原子性執(zhí)行。
#delifequals.lua文件,下面的是社區(qū)熱門代碼
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
//java調(diào)用
public void delifequals(){
String script = readScript("delifequals.lua");
int tag = 5;
String key = "key";
Object eval = jedis.eval(script, Lists.newArrayList(key), Lists.newArrayList(tag));
System.out.println(eval);
}
4.2 可重入鎖
redis有類似Java 語言里有個 ReentrantLock 就是可重入鎖嗎?
要支持可重入,需要對jedis 的 set 方法進(jìn)行包裝,思路是:使用 Threadlocal 存儲當(dāng)前持有鎖的計數(shù)??芍厝腈i加重了客戶端的復(fù)雜性,精確一點(diǎn)還需要考慮內(nèi)存鎖計數(shù)的過期時間,代碼復(fù)雜度將會繼續(xù)升高。
public class JedisWithReentrantLock {
private Jedis jedis;
/**
* 當(dāng)前線程的鎖及計數(shù)
*/
private ThreadLocal<Map<String, Integer>> lockers = new ThreadLocal<>();
public JedisWithReentrantLock(Jedis jedis) {
this.jedis = jedis;
}
private boolean set(String key) {
return jedis.set(key, "", "nx", "ex", 5L) != null;
}
private void del(String key) {
jedis.del(key);
}
private Map<String, Integer> getLockers() {
Map<String, Integer> refs = lockers.get();
if (refs != null) {
return refs;
}
lockers.set(Maps.newHashMap());
return lockers.get();
}
public boolean lock(String key) {
Map<String, Integer> refs = getLockers();
Integer refCount = refs.get(key);
if (refCount != null) {
refs.put(key, refCount + 1);
return true;
}
if (!this.set(key)) {
return false;
}
refs.put(key, 1);
return true;
}
public boolean unlock(String key) {
Map<String, Integer> refs = getLockers();
Integer refCount = refs.get(key);
if (refCount == null) {
return false;
}
refCount -= 1;
if (refCount > 0) {
refs.put(key, refCount);
} else {
refs.remove(key);
this.del(key);
}
return true;
}
}
@Test
public void runJedisWithReentrantLock() {
JedisWithReentrantLock redis = new JedisWithReentrantLock(jedis);
System.out.println(redis.lock("alex"));
System.out.println(redis.lock("alex"));
System.out.println(redis.unlock("alex"));
System.out.println(redis.unlock("alex"));
}
4.3 集群環(huán)境的缺陷
在集群環(huán)境下,這種方式是有缺陷的(數(shù)據(jù)不一致的情況)。比如在 Sentinel 集群中,主節(jié)點(diǎn)掛掉時(原先第一個客戶端在主節(jié)點(diǎn)中申請成功了一把鎖),從節(jié)點(diǎn)A 會取而代之并晉升為主(但是這把鎖還沒有來得及同步),雖然客戶端上卻并沒有明顯感知,但是這時另一個客戶端過來請求 從節(jié)點(diǎn)A 可以成功加鎖,這樣就會導(dǎo)致系統(tǒng)中同樣一把鎖被兩個客戶端同時持有。
主從發(fā)生故障轉(zhuǎn)移,一般持續(xù)時間極短,數(shù)據(jù)不一致的情況基本上都是小概率事件。
4.4 Redlock
上面的集群同步問題導(dǎo)致的缺陷,難道就沒有解決方案嗎?
為此Antirez 發(fā)明了 Redlock 算法,它的流程比較復(fù)雜,不過已經(jīng)有了很多開源的實現(xiàn)。
原理
使用 Redlock,需要提供多個 Redis 實例,這些實例之前相互獨(dú)立沒有主從關(guān)系。同很多分布式算法一樣,redlock 也使用少數(shù)服從多數(shù)。
加鎖時,它會向過半節(jié)點(diǎn)發(fā)送 set(key, value, nx, ex) 指令,只要過半節(jié)點(diǎn) set 成功,那就認(rèn)為加鎖成功。釋放鎖時,需要向所有節(jié)點(diǎn)發(fā)送 del 指令。缺陷:因為 Redlock 需要向多個節(jié)點(diǎn)進(jìn)行讀寫,意味著相比單實例 Redis 性能會下降一些。
注:Redlock算法還需要考慮出錯重試、時鐘漂移等很多細(xì)節(jié)問題
使用場景
如果你很在乎高可用性,希望掛了一臺 redis 完全不受影響,那就應(yīng)該考慮 redlock。
引用資料
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
Redis常用的數(shù)據(jù)結(jié)構(gòu)及實際應(yīng)用場景
本文介紹了Redis中常用的數(shù)據(jù)結(jié)構(gòu),包括字符串、列表、集合、哈希表、有序集合和Bitmap,并詳細(xì)說明了它們在各種場景下的使用,需要的朋友可以參考下2024-05-05
SpringSession+Redis實現(xiàn)集群會話共享的方法
為了保證WEB應(yīng)用的承載能力, 需要對WEB應(yīng)用進(jìn)行集群處理.這篇文章主要介紹了SpringSession+Redis實現(xiàn)集群會話共享的方法,需要的朋友參考下吧2018-08-08
Redis中統(tǒng)計各種數(shù)據(jù)大小的方法
這篇文章主要介紹了Redis中統(tǒng)計各種數(shù)據(jù)大小的方法,本文使用PHP實現(xiàn)統(tǒng)計Redis內(nèi)存占用比較大的鍵,需要的朋友可以參考下2015-03-03
spring?boot整合redis中間件與熱部署實現(xiàn)代碼
spring?boot整合redis最常用的有三個工具庫Jedis,Redisson,Lettuce,本文重點(diǎn)介紹spring?boot整合redis中間件與熱部署實現(xiàn),需要的朋友可以參考下2023-01-01
基于redis實現(xiàn)世界杯排行榜功能項目實戰(zhàn)
前段時間,做了一個世界杯競猜積分排行榜。對世界杯64場球賽勝負(fù)平進(jìn)行猜測,猜對+1分,錯誤+0分,一人一場只能猜一次。下面通過本文給大家分享基于redis實現(xiàn)世界杯排行榜功能項目實戰(zhàn),感興趣的朋友一起看看吧2018-10-10
關(guān)于Redis數(shù)據(jù)庫入門詳細(xì)介紹
大家好,本篇文章主要講的是關(guān)于Redis數(shù)據(jù)庫入門詳細(xì)介紹,感興趣的同學(xué)趕快來看一看吧,對你有幫助的話記得收藏一下,方便下次瀏覽2021-12-12

