漫談Redis分散式鎖實現

来源:https://www.cnblogs.com/wy123/archive/2019/07/06/11141017.html
-Advertisement-
Play Games

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

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Oracle異常處理在PL/SQL語句書寫時,需要處理的異常-- 不做異常處理時DECLARE v_name emp.ename%TYPE; v_sal emp.sal%TYPE;BEGIN SELECT ename,sal INTO v_name,v_sal FROM emp WHERE empn ...
  • 資料庫之MySQL與Python交互,內容包括 準備數據,SQL演練,資料庫的設計,Python 中操作 MySQL 步驟,pymysql增刪改查,參數化。 ...
  • [TOC] 資料庫的存儲引擎 什麼是存儲引擎? MySQL中的數據用各種不同的技術存儲在文件(或者記憶體)中。這些技術中的每一種技術都使用不同的存儲機制、 索引技巧、鎖定水平並且最終提供廣泛的不同的功能和能力。通過選擇不同的技術,你能夠獲得額外的速度或者功能, 從而改善你的應用的整體功能。在MySQL ...
  • 資料庫之MySQL查詢,內容包括 查詢,條件查詢,排序,聚合函數,分組,分頁,鏈接查詢,自關聯,子查詢,總結。 ...
  • 初始SQL語句 簡單使用 SQL語言共分為四大類: DQL (Data QueryLanguage )數據查詢語言 DML(Data manipulation language)數據操縱語言 DDL(Data definition language)資料庫定義語言 DCL(Data Control ...
  • mysql 安裝與基本管理 [TOC] MySQL介紹 mysql是什麼 資料庫管理軟體分類 下載安裝 Linux版本 源碼安裝mariadb Window版本 上一步解決了一些問題,但不夠徹底,因為在執行【mysqd】啟動MySQL伺服器時,當前終端會被hang住,那麼做一下設置即可解決此問題,即 ...
  • Mysql數據簡單入門 [TOC] 資料庫管理系統 DBMS 什麼是資料庫? 資料庫優點? 資料庫的優勢? 資料庫運行示意圖 專業名詞: DBMS 資料庫管理系統 (英語:Database Management System) 數據 data 文件夾 資料庫database 簡稱db 資料庫管理員 ...
  • ''' SELECT a.id, substring_index(substring_index(a.bill_ids, ',', b.id), ',', 1) bill_ids, a.status, a.card_name FROM (SELECT id, bill_ids,status,card ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...