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