在Redis上,可以通過對key值的獨占來實現分散式鎖,錶面上看,Redis可以簡單快捷通過set key這一獨占的方式來實現分散式鎖,也有許多重覆性輪子,但實際情況並非如此。總得來說,Redis實現分散式鎖,如何確保鎖資源的安全&及時釋放,是Redis實現分散式鎖的最關鍵因素。如下逐層分析Redi ...
在Redis上,可以通過對key值的獨占來實現分散式鎖,錶面上看,Redis可以簡單快捷通過set key這一獨占的方式來實現,也有許多重覆性輪子,但實際情況並非如此。
總得來說,Redis實現分散式鎖,如何確保鎖資源的安全&及時釋放,是分散式鎖的最關鍵因素。
如下逐層分析Redis實現分散式鎖的一些過程,以及存在的問題和解決辦法。
solution 1 :setnx
setnx命令設置key的方式實現獨占鎖
1,#併發線程搶占鎖資源
setnx an_special_lock 1
2,#如果1搶占到當前鎖,併發線程中的當前線程執行
if(成功獲取鎖)
execute business_method()
3,#釋放鎖
del an_special_lock
存在的問題很明顯:
從搶占鎖,然後併發線程中當前的線程操作,到最後的釋放鎖,並不是一個原子性操作,
如果最後的鎖沒有被成功釋放(del an_special_lock),也即2~3之間發生了異常,就會造成其他線程永遠無法重新獲取鎖
solution 2:setnx + expire key
為了避免solution 1中這種情況的出現,需要對鎖資源加一個過期時間,比如是10秒鐘,一旦從占鎖到釋放鎖的過程發生異常,可以保證過期之後,鎖資源的自動釋放
1,#併發線程搶占鎖資源
setnx an_special_lock 1
2,#設置鎖的過期時間
expire an_special_lock 10
3,#如果1搶占到當前鎖,併發線程中的當前線程執行
if(成功獲取鎖)
execute business_method()
4,#釋放鎖
del an_special_lock
通過設置過期時間(expire an_special_lock 10),避免了占鎖到釋放鎖的過程發生異常而導致鎖無法釋放的問題,
但是仍舊存在問題:
在併發線程搶占鎖成功到設置鎖的過期時間之間發生了異常,也即這裡的1~2之間發生了異常,鎖資源仍舊無法釋放
solution 2雖然解決了solution 1中鎖資源無法釋放的問題,但與此同時,又引入了一個非原子操作,同樣無法保證set key到expire key的以原子的方式執行
因此目前問題集中在:如何使得設置一個鎖&&設置鎖超時時間,也即這裡的1~2操作,保證以原子的方式執行?
solution 3 : set key value ex 10 nx
Redis 2.8之後加入了一個set key && expire key的原子操作:set an_special_lock 1 ex 10 nx
1,#併發線程搶占鎖資源,原子操作 set an_special_lock 1 ex 10 nx 2,#如果1搶占到當前鎖,併發線程中的當前線程執行 if(成功獲取鎖)
business_method() 3,#釋放鎖 del an_special_lock
目前,加鎖&&設置鎖超時,成為一個原子操作,可以解決當前線程異常之後,鎖可以得到釋放的問題。
但是仍舊存在問題:
如果在鎖超時之後,比如10秒之後,execute_business_method()仍舊沒有執行完成,此時鎖因過期而被動釋放,其他線程仍舊可以獲取an_special_lock的鎖,併發線程對獨占資源的訪問仍無法保證。
solution 4: 業務代碼加強
到目前為止,solution 3 仍舊無法完美解決併發線程訪問獨占資源的問題。
筆者能夠想到解決上述問題的辦法就是:
設置business_method()執行超時時間,如果應用程式中在鎖超時的之後仍無法執行完成,則主動回滾(放棄當前線程的執行),然後主動釋放鎖,而不是等待鎖的被動釋放(超過expire時間釋放)
如果無法確保business_method()在鎖過期放之前得到成功執行或者回滾,則分散式鎖仍是不安全的。
1,#併發線程搶占鎖資源,原子操作 set an_special_lock 1 ex 10 n 2,#如果搶占到當前鎖,併發線程中的當前線程執行 if(成功獲取鎖)
business_method()#在應用層面控制,業務邏輯操作在Redis鎖超時之前,主動回滾 3,#釋放鎖 del an_special_lock
solution 5 RedLock: 解決單點Redis故障
截止目前,(假如)可以認為solution 4解決“占鎖”&&“安全釋放鎖”的問題,仍舊無法保證“鎖資源的主動釋放”:
Redis往往通過Sentinel或者集群保證高可用,即便是有了Sentinel或者集群,但是面對Redis的當前節點的故障時,仍舊無法保證併發線程對鎖資源的真正獨占。
具體說就是,當前線程獲取了鎖,但是當前Redis節點尚未將鎖同步至從節點,此時因為單節點的Cash造成鎖的“被動釋放”,應用程式的其它線程(因故障轉移)在從節點仍舊可以占用實際上並未釋放的鎖。
Redlock需要多個Redis節點,RedLock加鎖時,通過多數節點的方式,解決了Redis節點故障轉移情況下,因為數據不一致造成的鎖失效問題。
其實現原理,簡單地說就是,在加鎖過程中,如果實現了多數節點加鎖成功(非集群的Redis節點),則加鎖成功,解決了單節點故障,發生故障轉移之後數據不一致造成的鎖失效。
而釋放鎖的時候,僅需要向所有節點執行del操作。
Redlock需要多個Redis節點,由於從一臺Redis實例轉為多台Redis實例,Redlock實現的分散式鎖,雖然更安全了,但是必然伴隨著效率的下降。
至此,從solution 1-->solution 2-->solution 3--solution 4-->solution 5,依次解決個前一步的問題,但仍舊是一個非完美的分散式鎖實現。
以下通過一個簡單的測試來驗證Redlock的效果。
case是一個典型的對資料庫“存在則更新,不存在則插入的”併發操作(這裡忽略資料庫層面的鎖),通過對比是否通過Redis分散式鎖控制來看效果。
#!/usr/bin/env python3 import redis import sys import time import uuid import threading from time import ctime,sleep from redis import StrictRedis from redlock import Redlock from multiprocessing import Pool import pymssql import random class RedLockTest: _connection_list = None _lock_resource = None _ttl = 10 #ttl def __init__(self, *args, **kwargs): for k, v in kwargs.items(): setattr(self, k, v) def get_conn(self): try: #如果當前線程獲取不到鎖,重試次數以及重試等待時間 conn = Redlock(self._connection_list,retry_count=100, retry_delay=10 ) except: raise return conn def execute_under_lock(self,thread_id): conn = self.get_conn() lock = conn.lock(self._lock_resource, self._ttl) if lock : self.business_method(thread_id) conn.unlock(lock) else: print("try later") ''' 模擬一個經典的不存在則插入,存在則更新,起多線程併發操作 實際中可能是一個非常複雜的需要獨占性的原子性操作 ''' def business_method(self,thread_id): print(" thread -----{0}------ execute business method begin".format(thread_id)) conn = pymssql.connect(host="127.0.0.1",server="SQL2014", port=50503, database="DB01") cursor = conn.cursor() id = random.randint(0, 100) sql_script = ''' select 1 from TestTable where Id = {0} '''.format(id) cursor.execute(sql_script) if not(cursor.fetchone()): sql_script = ''' insert into TestTable values ({0},{1},{1},getdate(),getdate()) '''.format(id,thread_id) else: sql_script = ''' update TestTable set LastUpdateThreadId ={0} ,LastUpdate = getdate() where Id = {1} '''.format(thread_id,id) cursor.execute(sql_script) conn.commit() cursor.close() conn.close() print(" thread -----{0}------ execute business method finish".format(thread_id)) if __name__ == "__main__": redis_servers = [{"host": "*.*.*.*","port": 9000,"db": 0}, {"host": "*.*.*.*","port": 9001,"db": 0}, {"host": "*.*.*.*","port": 9002,"db": 0},] lock_resource = "mylock" ttl = 2000 #毫秒 redlock_test = RedLockTest(_connection_list = redis_servers,_lock_resource=lock_resource, _ttl=ttl) #redlock_test.execute_under_lock(redlock_test.business_method) threads = [] for i in range(50): #普通的併發模式調用業務邏輯的方法,會產生大量的主鍵衝突 #t = threading.Thread(target=redlock_test.business_method,args=(i,)) #Redis分散式鎖控制下的多線程 t = threading.Thread(target=redlock_test.execute_under_lock,args=(i,)) threads.append(t) begin_time = ctime() for t in threads: t.setDaemon(True) t.start() for t in threads: t.join()
測試 1,簡單多線程併發
簡單地起多線程執行測試的方法,測試中出現兩個很明顯的問題
1,出現主鍵衝突(而報錯)
2,從列印的日誌來看,各個線程在測試的方法中存在交叉執行的情況(日誌信息的交叉意味著線程的交叉執行)
測試 2,Redis鎖控制下多線程併發
Redlock的Redis分散式鎖為三個獨立的Redis節點,無需做集群
當加入Redis分散式鎖之後,可以看到,雖然是併發多線程操作,但是在執行實際的測試的方法的時候,都是獨占性地執行,
從日誌也能夠看出來,都是一個線程執行完成之後,另一個線程才進入臨界資源區。
Redlock相對安全地解決了一開始分散式鎖的潛在問題,與此同時,也增加了複雜度,同時在一定程度上降低了效率。
以上粗淺分析了Redis分散式鎖的各種實現以及潛在問題,即便是Redlock,也不是一個完美的分散式鎖解決方案,關於Redis的Redlock的爭議也有
http://zhangtielei.com/posts/blog-redlock-reasoning.html
仔細閱讀會發現,恰恰這些“爭議”本身,才是Redis分散式鎖最大的精髓所在。