在*nix
系統(tǒng)編程中,遇到多個進程或者線程共享一塊資源的時候,通常會使用系統(tǒng)自身提供的鎖,譬如一個進程里的多線程,會用互斥鎖;多個進程之間,會用信號量等。這個場景中所謂的共享資源僅僅限于本地,倘若共享資源存在于網(wǎng)絡(luò)上,本地的“鎖”就不起作用了。互斥訪問某個網(wǎng)絡(luò)上的資源,需要有一個存在于網(wǎng)絡(luò)上的鎖服務(wù)器,負責鎖的申請與回收。Redis 可以充當鎖服務(wù)器的角色。首先,Redis 是單進程單線程的工作模式,所有前來申請鎖資源的請求都被排隊處理,能保證鎖資源的同步訪問。
可以借助 Redis 管理鎖資源,來實現(xiàn)網(wǎng)絡(luò)資源的互斥。
我們可以在 Redis 服務(wù)器設(shè)置一個鍵值對,用以表示一把互斥鎖,當申請鎖的時候,要求申請方設(shè)置(SET)這個鍵值對,當釋放鎖的時候,要求釋放方刪除(DEL)這個鍵值對。譬如申請鎖的過程,可以用下面的偽代碼表示:
lock = redis.get("mutex_lock");
if(!lock)
error("apply the lock error.");
else
-- 確定可以申請鎖
redis.set("mutex_lock","locking");
do_something();
這種申請鎖的方法,涉及到客戶端和 Redis 服務(wù)器的多次交互,當客戶端確定可以加鎖的時候,可能這時候鎖已經(jīng)被其他客戶端申請了,最終導致兩個客戶端同時持有鎖,互斥的語意非常容易被打破。在 Redis 官方文檔描述了一些方法并且參看了網(wǎng)上的文章,好些方法都提及了這個問題。我們會發(fā)現(xiàn),這些方法的共同特點就是申請鎖資源的整個過程分散在客戶端和服務(wù)端,如此很容易出現(xiàn)數(shù)據(jù)一致性的問題。
因此,最好的辦法是將“申請/釋放鎖”的邏輯操作都放在服務(wù)器上,Redis Lua 腳本可以勝任。下面給出申請互斥鎖的 Lua 腳本:
-- apply for lock
local key = KEYS[1]
local res = redis.call('get', key)
-- 鎖被占用,申請失敗
if res == '0' then
return -1
-- 鎖可以被申請
else
local setres = redis.call('set', key, 0)
if setres['ok'] == 'OK' then
return 0
end
end
return -1
get 命令不成功返回(nil).
實驗命令:保存lua 腳本redis-cli script load ”$(cat mutex_lock.lua)”
同樣,釋放鎖的操作也可以在 Lua 腳本中實現(xiàn):
-- releae lock
local key = KEYS[1]
local setres = redis.call('set', key, 1)
if setres['ok'] == 'OK' then
return 0
return -1
如上 Lua 腳本基本的鎖管理的問題,將鎖的管理邏輯放在服務(wù)器端,可見 Lua 能拓展 Redis 服務(wù)器的功能。但上面的鎖管理方案是有問題的。
首先是客戶端崩潰導致的死鎖。按照上面的方法,當某個客戶端申請鎖后因崩潰等原因無法釋放鎖,那么其他客戶端無法申請鎖,會導致死鎖。
一般,申請鎖是為了讓多個訪問方對某塊數(shù)據(jù)作互斥訪問(修改),而我們應(yīng)該將訪問的時間控制在足夠短,如果持有鎖的時間過長,系統(tǒng)整體的性能肯定是下降的??梢越o定一個足夠長的超時時間,當訪問方超時后尚未釋放鎖,可以自動把鎖釋放。
Redis 提供了 TTL 功能,鍵值對在超時后會自動被剔除,在 Redis 的數(shù)據(jù)集中有一個哈希表專門用作鍵值對的超時。所以,我們有下面的 Lua 代碼:
-- apply for lock
local key = KEYS[1]
local timeout = KEYS[2]
local res = redis.call('get', key)
-- 鎖被占用,申請失敗
if res == '0' then
return -1
-- 鎖可以被申請
else
local setres = redis.call('set', key, 0)
local exp_res = redis.call('pexpire', key, timeout)
if exp_res == 1 then
return 0
end
end
return -1
如此能夠解決鎖持有者崩潰而鎖資源無法釋放帶來的死鎖問題。
再者是 Redis 服務(wù)器崩潰導致的死鎖。當管理鎖資源的 Redis 服務(wù)器宕機了,客戶端既無法申請也無法釋放鎖,死鎖形成了。一種解決的方法是設(shè)置一個備份 Redis 服務(wù)器,當 Redis 主機宕機后,可以使用備份機,但這需要保證主備的數(shù)據(jù)是同步的,不允許有延遲。
在同步有延遲的情況下,依舊會出現(xiàn)兩個客戶端同時持有鎖的問題。