Redis是一個將資料存放在記憶體的存儲,而且我們都知道記憶體有多不可靠。更有甚者,在昨天,Redis的持久化中有提到Redis的儲存比我們想的更不可靠。因此,使用Redis當鎖來保證同步是很危險的。
但是,這篇文章,不是專注在告訴你有多危險,而是要更進一步分析,我們使用的「鎖」真的有用對嗎?
事實上,我們通常說的鎖有兩種截然不同的概念。
儘管,這兩個概念有截然不同的目的且有完全相異的實作方式,但我們通常沒有仔細定義我們的需求,並且選擇錯誤的實作,導致最後做出一個畸形。
排他鎖的目的是我們所熟知的互斥(mutex)鎖,用來控制臨界區段的同時使用者僅能有一個。
進入臨界區段的時候上鎖,而離開時解鎖。這機制也很常在使用資料庫時搭配使用以避免競爭條件,例如,MySQL無法避免更新遺失,因此會用一個排他鎖FOR UPDATE
作為兩個同時更新的同步機制。
根據上面描述,我們可以寫出偽代碼。
while not tryLock(forLongTime):
sleep(veryShortTime)
doSomeThing()
releaseLock()
有幾個重點必須解釋一下。
tryLock
這個函式包含兩個動作:取得鎖以及上鎖。如果可以取得鎖,那麼就應該當下立刻上鎖,不然鎖有可能也會被別人取得,那麼臨界區段就破功了。再一次聲明,排他鎖的目的是為了控制臨界區段的存取。如果兩個同時發起的操作,那麼兩個操作都會執行,但是一前一後。
屏障的目的是為了減少特定操作的頻率。
舉例來說,如果連續呼叫一個API兩次,那麼第二個API可能會拿到「請稍後再試」,這就是典型的屏障。
既然目的是為了減少頻率,那麼鎖定的時間就很重要,要根據功能需求來決定。例如:如果產品規格是兩個API必須間隔5秒,那麼鎖的時間就是5秒。這通常比排他鎖的鎖定時間短。
我們也試著寫出偽代碼。
if not tryLock(featureSpecTime):
return False
return doSomeThing()
相較於排他鎖,屏障並沒有解鎖和等待,但同樣會使用tryLock
,只是鎖的時間來自於產品規格。
屏障是為了減少頻率,因此如果兩個操作同時發生,那麼一個會成功而另一個會失敗。
好,我們都已經了解兩種鎖的概念了,因此我們來看看與Redis的關係。
Redis是最常被用來實作排他鎖和屏障的工具,因為利用Redis來實作tryLock
非常容易,只需要一行指令:
SET someKey 1 EX someTime NX
根據指令的結果是OK
或nil
就可以知道有沒有成功取得鎖,以實作來說實在非常單純。
但是,就像文章一開始說的,Redis並不可靠,因此使用Redis作為排他鎖會背負風險。
Redis作為屏障還不錯,就算真的資料消失,也頂多是原本只能執行一次的操作變成執行兩次而已,但不會影響系統穩定。反之,若是排他鎖,我們就得要仔細權衡風險了。
對我來說,如果要避免MySQL的更新遺失,我總是會使用MySQL內建的FOR UPDATE
而不是Redis,不僅更容易使用也比Redis更安全。關於MySQL使用鎖的細節在前幾天我們已經有介紹過了。
這篇文章解釋了兩種我們常稱的鎖和其意義,儘管他們有著類似的名稱,但無論目的還是實作都截然不同。
總結一下。
當我們提到鎖,請確定到底要解決的是什麼使用情境,並且將鎖用對。
對我來說,即使在系統中有Redis且我也會用他,但我在設計系統時我總是當Redis內的資料不存在,如此一來才會正確的對待各種「不幸」。
請記得墨菲定律:當事情有機會出錯時,就總是會出錯。