redis實現分布式鎖——核心 setx+pipe watch監控key變化-事務


如何設計一把分布式鎖

我們用 redis 來實現這把分布式的鎖,redis 速度快、支持事務、可持久化的特點非常適合創建分布式鎖。

分布式環境中如何消除網絡延遲對鎖獲取的影響

鎖,簡單來說就是存於 redis 中一個唯一的 key。一般而言,redis 用 set 命令來完成一個 key 的設置(加鎖),使用 get 命令獲取 key 的信息(檢查鎖)。由於網絡延遲的存在,簡單的使用 setget 命令可能會帶來如下問題:

線程 A 檢查鎖是否存在(get)–>否–>加鎖(set),在 A 發起加鎖命令但是還沒有加鎖成功的時候,可能線程 B 已經完成了 set 操作,鎖被 B 獲得,但是 A 也發起了加鎖請求,由於 set 命令並不檢查 key 的存在,B 的鎖很可能會被 A 的 set 操作破壞。

幸運的是,redis 提供了另一個命令 setx : 當指定的 key 不存在時,設置 key 的值為指定 value,如果存在,不做任何操作,成功則返回 1,失敗則返回 0。也就是只要命令返回成功,線程就能正確獲得鎖,不需要再做類似 get 檢查操作。

使用 setx 可以消除網絡延遲對鎖設置的影響。

加鎖的客戶端發生 crash 導致鎖不能被正確釋放應該怎么處理?

加鎖成功並操作完成時候,就需要加鎖線程對鎖進行釋放,以讓出資源的控制權。釋放鎖,簡單來說就是刪除 redis 中這個唯一的 key,但是一定要保證刪除的這個 key 是該線程創建的,因而鎖創建時必須攜帶執行線程的唯一特征以標示創建者的身份。

如果加鎖的線程出現異常 crash 了而不能及時刪除鎖,則會導致鎖一直無法被正確釋放,資源處於一直被占有,別的線程處於一直等待的狀態。為了避免這樣的情況發生,鎖一定要在異常發生之后 可以自己釋放,以讓出資源的控制權,可以使用 redis 的超時機制來達到這個目的。超時時間視不同的業務場景而定,一般是最大允許等待時間。需要注意的是,只有在加鎖成功之后才可以對 key 設置 TTL,否則很容易導致 key 被多個線程不斷設置 TTL 而無法過期。

if CONN.setnx(lockname, identifier): CONN.expire(lockname, timeout)

加鎖之后如何有效監測鎖是否被篡改?

redis 提供了 pipeline 和事務操作來保證多個命令可以在一個事務內全部完成從而減少多次網絡請求帶來的開銷,watch 命令又可以在事務開始執行之前對所要操作的 key 執行監測,從而保證了事務的完整性和一致性。因此,為了防止鎖篡改,可以在加鎖完成之后對鎖進行 watch 操作,一旦鎖發生變化,則終止事務,回滾操作。

pipe = CONN.pipeline(True) pipe.watch(lock)

提供鎖的宿主機( redis 服務器) crash 導致鎖不能被正確建立和釋放該如何處理?**

不論是通信故障或是服務器故障而導致的鎖服務器無法響應,此時都會導致客戶端加鎖和釋放鎖的請求無法完成,因此一定要有相應的應急處理,以確保程序流程的完整體驗,加強客戶端的健壯性。比如相應的超時提示,異常告警等。

哪些邊界需要注意

1.只有鎖正確釋放才算是整個事務的完整結束,如果鎖釋放失敗,比如被篡改、鎖服務器異常等,不同的業務可以根據自己的需求進行變動和調整。

2.設置 TTL 一定要是加鎖成功之后,否則所有獲取鎖的客戶端都會嘗試 TTL 導致鎖無法過期。

3.鎖的過期時間也就是獲取鎖的客戶端的最大等待時間,這個時間根據執行的事務能夠容忍的最長時間為限

一個簡單的 python 實現

import time import redis import logging logger = logging.getLogger('service.redis_lock') CONN = redis.Redis(host='localhost') def acquire_lock(lockname, identifier, wait_time=20, timeout=15): end = time.time() + wait_time while end > time.time(): if CONN.setnx(lockname, identifier): CONN.expire(lockname, timeout) # set expire time return identifier time.sleep(0.001) #wait until the lock expired or release by some thread return False def release_lock(lockname, identifier): pipe = CONN.pipeline(True) try: #watch lock once lock has been changed, break this transaction pipe.watch(lockname) #check if lock has been changed if pipe.get(lockname) == identifier: pipe.multi() pipe.delete(lockname) pipe.execute() return True pipe.unwatch() #execu when identifier not equal except redis.exceptions.WatchError as e: logger.error(e) return False except Exception as e: logger.error(e) return False return False if __name__ == '__main__': print release_lock('h', 'a')

轉自:https://gold.xitu.io/entry/57bae53f5bbb500063fedf31


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM