Redis併發問題
1.客戶端角度,爲保證每個客戶端間正常有序與Redis進行通信,對連接進行池化,同時對客戶端讀寫Redis操作採用內部鎖synchronized。
2.服務器角度,利用setnx實現鎖。
對於第一種,需要應用程序自己處理資源的同步,可以使用的方法比較通俗,可以使用synchronized也可以使用lock;第二種需要用到Redis的setnx命令,但是需要注意一些問題。
語法:
功能:
將 key 的值設爲 value ,當且僅當 key 不存在;若給定的 key 已經存在,則 SETNX 不做任何動作。
時間複雜度:
O(1)
返回值:
設置成功,返回 1 。
設置失敗,返回 0 。
模式:將 SETNX 用於加鎖(locking)
SETNX 可以用作加鎖原語(locking primitive)。比如說,要對關鍵字(key) foo 加鎖,客戶端可以嘗試以下方式:
SETNX lock.foo <current Unix time + lock timeout + 1>
如果 SETNX 返回 1 ,說明客戶端已經獲得了鎖, key 設置的unix時間則指定了鎖失效的時間。之後客戶端可以通過 DEL lock.foo 來釋放鎖。
如果 SETNX 返回 0 ,說明 key 已經被其他客戶端上鎖了。如果鎖是非阻塞(non blocking lock)的,我們可以選擇返回調用,或者進入一個重試循環,直到成功獲得鎖或重試超時(timeout)。
但是已經證實僅僅使用SETNX加鎖帶有競爭條件,在特定的情況下會造成錯誤。
處理死鎖(deadlock)
上面的鎖算法有一個問題:如果因爲客戶端失敗、崩潰或其他原因導致沒有辦法釋放鎖的話,怎麼辦?
這種狀況可以通過檢測發現——因爲上鎖的 key 保存的是 unix 時間戳,假如 key 值的時間戳小於當前的時間戳,表示鎖已經不再有效。
但是,當有多個客戶端同時檢測一個鎖是否過期並嘗試釋放它的時候,我們不能簡單粗暴地刪除死鎖的 key ,再用 SETNX 上鎖,因爲這時競爭條件(race condition)已經形成了:
C1 和 C2 讀取 lock.foo 並檢查時間戳, SETNX 都返回 0 ,因爲它已經被 C3 鎖上了,但 C3 在上鎖之後就崩潰(crashed)了。
C1 向 lock.foo 發送 DEL 命令。
C1 向 lock.foo 發送 SETNX 併成功。
C2 向 lock.foo 發送 DEL 命令。
C2 向 lock.foo 發送 SETNX 併成功。
出錯:因爲競爭條件的關係,C1 和 C2 兩個都獲得了鎖。
幸好,以下算法可以避免以上問題。來看看我們聰明的 C4 客戶端怎麼辦:
C4 向 lock.foo 發送 SETNX 命令。
因爲崩潰掉的 C3 還鎖着 lock.foo ,所以 Redis 向 C4 返回 0 。
C4 向 lock.foo 發送 GET 命令,查看 lock.foo 的鎖是否過期。如果不,則休眠(sleep)一段時間,並在之後重試。
另一方面,如果 lock.foo 內的 unix 時間戳比當前時間戳老,C4 執行以下命令:
GETSET lock.foo <current Unix timestamp + lock timeout + 1>
因爲 GETSET 的作用,C4 可以檢查看 GETSET 的返回值,確定 lock.foo 之前儲存的舊值仍是那個過期時間戳,如果是的話,那麼 C4 獲得鎖。
如果其他客戶端,比如 C5,比 C4 更快地執行了 GETSET 操作並獲得鎖,那麼 C4 的 GETSET 操作返回的就是一個未過期的時間戳(C5 設置的時間戳)。C4 只好從第一步開始重試。
注意,即便 C4 的 GETSET 操作對 key 進行了修改,這對未來也沒什麼影響。
這裏假設鎖key對應的value沒有實際業務意義,否則會有問題,而且其實其value也確實不應該用在業務中。
爲了讓這個加鎖算法更健壯,獲得鎖的客戶端應該常常檢查過期時間以免鎖因諸如 DEL 等命令的執行而被意外解開,因爲客戶端失敗的情況非常複雜,不僅僅是崩潰這麼簡單,還可能是客戶端因爲某些操作被阻塞了相當長時間,緊接着 DEL 命令被嘗試執行(但這時鎖卻在另外的客戶端手上)。
語法:
功能:
將給定 key 的值設爲 value ,並返回 key 的舊值(old value)。當 key 存在但不是字符串類型時,返回一個錯誤。
時間複雜度:
O(1)
返回值:
返回給定 key 的舊值;當 key 沒有舊值時,也即是, key 不存在時,返回 nil 。
ref by