漫談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
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...