1. 程式人生 > 程式設計 >一種用django_cache實現分散式鎖的方式

一種用django_cache實現分散式鎖的方式

問題背景

在專案開發過程中,我遇到一個需求:對於某條記錄,一個使用者對它進行操作時會持續比較久,希望在一個使用者的操作期間,不允許有另一個使用者操作它,否容易會出現混亂。

在與同事們討論後,想通過加鎖的方式,起初想用redis鎖,但這樣會為專案增加別的依賴,因此轉而使用django-cache的快取資料庫,來實現該功能。

資料查詢

基於快取實現分散式鎖,在網路上查找了實現方式,大概可以總結為以下3種:

第一種鎖命令INCR

這種加鎖的思路是, key 不存在,那麼 key 的值會先被初始化為 0 ,然後再執行 INCR 操作進行加一。 然後其它使用者在執行 INCR 操作進行加一時,如果返回的數大於 1 ,說明這個鎖正在被使用當中。

第二種鎖命令SETNX

這種加鎖的思路是,如果 key 不存在,將 key 設定為 value 如果 key 已存在,則 SETNX 不做任何動作

第三種鎖命令SET

上面兩種方法都有一個問題,會發現,都需要設定 key 過期。那麼為什麼要設定key過期呢?如果請求執行因為某些原因意外退出了,導致建立了鎖但是沒有刪除鎖,那麼這個鎖將一直存在,以至於以後快取再也得不到更新。於是乎我們需要給鎖加一個過期時間以防不測。

在實際編寫中,我綜合了第二種和第三種方式,即用鍵名來設定鎖,同時設定了過期時間,以防長時間佔用。

另外,關於如何使用django-cache去使用資料庫快取,相關的API整理如下:

from django.core.cache import caches
# 設定鎖和超時時間
cache.set('my_key','Initial value',60)
# 獲取鎖
cache.get('my_key')
# 更新鎖
cache.add('add_key','New value')
複製程式碼

程式碼編寫

在經過多次的迭代,並且對比了網上的各路寫法後,我結合django-cache的特性,最終總結了一套較為簡潔的寫法。

首先是一個CacheLock的類,初始化方法裡可以傳執行超時時間,和拿鎖等待的時間。CacheLock類的主要方法有兩個,一個是拿鎖的方法,一個是釋放鎖的方法。

拿鎖的方法中,鍵名根據操作的具體物件來定,鍵值為uuid值,超時時間預設為60s。一旦發現能拿到鎖,則返回uuid值。

釋放鎖的方法中,首先比較鍵值和uuid值是否一致,一致則釋放,避免因超時情況導致把其他的正在操作的鎖給釋放掉。

class CacheLock(object):
    def __init__(self,expires=60,wait_timeout=0):
        self.cache = cache
        self.expires = expires  # 函式執行超時時間
        self.wait_timeout = wait_timeout  # 拿鎖等待超時時間

    def get_lock(self,lock_key):
        # 獲取cache鎖
        wait_timeout = self.wait_timeout
        identifier = uuid.uuid4()
        while wait_timeout >= 0:
            if self.cache.add(lock_key,identifier,self.expires):
                return identifier
            wait_timeout -= 1
            time.sleep(1)
        raise LockTimeout({'msg': '當前有其他使用者正在編輯該採集配置,請稍後重試'})

    def release_lock(self,lock_key,identifier):
        # 釋放cache鎖
        lock_value = self.cache.get(lock_key)
        if lock_value == identifier:
            self.cache.delete(lock_key)
複製程式碼

另外,將快取鎖寫成一個裝飾器,對需要加鎖的地方,新增上該裝飾器,則可以很輕鬆地實現鎖功能。

def lock(cache_lock):
    def my_decorator(func):
        def wrapper(*args,**kwargs):
            lock_key = 'bk_monitor:lock:xxx' # 具體的lock_key要根據呼叫時傳的引數而定
            identifier = cache_lock.get_lock(lock_key)
            try:
                return func(*args,**kwargs)
            finally:
                cache_lock.release_lock(lock_key,identifier)
        return wrapper
    return my_decorator
複製程式碼

再舉一個實際呼叫中的例子:

@lock(CacheLock())
def f():
    pass
複製程式碼

另外,我在設定快取的key名的時候,會根據函式的具體操作物件,從而給裝飾器傳遞相應的引數,這裡就不再舉例了。

優化改進

當然,實現以上功能需求一定還有別的更好的方式,關於鎖的實現,網路上有很多別的方式,比如基於zookeeper實現分散式鎖、基於資料庫實現分散式鎖等等,它們在可靠性或效能方面都各有長短,要根據具體場景進行取捨,所以還有非常多值得研究的地方。

我這裡也只是拋磚引玉,歡迎拍磚~

參考資料

zhuanlan.zhihu.com/p/42056183

www.hollischuang.com/archives/17…

www.jianshu.com/p/182b5ff76…

www.cnblogs.com/huim/p/1086…

chris-lamb.co.uk/posts/distr…

gist.github.com/adewes/6103…

docs.djangoproject.com/en/1.8/topi…