深入Redis(一)分布式鎖
阿新 • • 發佈:2018-08-18
參數 包裝 情況 變量 clas return 一個 set 標簽
分布式鎖
由於分布式應用在邏輯處理時存在並發問題,比方修改數據,要先讀取到內存,在內存中修改後再保存回去,這兩個操作是單獨的,如果同時進行,就會出現並發問題。
此時就要用到分布式鎖來限制程序的並發執行。
本質
本質就是在Redis內占一個位置,若別的進程也想占用該位置,發現有進程在使用該位置,就放棄或等待。
在Redis中實現依靠
setnx
(set if not exists)指令,用完了再調用del
指令來釋放位置。在1中,如果邏輯執行到中間出現異常,可能導致
del
未調用,這就陷入死鎖,鎖永遠得不到釋放,因此可以給這個位置增加一個過期時間,這樣即使出現異常也能保證位置會被釋放。在2中,如果在
setnx
expire
中間出現問題導致進程掛掉,則expire
不會執行,同樣造成死鎖。因此出現了分布式鎖的library,但Redis作者從2.8版本開始加入了set
指令的拓展參數,因此可以通過set key value ex seconds nx
指令來合並nx和expire為原子操作,這就是奧義。
超時問題
如果加解鎖之間的邏輯執行時間超出過期時間,則會導致這個鎖被其它進程獲取,而其它進程執行邏輯時,若第一個邏輯執行完,它將調用解鎖操作,則會導致第二個程序還沒運行完鎖就被釋放。
為了解決後一個問題,引入tag標簽來標記鎖,設置一個隨機數作為鎖的value,在釋放鎖時要匹配該隨機數,但Redis沒有提供匹配的方法,因此需要Lua腳本來處理。(Lua腳本可以保證連續多個指令的原子性執行)
以上並沒有完美解決超時問題,只是相對安全一點。
可重入性
可重入性指線程在持有鎖的情況下再次請求加鎖,如果一個鎖支持同一個線程的多次加鎖,則這個鎖就是可重入的。這是為了解決超時問題,若超時判斷邏輯是否執行完,未完則再加鎖,直到由代碼調用解鎖。
Redis分布式鎖要支持可重入,則需對客戶端set
方法進行包裝,用線程的Treadlocal變量存儲當前持有鎖的計數。
import redis import threading locks = threading.local() locks.redis = {} def key_for(user_id): return "account_{}".format(user_id) def _lock(client, key): return bool(client.set(key, True, nx=True, ex=5)) def _unlock(client, key): client.delete(key) def lock(client, user_id): key = key_for(user_id) if key in locks.redis: locks.redis[key] += 1 return True ok = _lock(client, key) if not ok: return False locks.redis[key] = 1 return True def unlock(client, user_id): key = key_for(user_id) if key in locks.redis: locks.redis[key] -= 1 if locks.redis[key] <= 0: _unlock(client, key) return True return False client = redis.StrictRedis()
不推薦使用可重入鎖,其加重了客戶端的復雜性,調整邏輯結構完全可以不使用可重入鎖。
深入Redis(一)分布式鎖