簡單的說,鎖是一個單一的參考點,多個線程基于它來檢查是否允許訪問資源。例如,一個想寫數據的線程,它必須先檢查是否存在一個寫鎖。如果寫鎖存在,需要等待直到鎖釋放后它才能獲取到屬于它的鎖并執行寫操作。這樣,通過鎖就可以避免多個線程的同時寫造成的數據沖突。
現代的操作系統提供了內置的函數來幫助程序員實現并發控制,例如 flock 函數。但是如果多線程的程序運行在多臺機器上呢?如何在分布式系統下控制對資源的訪問呢?
首先,我們需要一個所有線程都可以訪問到的地方來存儲鎖。這個鎖只能存在于一個地方,從而保證只有一個權威的地方可以定義鎖的建立和釋放。
Redis是實現鎖的一個理想的候選方案。作為一個輕量級的內存數據庫,快速,事務性和一致性是選擇redis所為鎖服務的主要原因。
鎖本身是很簡單的,就是redis數據庫中一個簡單的key。建立和釋放鎖,并保證絕對的安全,是這個鎖的設計比較棘手的地方。有兩個潛在的陷阱:
1. 應用程序通過網絡和redis交互,這意味著從應用程序發出命令到redis結果返回之間會有延遲。這段時間內,redis可能正在運行其他的命令,而redis內數據的狀態可能不是你的程序所期待的。如果保證程序中獲取鎖的線程和其他線程不發生沖突?
2. 如果程序在獲取鎖后突然crash,而無法釋放它?這個鎖會一直存在而導致程序進入“餓死”(原文成為“死鎖”,感覺不太準確)。
可能想到的最簡單的方法是“用GET方法檢查鎖,如果鎖不存在,就用SET方式設置一個值”。
這個方法雖然簡單,但是不能保證獨占鎖。回顧前面所說的第1個陷阱:因為在GET和SET操作之間有延遲,我們沒法知道從“發送命令”到“redis服務器返回結果”之間的這段時間內是否有其他線程也去建立鎖。當然,這些都在幾毫秒之內,發生的可能性相當低。但是如果在一個繁忙的環境中運行著大量的并發線程和命令,重疊的可能性并不是微不足道的。
為了解決這個問題,應該用SETNX命令。SETNX消除了GET命令需要等待返回值的問題,SETNX只有在key不存在時才返回成功。這意味著只有一個線程可以成功運行SETNX命令,而其他線程會失敗,然后不斷重試,直到它們能建立鎖。
一旦線程成功執行了SETNX命令,它就建立了鎖并且可以基于資源進行工作。工作完成后,線程需要通過刪除redis的key來釋放這個鎖,從而允許其他線程能盡快的獲取鎖。
盡管如此,也有需要小心的地方!回顧前面說的第2個陷阱:如果線程crash了,它永遠都不會刪除redis的key,所以這個鎖會一直存在,從而導致“餓死”現象。那么如何避免這個問題呢?
我們可以給鎖加一個存活時間(TTL),這樣一旦TTL超時,這個鎖的key會被redis自動刪除。任何由于線程錯誤而遺留下來的鎖在一個合適的時間之后都會被釋放,從而避免了“餓死”。這純粹是一個安全特性,更有效的方式仍然是確保盡量在線程里面釋放鎖。
可以通過PEXPIRE命令為Redis的key設置TTL,而且線程里可以通過MULTI/EXEC事務的方式在SETNX命令后立即執行,例如:
MULTI SETNX lock-key PEXPIRE 10000 lock-key EXEC
盡管如此,這會產生另外一個問題。PEXPIRE命令沒有判斷SETNX命令的返回結果,無論如何都會設置key的TTL。如果這個地方無法獲取到鎖或有異常,那么多個線程每次想獲取鎖時,都會頻繁更新key的TTL,這樣會一直延長key的TTL,導致key永遠都不會過期。為了解決這個問題,我們需要Redis在一個命令里面處理這個邏輯。我們可以通過Redis腳本的方式來實現。
注意-如果不采用腳本的方式來實現,可以使用Redis 2.6.12之后版本SET命令的PX和NX參數來實現。為了考慮兼容2.6.0之前的版本,我們還是采用腳本的方式來實現。
由于Redis支持腳本,我們可以寫一個Lua腳本在Redis服務端運行多個Redis命令。應用程序通過一條EVALSHA命令就可以調用被Redis服務端緩存的腳本。這里強大的地方在于你的程序只需要運行一條命令(腳本)就可以以事務的方式運行多個redis命令,還能避免并發沖突,因為一個redis腳本同一時刻只能運行一次。
這是Redis里面一個設置帶TTL的鎖的Lua腳本:
-- -- Set a lock -- -- KEYS[1] - key -- KEYS[2] - ttl in ms -- KEYS[3] - lock content local key = KEYS[1] local ttl = KEYS[2] local content = KEYS[3] local lockSet = redis.call('setnx', key, content) if lockSet == 1 then redis.call('pexpire', key, ttl) end return lockSet
從這個腳本可以很清楚的看到,我們通過在鎖上只運行PEXPIRE命令就解決了前面提到的“無休止的TTL”問題。
上面我們介紹了基于Redis的鎖的理論,這里有一個用Node.js寫的開源模塊Warlock,通過npm可以獲取。它使用了redis腳本來創建/釋放鎖,用于為緩存,數據庫,任務隊列和其他需要并發的地方提供分布式的鎖服務,詳見Github。
原文中還缺少一個釋放鎖的腳本,如果一直依賴TTL來釋放鎖,效率會很低。Redis的SET操作文檔就提供了一個釋放鎖的腳本:
if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end
應用程序中只要加鎖的時候指定一個隨機數或特定的value作為key的值,解鎖的時候用這個value去解鎖就可以了。當然,每次加鎖時的value必須要保證是唯一的。
原文地址:基于Redis Lua腳本實現的分布式鎖, 感謝原作者分享。
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。TEL:177 7030 7066 E-MAIL:11247931@qq.com