天天看點

redis setnx 分布式鎖_對比各類分布式鎖缺陷,抓住Redis分布式鎖實作命門

redis setnx 分布式鎖_對比各類分布式鎖缺陷,抓住Redis分布式鎖實作命門

近兩年來微服務變得越來越熱門,越來越多的應用部署在分布式環境中,在分布式環境中,資料一緻性是一直以來需要關注并且去解決的問題,分布式鎖也就成為了一種廣泛使用的技術。

常用的分布式實作方式為Redis,Zookeeper,其中基于Redis的分布式鎖的使用更加廣泛。

但是在工作和網絡上看到過各個版本的Redis分布式鎖實作,每種實作都有一些不嚴謹的地方,甚至有可能是錯誤的實作,包括在代碼中,如果不能正确的使用分布式鎖,可能造成嚴重的生産環境故障。

本文主要對目前遇到的各種分布式鎖以及其缺陷做了一個整理,并對如何選擇合适的Redis分布式鎖給出建議。

一、各個版本的Redis分布式鎖

1、V1.0

tryLock{

SETNX Key 1

EXPIRE Key Seconds

}

release{

DELETE Key

}

這個版本應該是最簡單的版本,也是出現頻率很高的一個版本,首先給鎖加一個過期時間操作是為了避免應用在服務重新開機或者異常導緻鎖無法釋放後,不會出現鎖一直無法被釋放的情況。

  • 這個方案的一個問題在于每次送出一個Redis請求,如果執行完第一條指令後應用異常或者重新開機,鎖将無法過期,一種改善方案就是使用Lua腳本(包含SETNX和EXPIRE兩條指令),但是如果Redis僅執行了一條指令後crash或者發生主從切換,依然會出現鎖沒有過期時間,最終導緻無法釋放。
  • 另外一個問題在于,很多同學在釋放分布式鎖的過程中,無論鎖是否擷取成功,都在finally中釋放鎖,這樣是一個鎖的錯誤使用,這個問題将在後續的V3.0版本中解決。
  • 針對鎖無法釋放問題的一個解決方案基于GETSET指令來實作。

2、V1.1 基于GETSET

tryLock{

NewExpireTime=CurrentTimestamp+ExpireSeconds

if(! SET Key NewExpireTime Seconds NX){

oldExpireTime = GET(Key)

if( oldExpireTime < CurrentTimestamp){

NewExpireTime=CurrentTimestamp+ExpireSeconds

CurrentExpireTime=GETSET(Key,NewExpireTime)

if(CurrentExpireTime == oldExpireTime){

return 1;

}else{

return 0;

}

}

}

}

release{

DELETE key

}

思路:

  • SETNX(Key,ExpireTime)擷取鎖。
  • 如果擷取鎖失敗,通過GET(Key)傳回的時間戳檢查鎖是否已經過期。
  • GETSET(Key,ExpireTime)修改Value為NewExpireTime。
  • 檢查GETSET傳回的舊值,如果等于GET傳回的值,則認為擷取鎖成功。

注意:這個版本去掉了EXPIRE指令,改為通過Value時間戳值來判斷過期。

問題:

  • 在鎖競争較高的情況下,會出現Value不斷被覆寫,但是沒有一個Client擷取到鎖。
  • 在擷取鎖的過程中不斷的修改原有鎖的資料,設想一種場景C1,C2競争鎖,C1擷取到了鎖,C2鎖執行了GETSET操作修改了C1鎖的過期時間,如果C1沒有正确釋放鎖,鎖的過期時間被延長,其它Client需要等待更久的時間。

3、V2.0 基于SETNX

tryLock{

SET Key 1 Seconds NX

}

release{

DELETE Key

}

Redis2.6.12版本後SETNX增加過期時間參數,這樣就解決了兩條指令無法保證原子性的問題。但是設想下面一個場景:

  • C1成功擷取到了鎖,之後C1因為GC進入等待或者未知原因導緻任務執行過長,最後在鎖失效前C1沒有主動釋放鎖。
  • C2在C1的鎖逾時後擷取到鎖,并且開始執行,這個時候C1和C2都同時在執行,會因重複執行造成資料不一緻等未知情況。
  • C1如果先執行完畢,則會釋放C2的鎖,此時可能導緻另外一個C3程序擷取到了鎖。

大緻的流程圖:

redis setnx 分布式鎖_對比各類分布式鎖缺陷,抓住Redis分布式鎖實作命門

存在問題:

  • 由于C1的停頓導緻C1 和C2同都獲得了鎖并且同時在執行,在業務實作間接要求必須保證幂等性。
  • C1釋放了不屬于C1的鎖。

4、V3.0

tryLock{

SET Key UnixTimestamp Seconds NX

}

release{

EVAL(

//LuaScript

if redis.call("get

繼續閱讀