1. 程式人生 > >使用Redis SETNX 命令實現分散式鎖”

使用Redis SETNX 命令實現分散式鎖”

使用Redis的 SETNX 命令可以實現分散式鎖,本文介紹其實現方法。

直接進入正題,現在分散式的應用場景很多,為了保持資料的一致性,經常碰到需要對資源加鎖的情形。 利用redis來實現分散式鎖就是其中的一種實現方案。

SETNX命令簡介

命令格式

SETNX key value

將 key 的值設為 value ,當且僅當 key 不存在。

若給定的 key 已經存在,則 SETNX 不做任何動作。

SETNX 是『SET if Not eXists』(如果不存在,則 SET)的簡寫。

返回值

設定成功,返回 1 。
設定失敗,返回 0 。

示例

redis> EXISTS job                # job 不存在
(integer) 0

redis> SETNX job "programmer"    # job 設定成功
(integer) 1

redis> SETNX job "code-farmer"   # 嘗試覆蓋 job ,失敗
(integer) 0

redis> GET job                   # 沒有被覆蓋
"programmer"

SETNX分散式鎖實現方案

利用SETNX的特性,很容易的想到,在需要加鎖的時候,呼叫SETNX命令,如果返回了1,表示設定成功,獲得了當前鎖,之後做一些想要的操作,完成之後呼叫DEL命令釋放鎖。

redis> SETNX lock true    # 獲得鎖成功
(integer) 1
... do thing ...
redis> DEL lock    # 釋放鎖
(integer) 1

但是這樣存在一個問題,如果在執行DEL命令之前,當前程式發生錯誤,那麼這個鎖就永遠得不到釋放,其他程式也永遠無法加鎖成功。

於是我們可以在加鎖之後為這個鎖設定一個過期時間,過期時間之後,如果沒有釋放,就自動刪除,防止鎖被一直佔用。

redis> SETNX lock true    # 獲得鎖成功
(integer) 1
redis> EXPIRE lock 5    # 設定5秒的過期時間
(integer) 1
... do thing ...
redis> DEL lock    # 釋放鎖
(integer) 1

但是這樣還是有問題,如果在SETNX和EXPIRE之間程式又發生了錯誤,當前鎖又無法釋放。所以根本原因還是需要一個原子的操作,在獲得鎖的同時能夠同時設定鎖的過期時間。

為了解決這個問題,Redis 2.8 版本中作者加入了 set 指令的擴充套件引數,使得 setnx 和 expire 指令可以一起執行, 這個可以在下一篇介紹。 本文介紹另一種方式。

SETNX設定鎖

在設定鎖的時候,我們可以利用鎖的值來實現過期的特性

SETNX lock  <current Unix time + lock timeout>

我們不是設定一個簡單的值到lock中,而是將過期的時間寫入到lock中。 獲得鎖的判斷條件仍舊是跟之前一樣, 如果返回了1的話,表示獲得了鎖,可以進行下一步的操作。

判斷過期條件

正常情況下,操作完成之後,仍舊執行DEL操作將當前鎖釋放。那麼如果當前程式發生了錯誤退出了,當前鎖沒有正常釋放,其他的程序如何獲得鎖呢。

假設上一個程序加鎖之後異常退出,沒有釋放鎖。當前的程序想要加鎖,在呼叫SETNX的時候發現加鎖失敗,然後需要呼叫GET命令獲得當前鎖的值,即上一個程序寫入的過期時間。 如果獲得的過期時間未到,那麼當前程序繼續等待; 如果鎖的過期時間已經到了,很大的概率上一個獲得鎖的程序已經發生了錯誤,因為我們這個過期時間一般會設定的比正常的執行時間要長。在這種情況下, 當前程序可以重新寫入這個鎖並進行後續的操作。

解決競爭條件

但是這樣又帶來一個新的問題: 假設有P1和P2兩個程序同時想獲得鎖,他們都檢測到了當前的鎖已經過期了, 他們可以寫入,他們呼叫SET命令寫入都會成功,那麼如果決定到底是哪個程序獲得了鎖呢。

所以在這邊重新寫入的時候不能簡單的呼叫SET命令, 還有另一個命令可以考慮: GETSET。GETSET命令在設定值的同時,會將設定之前的值返回。

仍舊考慮剛才的情形, P1和P2同時在競爭鎖,發現鎖的時間T已經過期了,然後他們同時呼叫GETSET命令設定新的鎖。假設P1先設定成功時間T1,那麼呼叫GETSET得到的值就是T; P2呼叫GETSET雖然將鎖的時間設定成了T2,但是他得到的值是T1。

通過判斷GETSET返回的值,就能判斷自己是否獲得了鎖。如果返回的值仍然是一個過期的時間,那麼說明正確的加鎖了;否則的話,說明正好有別的程序已經設定了鎖,當前程序只是更新了一下鎖而已,就繼續等待。

可能會說這邊有一個小問題,P1設定的鎖的過期時間被P2更改了。考慮到產生這種競態條件的時候肯定時間間隔是非常小的, 即使重新設定了過期時間,這種很短的時間修改在大多數情況下都可以忽略不計。

虛擬碼

所以,我們能夠得到最終的一個過程,用偽程式碼表示

while 1:
	lock = redis.SETNX(key, time.now() + timeout)
	if lock == 1:
		// 獲得鎖
		break
	lock_ts = redis.GET(key)
	if (lock_ts < time.now()) && (redis.GETSET(key, time.now() + timeout) < time.now()):
		// 鎖已經過期,用GETSET重新寫鎖
		// 返回的原來的時間仍舊過期,說明加鎖成功
		break
	else:
		sleep
		
.... do something ...

// 完成之後釋放鎖
redis.DEL(key)