漫談Redis分佈式鎖實現

 

在Redis上,能夠經過對key值的獨佔來實現分佈式鎖,表面上看,Redis能夠簡單快捷經過set key這一獨佔的方式來實現,也有許多重複性輪子,但實際狀況並不是如此。
總得來講,Redis實現分佈式鎖,如何確保鎖資源的安全&及時釋放,是分佈式鎖的最關鍵因素。
以下逐層分析Redis實現分佈式鎖的一些過程,以及存在的問題和解決辦法。html

 

solution 1 :setnxpython

setnx命令設置key的方式實現獨佔鎖redis

1,#併發線程搶佔鎖資源
setnx an_special_lock 1
2,#若是1搶佔到當前鎖,併發線程中的當前線程執行
if(成功獲取鎖)
  execute business_method()
  3,#釋放鎖
  del an_special_locksql

存在的問題很明顯:
從搶佔鎖,而後併發線程中當前的線程操做,到最後的釋放鎖,並非一個原子性操做,
若是最後的鎖沒有被成功釋放(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操做,保證以原子的方式執行?app

 

solution 3 : set key value ex 10 nxdom

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分佈式鎖最大的精髓所在。

相關文章
相關標籤/搜索