Redis 實(shí)現(xiàn)分布式鎖演進(jìn)原理

點(diǎn)擊上方「Java有貨」關(guān)注我們

+
引言
分布式鎖是一個(gè)老生常談的話題,但是如何寫好鎖,避免更多的坑,讓我一起從無到有的演進(jìn)一次!
分布式鎖
分布式鎖是控制分布式系統(tǒng)之間同步訪問共享資源的一種方式。
在分布式系統(tǒng)中,常常需要協(xié)調(diào)他們的動(dòng)作。如果不同的系統(tǒng)或是同一個(gè)系統(tǒng)的不同主機(jī)之間共享了一個(gè)或一組資源,那么訪問這些資源的時(shí)候,往往需要互斥來防止彼此干擾來保證一致性,這個(gè)時(shí)候,便需要使用到分布式鎖
無鎖的應(yīng)用
@GetMapping(value = "test")public void test() {ReentrantLock reentrantLock = new ReentrantLock();reentrantLock.lock();try {order();}finally {reentrantLock.unlock();}}
我們?cè)陂_發(fā)應(yīng)用的時(shí)候,如果需要對(duì)某一個(gè)共享變量進(jìn)行多線程同步訪問的時(shí)候,可以使用我們學(xué)到的鎖進(jìn)行處理,并且可以完美的運(yùn)行,毫無Bug!
但是如果是分布式環(huán)境下呢?這時(shí)就必須采用分布式鎖,才能保證所有服務(wù)操作資源的一致性,分布式鎖有很多種實(shí)現(xiàn)方式,如:數(shù)據(jù)庫、Redis、zookeeper....今天我們將介紹Redis實(shí)現(xiàn)分布式鎖的演進(jìn)流程,和其中的一些坑。
Redis實(shí)現(xiàn)分布式鎖的演進(jìn)流程
上面的代碼在分布式情況下肯定是有問題的,那我稍加調(diào)整一下,引入Redis
第一次演進(jìn)
@GetMapping(value = "test2")public void test2() {Boolean javayh = redisTemplate.opsForValue().setIfAbsent(RedisKey.key("redis-order-lock"), "javayh");try {if (javayh) {order();}//實(shí)現(xiàn)自旋else {test2();}} finally {redisTemplate.delete(RedisKey.key("redis-order-lock"));}}
大家看這段代碼有什么問題?
在所有的一切都是按照我們的預(yù)期去執(zhí)行的,好像沒什么問題,但是并發(fā)下往往不會(huì)按照我的預(yù)期去執(zhí)行。
問題
在分布式中,其中一個(gè)線程得到了鎖,進(jìn)行執(zhí)行,其他的線程進(jìn)行不斷的嘗試獲取鎖,但是如果獲取到鎖的服務(wù)器掛了,沒有釋放鎖,這就會(huì)造成死鎖...
第二次演進(jìn)
上面的問題似乎出現(xiàn)了加鎖后,沒有執(zhí)行釋放鎖的代碼,那么我們是不是可以給鎖設(shè)置過期時(shí)間,實(shí)現(xiàn)到期自動(dòng)刪除
(value = "test3")public void test3() {String key = RedisKey.key("redis-order-lock");// 上面的代碼 沒有辦法釋放鎖,那好,我們給他指定失效時(shí)間,但是這里有沒有坑呢Boolean javayh = redisTemplate.opsForValue().setIfAbsent(key, "javayh");try {redisTemplate.expire(key, 30, TimeUnit.SECONDS);if (javayh) {order();}//實(shí)現(xiàn)自旋else {test2();}} finally {redisTemplate.delete(key);}}
大家看這段代碼有什么問題?
問題
但是就像之前一下,在沒有執(zhí)行給鎖設(shè)置過期時(shí)間,服務(wù)器就掛了呢?是不是也會(huì)造成死鎖。也就是說,上下兩個(gè)操作不是原子的操作。
第三次演進(jìn)
知道了問題所在我們繼續(xù)改。
(value = "test4")public void test4() {// 上面的代碼 沒有辦法釋放鎖,那我們將加鎖和設(shè)置失效時(shí)間的代碼放在一起就可以// 這樣好像看似沒什么問題了,但是確實(shí)是這樣嗎?String key = RedisKey.key("redis-order-lock");Boolean javayh = redisTemplate.opsForValue().setIfAbsent(key, "javayh", 30, TimeUnit.SECONDS);try {if (javayh) {order();}//實(shí)現(xiàn)自旋else {test4();}} finally {redisTemplate.delete(key);}}
大家看這段代碼有什么問題?
問題
我們?cè)賮矸治鲆幌拢杭偃?/span>這是有三個(gè)線程,其中一個(gè)線程獲取了鎖,執(zhí)行了起來,但是性能很慢,超過了我們的過期時(shí)間, 這時(shí)鎖已經(jīng)被釋放,其他線程就可以進(jìn)行獲取鎖,但是當(dāng)?shù)谝粋€(gè)線程執(zhí)行完業(yè)務(wù)邏輯,想要?jiǎng)h除這把鎖、,這時(shí)就會(huì)把其他線程鎖住的資源進(jìn)行釋放了,這也是坑根據(jù)上面的分析,我們可以將key重新設(shè)置一下,修改后的代碼
第四次演進(jìn)
@GetMapping(value = "test4")public void test4() {// 重新生成的keyString key = RedisKey.key("redis-order-lock") + UUID.randomUUID().toString();Boolean javayh = redisTemplate.opsForValue().setIfAbsent(key, "javayh", 30, TimeUnit.SECONDS);try {if (javayh) {order();}//實(shí)現(xiàn)自旋else {test4();}} finally {Object o = redisTemplate.opsForValue().get(key);if (o.equals(key)) {redisTemplate.delete(key);}}}
大家看這段代碼有什么問題?
問題
這樣看起來好像沒什么問題,還加了判斷鎖與redis的鎖是不是一致的,但是Redis的官方并不推薦我們這樣操作,他更希望我們可以使用腳本在進(jìn)行。
第五次演進(jìn)
(value = "test5")public void test5() {// 這里看似ok。我們先不看String key = RedisKey.key("redis-order-lock") + UUID.randomUUID().toString();Boolean javayh = redisTemplate.opsForValue().setIfAbsent(key, "javayh", 30, TimeUnit.SECONDS);try {if (javayh) {order();}//實(shí)現(xiàn)自旋else {test2();}} finally {// 官方建議使用腳本操作String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +"then\n" +" return redis.call(\"del\",KEYS[1])\n" +"else\n" +" return 0\n" +"end";redisTemplate.execute(new DefaultRedisScript<Long>(script), Arrays.asList(key), key);}}
最終的演進(jìn)到這里就差不多了,當(dāng)然這只是demo,問題肯定還是有的。
演進(jìn)的基礎(chǔ)理論
這一切的演進(jìn)其實(shí)都來源于Redis官方的說明,如下:
The command
SET resource-name anystring NX EX max-lock-timeis a simple way to implement a locking system with Redis.命令SET resource-name anystring NX EX max-lock-time是用Redis實(shí)現(xiàn)鎖定系統(tǒng)的一種簡單方法。
A client can acquire the lock if the above command returns
OK(or retry after some time if the command returns Nil), and remove the lock just using DEL.客戶端可以獲得鎖,如果上面的命令返回OK(或重試一段時(shí)間后,如果命令返回Nil),并使用DEL刪除鎖。
The lock will be auto-released after the expire time is reached.
鎖定將在到達(dá)過期時(shí)間后自動(dòng)釋放。
It is possible to make this system more robust modifying the unlock schema as follows:
修改解鎖模式可以使這個(gè)系統(tǒng)更健壯,如下所示:
Instead of setting a fixed string, set a non-guessable large random string, called token.
與其設(shè)置固定的字符串,不如設(shè)置一個(gè)不可猜測(cè)的大型隨機(jī)字符串,稱為token。
Instead of releasing the lock with DEL, send a script that only removes the key if the value matches.
發(fā)送一個(gè)只在值匹配時(shí)移除鍵的腳本,而不是使用DEL釋放鎖。
This avoids that a client will try to release the lock after the expire time deleting the key created by another client that acquired the lock later.
這避免了客戶端在過期時(shí)間后試圖釋放鎖,刪除另一個(gè)客戶端創(chuàng)建的密鑰,該密鑰是稍后獲得鎖的。
An example of unlock script would be similar to the following:
解鎖腳本的示例如下:
if redis.call("get",KEYS[1]) == ARGV[1]
then
return redis.call("del",KEYS[1])
else
return 0
endThe script should be called with
EVAL ...script... 1 resource-name token-value
