自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Redis分布式鎖的前世今生

數(shù)據(jù)庫(kù) Redis
redisson是redis官方推薦的一個(gè)分布式鎖的框架,它幫我們解決了上面提到的所有問(wèn)題,底層也是用了lua腳本實(shí)現(xiàn),同時(shí)又提供了watchdog(看門(mén)狗)機(jī)制,在鎖將要過(guò)期的時(shí)候,會(huì)自動(dòng)檢測(cè)業(yè)務(wù)是否執(zhí)行完成,如果沒(méi)有完成,則自動(dòng)延長(zhǎng)鎖的過(guò)期時(shí)間,直到業(yè)務(wù)執(zhí)行完成。

背景

如今的服務(wù)大多數(shù)是集群化部署,這就使得像synchronized、ReentrantLock等傳統(tǒng)的本地鎖失去了功效。因此需要引入第三方的服務(wù)實(shí)現(xiàn)對(duì)這些并發(fā)進(jìn)程的調(diào)度,從而控制對(duì)共享資源的訪問(wèn),像redis、zookeeper、mysql等。其中又以redis的應(yīng)用最為廣泛。

分布式鎖的要素

最重要的兩個(gè)要素:排他性、容錯(cuò)性。

排他性是指在分布式應(yīng)用集群中,同一個(gè)方法在同一時(shí)間內(nèi)只能被一臺(tái)機(jī)器上的一個(gè)線程執(zhí)行。

容錯(cuò)性是指不論正常的業(yè)務(wù)執(zhí)行完成,還是突發(fā)性的程序崩潰或者網(wǎng)絡(luò)中斷,都要保證分布式鎖最終一定能得到釋放,不能出現(xiàn)死鎖現(xiàn)象。

redis分布式鎖的基本命令

1、加鎖 SETNX key value

setnx 的含義就是 SET if Not Exists,有兩個(gè)參數(shù) setnx(key, value),該方法是原子性操作。如果 key 不存在,則設(shè)置當(dāng)前 key 成功,返回 1;如果當(dāng)前 key 已經(jīng)存在,則設(shè)置當(dāng)前 key 失敗,返回 0。

2、解鎖 del (key)

得到鎖的線程執(zhí)行完任務(wù),需要釋放鎖,以便其他線程可以進(jìn)入。

3、配置鎖超時(shí) expire (key,30s)

客戶端崩潰或者網(wǎng)絡(luò)中斷,資源將會(huì)永遠(yuǎn)被鎖住,即死鎖,因此需要給key配置過(guò)期時(shí)間,以保證即使沒(méi)有被顯式釋放,這把鎖也要在一定時(shí)間后自動(dòng)釋放。

OK,有了上面的理論基礎(chǔ),我們就可以來(lái)逐步的揭開(kāi)redis分布式鎖的神秘面紗。

我們以常見(jiàn)的扣減庫(kù)存的場(chǎng)景為例,當(dāng)有線程來(lái)執(zhí)行扣減庫(kù)存的方法時(shí),大致邏輯是先判斷當(dāng)前庫(kù)存,如果還有庫(kù)存的話,就庫(kù)存減1,然后生成明細(xì)記錄。

一把問(wèn)題很多的鎖

首先看一段偽代碼。

methodA(){
//ID為666的商品庫(kù)存扣減key
String key = "stock:deduct:666"

if(setnx(key,1) == 1{
expire(key,10,TimeUnit.SECONDS)
try {
//查詢是否有庫(kù)存
//扣減庫(kù)存
//生成明細(xì)記錄
} finally {
del(key)
}
}else{
//獲取鎖失敗,睡眠100毫秒,然后自旋調(diào)用本方法
methodA()
}
}

這段代碼的主要邏輯是,先給ID為666的商品庫(kù)存上鎖,然后設(shè)置key的過(guò)期時(shí)間為10秒,之后就執(zhí)行扣減庫(kù)存的邏輯了,等業(yè)務(wù)邏輯執(zhí)行完成,就刪除key釋放鎖。在此期間如果有其他線程來(lái)獲取鎖,會(huì)上鎖失敗,失敗后就等一會(huì)再次調(diào)用methodA方法繼續(xù)嘗試上鎖,然后循環(huán)往復(fù),直到上鎖成功。

看上去大功告成了,所謂的分布式鎖也不過(guò)如此。

然而,正如我們標(biāo)題上寫(xiě)的,這是一把問(wèn)題很多的鎖,有什么問(wèn)題呢?

首先最大的問(wèn)題是,多個(gè)命令之間不是原子操作。在setnx和expire之間是分了兩步來(lái)執(zhí)行的,如果setnx成功,但是expire卻執(zhí)行失敗,或者還沒(méi)有執(zhí)行就突發(fā)宕機(jī),就造成了這個(gè)資源的死鎖,違反了我們上面提到的容錯(cuò)性原則。

另外存在的一個(gè)問(wèn)題是,可能會(huì)出現(xiàn)線程A刪掉了線程B的鎖。假設(shè)有兩個(gè)線程A和B,A先上鎖成功開(kāi)始執(zhí)行業(yè)務(wù)邏輯,但由于某些原因?qū)е翧執(zhí)行很慢,15秒才執(zhí)行完,但A的鎖有效期只有10秒,A鎖過(guò)期后,B上鎖成功,但是B還沒(méi)有執(zhí)行完業(yè)務(wù)邏輯,線程A業(yè)務(wù)邏輯執(zhí)行完成,執(zhí)行刪鎖操作,此時(shí)刪除的,實(shí)際上是B的鎖,B的鎖刪掉了,也就無(wú)法阻止其他線程來(lái)加鎖,違反了上面提到的排他性原則。

如何解決這兩個(gè)問(wèn)題呢?

優(yōu)化后的鎖

第一個(gè)問(wèn)題,既然多個(gè)命令之間不是原子操作,我們用一個(gè)命令就行了,而redis恰好也提供了一個(gè)這樣的命令,setex,即在賦值的時(shí)候設(shè)置過(guò)期時(shí)間,這是一個(gè)原子命令。對(duì)應(yīng)到j(luò)ava中,也有這樣的API供我們使用:

redisTemplate.opsForValue().setIfAbsent("key","success",10,TimeUnit.SECONDS)

第二個(gè)問(wèn)題,可以在刪除鎖之前做一個(gè)判斷,驗(yàn)證當(dāng)前要?jiǎng)h除的鎖是不是自己的鎖,實(shí)現(xiàn)方式也很簡(jiǎn)單,可以將value值設(shè)置為當(dāng)前的線程ID或者隨便一個(gè)UUID。

優(yōu)化后的偽代碼應(yīng)該是這樣的:

methodA(){
//ID為666的商品庫(kù)存扣減key
String key = "stock:deduct:666";
String value = Thread.currentThread().getId();

if(setex(key, 10, value) == 1{
try {
//查詢是否有庫(kù)存
//扣減庫(kù)存
//生成明細(xì)記錄
} finally {
if(get(key).equals(value)){
del(key)
}
}
}else{
//獲取鎖失敗,睡眠100毫秒,然后自旋調(diào)用本方法
methodA()
}
}

這把鎖總沒(méi)問(wèn)題了吧?

然而,細(xì)細(xì)考究一下,還是會(huì)發(fā)現(xiàn)不妥之處。雖然我們刪除鎖的時(shí)候做了判斷,但仍有可能刪錯(cuò)鎖。根本原因是判斷鎖和刪除鎖同樣不是原子操作。

那到底如何保證絕對(duì)的原子性?

lua腳本的橫空出世

這里我們不去深究lua腳本是什么,只需要知道,lua是一個(gè)腳本語(yǔ)言,redis執(zhí)行l(wèi)ua腳本的時(shí)候,會(huì)將它里面的命令當(dāng)做一個(gè)整體去執(zhí)行,要么全部執(zhí)行成功,要么出現(xiàn)異常,結(jié)果不會(huì)更新到redis中。

因此,上面的刪鎖操作,我們完全可以將判斷命令和刪除命令都放到lua腳本中,然后由代碼去執(zhí)行l(wèi)ua腳本,最終會(huì)實(shí)現(xiàn)我們想要的原子操作。

實(shí)際上,這也正是redis官方推薦的做法。具體可查看官方文檔:??set 命令 -- Redis中國(guó)用戶組(CRUG)??。

這里提供一段java中調(diào)用lua腳本的代碼,大家看了后可以加深理解:

String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
Integer result = redisTemplate.execute(new DefaultRedisScript<>(script, Integer.class), Arrays.asList(lockKey), uuid);

其中的腳本代碼是官方文檔中提供的,可以直接復(fù)制過(guò)來(lái)使用。

原生分布式鎖

綜合上面所說(shuō)的,一個(gè)完整的原生分布式鎖應(yīng)該就是下面這個(gè)樣子了:

methodA(){
//ID為666的商品庫(kù)存扣減key
String key = "stock:deduct:666";
String value = Thread.currentThread().getId();
String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
if(setex(key, 10, value) == 1{
try {
//查詢是否有庫(kù)存
//扣減庫(kù)存
//生成明細(xì)記錄
} finally {
//解鎖
Long result = redisTemplate.execute( new DefaultRedisScript<>(script,Long.class),Arrays.asList(lockKey),uuid);
}
}else{
//獲取鎖失敗,睡眠100毫秒,然后自旋調(diào)用本方法
methodA()
}
}

這就是一個(gè)比較完善的分布式鎖了,既滿足了對(duì)共享資源的并發(fā)控制,又保證了加鎖、解鎖的原子性操作,防止突發(fā)狀況造成的死鎖問(wèn)題。

這里大家再想一個(gè)問(wèn)題,如何避免業(yè)務(wù)執(zhí)行時(shí)間過(guò)長(zhǎng)鎖過(guò)期的問(wèn)題?為了保證排他性,肯定要保證在業(yè)務(wù)執(zhí)行時(shí)間內(nèi),鎖是一定不能過(guò)期的。在原生的分布式鎖中,沒(méi)有什么好的方法,只能加長(zhǎng)鎖的過(guò)期時(shí)間,保證業(yè)務(wù)一定能執(zhí)行完成。

那么,有沒(méi)有更好的解決方案呢?

Hi,我叫redisson

redisson是redis官方推薦的一個(gè)分布式鎖的框架,它幫我們解決了上面提到的所有問(wèn)題,底層也是用了lua腳本實(shí)現(xiàn),同時(shí)又提供了watchdog(看門(mén)狗)機(jī)制,在鎖將要過(guò)期的時(shí)候,會(huì)自動(dòng)檢測(cè)業(yè)務(wù)是否執(zhí)行完成,如果沒(méi)有完成,則自動(dòng)延長(zhǎng)鎖的過(guò)期時(shí)間,直到業(yè)務(wù)執(zhí)行完成。而且最重要的一點(diǎn),使用起來(lái)非常簡(jiǎn)單,幾行代碼就可以搞定,不像原生鎖那樣繁瑣,是我們進(jìn)行分布式鎖開(kāi)發(fā)的不二選擇。這里不做詳細(xì)描述了,感興趣的可以在網(wǎng)上搜索一下。

好了,關(guān)于redis分布式鎖就到這里了。

責(zé)任編輯:姜華 來(lái)源: 今日頭條
相關(guān)推薦

2019-06-19 15:40:06

分布式鎖RedisJava

2023-08-21 19:10:34

Redis分布式

2022-01-06 10:58:07

Redis數(shù)據(jù)分布式鎖

2019-02-26 09:51:52

分布式鎖RedisZookeeper

2019-06-10 14:53:15

分布式架構(gòu)應(yīng)用服務(wù)

2022-12-01 07:36:40

2019-11-25 09:32:26

軟件程序員數(shù)據(jù)結(jié)構(gòu)

2023-03-01 08:07:51

2024-10-07 10:07:31

2020-11-16 12:55:41

Redis分布式鎖Zookeeper

2022-09-19 08:17:09

Redis分布式

2019-07-16 09:22:10

RedisZookeeper分布式鎖

2021-06-16 07:56:21

Redis分布式

2024-04-01 05:10:00

Redis數(shù)據(jù)庫(kù)分布式鎖

2019-12-25 14:35:33

分布式架構(gòu)系統(tǒng)

2023-01-13 07:39:07

2023-04-03 10:00:00

Redis分布式

2021-06-03 00:02:43

RedisRedlock算法

2022-12-18 20:07:55

Redis分布式

2022-03-08 07:22:48

Redis腳本分布式鎖
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)