背景
在類似秒殺這樣的并發(fā)場景下脚草,為了確保同一時刻只能允許一個用戶訪問資源弄砍,需要利用加鎖的機制控制資源的訪問權造锅。如果服務只在單臺機器上運行撼唾,可以簡單地用一個內存變量進行控制。而在多臺機器的系統(tǒng)上哥蔚,則需要用分布式鎖的機制進行并發(fā)控制倒谷。基于redis的一些特性糙箍,利用redis可以既方便又高效地模擬鎖的實現渤愁。
一個簡單方案
讓我們先從一個簡單的實現說起,這里用到了redis的兩個命令深夯,SETNX和EXPIRE抖格。如果lock_key不存在,那么就設置lock_key的值為1咕晋,并且設置過期時間雹拄;如果lock_key存在,說明已經有人在使用這把鎖捡需,訪問失敗办桨。
def acquire_lock(lock_key, expire_timeout=60):
if redis.setnx(lock_key, 1):
redis.expire(lock_key, expire_timeout)
return True
return False
邏輯上看似乎沒有問題,但是考慮一下異常情況:如果setnx設置成功站辉,但expire由于某些原因(比如超時)操作失敗呢撞,那么這把鎖就永遠存在了,也就是所謂的死鎖饰剥,后面的人永遠無法訪問這個資源殊霞。
利用時間戳取值的方案
為了解決死鎖,我們可以利用setnx的value來做文章汰蓉。上例中的我們設的value是1绷蹲,其實并沒有派上用場。因此可以考慮將value設為當前時間加上expire_timeout,當setnx設置失敗后祝钢,我們去讀lock_key的value比规,并且和當前時間作比對,如果當前時間大于value拦英,那么資源理當被釋放蜒什。代碼示例如下:
def acquire_lock(lock_key, expire_timeout=60):
expire_time = int(time.time()) + expire_timeout
if redis.setnx(lock_key, expire_time):
redis.expire(lock_key, expire_timeout)
return True
redis_value = redis.get(lock_key)
if redis_value and int(time.time()) > int(redis_value):
redis.delete(lock_key)
return False
然而仔細推敲下這段代碼仍然能發(fā)現一些問題。第一疤估,這個方案依賴時間灾常,如果在分布式系統(tǒng)中的時間沒有同步,則會對方案產生一定偏差铃拇。第二钞瀑,假設C1和C2都沒拿到鎖,它們都去讀value并對比時間慷荔,在競態(tài)條件(race condition)下可能產生如下的時序:C1刪除lock_key雕什,C1獲得鎖,C2刪除lock_key拧廊,C2獲得鎖监徘。這樣C1和C2同時拿到了鎖,顯然是不對的吧碾。
改進后的方案
幸運的是凰盔,redis里還有一個指令可以幫助我們解決這個問題。GETSET指令在set新值的同時會返回老的值倦春,這樣的話我們可以檢查返回的值户敬,如果該值和之前讀出來的值相同,那么這次操作有效睁本,反之則無效尿庐。代碼示例如下:
def acquire_lock(lock_key, expire_timeout=60):
expire_time = int(time.time()) + expire_timeout
if redis.setnx(lock_key, expire_time):
redis.expire(lock_key, expire_timeout)
return True
redis_value = redis.get(lock_key)
if redis_value and int(time.time()) > int(redis_value):
expire_time = int(time.time()) + expire_timeout
old_value = redis.getset(lock_key, expire_time)
if int(old_value) == int(redis_value):
return True
return False
這個方案基本可以滿足要求,除了有一個小瑕疵呢堰,由于getset會去修改value抄瑟,在競態(tài)條件下可能會被修改多次導致timeout有細微的誤差,但這個對結果影響不大枉疼。
最終方案
以上方案實現起來略顯繁瑣皮假,但從redis 2.6.12版本開始有一個更為簡便的方法。我們可以使用SET指令的擴展 ** SET key value [EX seconds] [PX milliseconds] [NX|XX] **骂维,這個指令相當于對SETNX和EXPIRES進行了合并惹资,因而我們的算法可以簡化為如下一行:
def acquire_lock(lock_key, expire_timeout=60):
ret = redis.set(lock_key, int(time.time()), nx=True, ex=expire_timeout):
return ret
總結
在redis 2.6.12版本之后我們可以用一個簡單的SET命令實現分布式鎖,而在此版本之前則需要將SETNX和GETSET配合使用一個較為繁瑣的方案航闺。簡化后的方案對于開發(fā)者來說當然是好事褪测,但通過學習這一演變過程我們會對問題有更深刻的印象猴誊。