摘要:分布式鎖也有類似的首先獲取鎖,然后執行操作,最后釋放鎖動作,但這種鎖既不是給同一個進程中的多個線程使用,也不是給同一臺機器上的多個進程使用,而是由不同機器上的不同客戶端進行獲取和釋放的。
一般來說,在對數據進行“加鎖”時,程序首先需要通過獲取(acquire)鎖來得到對數據進行排他性訪問的能力,然后才能對數據執行一系列操作,最后還要釋放(release)給其他程序。對于能夠被多個線程訪問的共享內存數據結構(shared-memory data structure)來說,這種“先獲取鎖,然后執行操作,最后釋放鎖”的動作非常常見。Redis使用WATCH命令來代替對數據進行加鎖,因為WATCH只會在數據被其他客戶端搶先修改了的情況下通知執行了這個命令的客戶端,而不會阻止其他客戶端對數據的修改,所以這個命令被稱為樂觀鎖(optimistic locking)。
分布式鎖也有類似的“首先獲取鎖,然后執行操作,最后釋放鎖”動作,但這種鎖既不是給同一個進程中的多個線程使用,也不是給同一臺機器上的多個進程使用,而是由不同機器上的不同Redis客戶端進行獲取和釋放的。
為了防止客戶端在取得鎖之后崩潰,并導致鎖一直處于“已被獲取”的狀態,最終版的鎖實現將帶有超時限制特性:如果獲得鎖的進程未能在指定的時限內完成操作,那么鎖將自動釋放。
導致鎖出現不正確行為的原因,以及鎖在不正確運行時的癥狀:
持有鎖的進程因為操作時間過長而導致鎖被自動釋放,但進程本身并不知曉這一點,甚至還可能會錯誤地釋放掉了其他進程持有的鎖。
一個持有鎖并打算執行長時間操作的進程已經崩潰,但其他想要獲取鎖的進程不知道哪個進程持有著鎖,也無法檢測出持有鎖的進程已經崩潰,只能白白地浪費時間等待鎖被釋放。
在一個進程持有的鎖過期之后,其他多個進程同時嘗試去獲取鎖,并且都獲得了鎖。
上面第一種情況和第三種情況同時出現,導致有多個進程獲得了鎖,而每個進程都以為自己是唯一一個獲得鎖的進程。
簡易鎖
為了對數據進行排他性訪問,程序首先要做的就是獲取鎖。SETNX命令天生就適合用來實現鎖的獲取功能,這個命令只會在鍵不存在的情況下為鍵賦值,而鎖要做的就是將一個隨機生成的128位UUID設置為鍵的值,并使用這個值來防止鎖被其他進程取得。
如果程序嘗試獲取鎖的時候失敗,那么它將不斷地進行重試,直到成功地取得鎖或者超過給定的時限為止。
def acquire_lock(conn, lockname, acquire_timeout=10): identifier = str(uuid.uuid4()) //128位隨機標識符 end = time.time() + acquire_timeout while time.time() < end: if conn.setnx("lock:" + lockname, identifier): //嘗試獲取鎖 return identifier time.sleep(.001) return False
下面代碼展示了使用鎖重新實現的商品購買操作:程序首先對市場進行加鎖,接著檢查商品的價格,并在確保買家有足夠的錢來購買商品之后,對錢和商品進行相應的轉移。當操作執行完之后,程序就會釋放鎖。
def purchase_item_with_lock(conn, buyerid, itemid, sellerid): buyer = "users:%s"%buyerid sellerid = "users:%s"%sellerid item = "%s.%s"%(itemid, sellerid) inventory = "inventory:%s"%buyerid locked = acquire_lock(conn, market) if not locked: return False pipe = conn.pipeline(True) try://檢查指定的商品是否仍在出售,以及買家是否有足夠的錢來購買該商品 pipe.zscore("market:", item) pipe.hget(buyer, "funds") price, funds = pipe.execute() if price is None or price > funds: return None pipe.hincrby(seller, "funds", int(price)) pipe.hincrby(buyer, "funds", int(-price)) pipe.sadd(inventory, itemid) pipe.zrem("market:", item) pipe.execute() return True finally: release_lock(conn, market, locked) //釋放鎖
上面代碼的鎖似乎是用來加鎖整個購買操作的,但實際上這把鎖是用來鎖住市場數據的,它之所以會包圍著執行購買操作的代碼,是因為程序在操作市場數據期間必須一直持有鎖。
接下面的代碼release_lock函數展示了鎖釋放操作的實現代碼:函數首先使用WATCH命令監視代表鎖的鍵,接著檢查鍵目前的值是否和加鎖時設置的值相同,并且確認值沒有變化之后刪除該鍵(這個檢查還可以防止程序錯誤地釋放同一個鎖多次)。
def release_lock(conn, lockname, identifier): pipe = conn.pipeline(True) lockname = "lock:" + lockname while True: try: pipe.watch(lockname) if pipe.get(lockname) == identifier: pipe.multi() pipe.delete(lockname) pipe.execute() return True pipe.unwatch() break except redis.exceptions.WatchError: pass return False
經過測試,與之前WATCH實現相比,鎖實現的上架商品數量雖然有所減少,但是在買入商品時卻不需要進行重試,并且上架商品數量和買入商品數量之間的比率,也跟賣家數量和買家數量之間的比率接近。
帶有超時限制的鎖
目前的鎖實現在持有者崩潰的時候不會自動釋放,這將導致鎖一直處于已被獲取的狀態。為了解決這個問題,我們將為鎖加上超時功能。
為了給鎖加上超時的限制特性,程序將在取得鎖之后,調用EXPIRE命令來為鎖設置過期時間,使得Redis可以自動刪除超時的鎖。為了確保鎖在客戶端已經崩潰(客戶端在執行介于SETNX和EXPIRE之間的時候崩潰是最糟糕的)的情況下仍然能夠自動被釋放,客戶端會嘗試獲取鎖失敗之后,檢查鎖的超時時間,并為未設置超時時間的鎖設置超時時間。因為鎖總會帶有超時時間,并最終因為超時而自動被釋放,使得其他客戶端可以繼續嘗試獲取已被釋放的鎖。
需要注意的一點是,因為多個客戶端在同一時間內設置的超時時間基本上都是相同的,所以即使有多個客戶端同時為同一個鎖設置超時時間,鎖的超時時間也不會產生太大變化。
def acquire_lock_with_timeout(conn, lockname, acquire_timeout=10, lock_timeout=10): identifier = str(uuid.uuid4()) lockname = "lock:" + lockname lock_timeout = int(math.ceil(lock_timeout)) end = time.time() + acquire_timeout while time.time() < end: if conn.setnx(lockname, identifier): conn.expire(lockname, lock_timeout) return identifier elif not conn.ttl(lockname): conn.expire(lockname, lock_timeout) time.sleep(.001) return False
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/44609.html
摘要:集群實現分布式鎖上面的討論中我們有一個非常重要的假設是單點的。但是其實這已經超出了實現分布式鎖的范圍,單純用沒有命令來實現生成。這個問題用實現分布式鎖暫時無解。結論并不能實現嚴格意義上的分布式鎖。 關于Redis實現分布式鎖的問題,網絡上很多,但是很多人的討論基本就是把原來博主的貼過來,甚至很多面試官也是一知半解經不起推敲就來面候選人,最近結合我自己的學習和資料查閱,整理一下用Redi...
摘要:分布式鎖的作用在單機環境下,有個秒殺商品的活動,在短時間內,服務器壓力和流量會陡然上升。分布式集群業務業務場景下,每臺服務器是獨立存在的。這里就用到了分布式鎖這里簡單介紹一下,以的事務機制來延生。 Redis 分布式鎖的作用 在單機環境下,有個秒殺商品的活動,在短時間內,服務器壓力和流量會陡然上升。這個就會存在并發的問題。想要解決并發需要解決以下問題 1、提高系統吞吐率也就是qps 每...
閱讀 867·2021-10-25 09:45
閱讀 3284·2021-09-22 14:58
閱讀 3844·2021-08-31 09:43
閱讀 915·2019-08-30 15:55
閱讀 917·2019-08-29 13:51
閱讀 1225·2019-08-29 13:02
閱讀 3483·2019-08-29 12:52
閱讀 1961·2019-08-26 13:27