1. 程式人生 > >深入Redis(一)分布式鎖

深入Redis(一)分布式鎖

參數 包裝 情況 變量 clas return 一個 set 標簽

分布式鎖

由於分布式應用在邏輯處理時存在並發問題,比方修改數據,要先讀取到內存,在內存中修改後再保存回去,這兩個操作是單獨的,如果同時進行,就會出現並發問題。

此時就要用到分布式鎖來限制程序的並發執行。

本質

本質就是在Redis內占一個位置,若別的進程也想占用該位置,發現有進程在使用該位置,就放棄或等待。

  1. 在Redis中實現依靠setnx(set if not exists)指令,用完了再調用del指令來釋放位置。

  2. 在1中,如果邏輯執行到中間出現異常,可能導致del未調用,這就陷入死鎖,鎖永遠得不到釋放,因此可以給這個位置增加一個過期時間,這樣即使出現異常也能保證位置會被釋放。

  3. 在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(一)分布式鎖