1. 程式人生 > >漫談Redis分散式鎖實現

漫談Redis分散式鎖實現

 

在Redis上,可以通過對key值的獨佔來實現分散式鎖,表面上看,Redis可以簡單快捷通過set key這一獨佔的方式來實現分散式鎖,也有許多重複性輪子,但實際情況並非如此。
總得來說,Redis實現分散式鎖,如何確保鎖資源的安全&及時釋放,是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分散式鎖最大的精髓所在。

&n