2、分布式鎖實現原理與最佳實踐(二)

常見分布式鎖的原理

4.1 Redisson

Redis 2.6之后才可以執行lua腳本,比起管道而言,這是原子性的,模擬一個商品減庫存的原子操作:

//lua腳本命令執行方式:redis-cli --eval /tmp/test.lua , 10
jedis.set("product_stock_10016", "15");  
//初始化商品10016的庫存
String script = " local count = redis.call('get', KEYS[1]) " +" local a = tonumber(count) " +" local b = tonumber(ARGV[1]) " +" if a >= b then " +"   redis.call('set', KEYS[1], a-b) " +"   return 1 " +" end " +" return 0 ";
Object obj = jedis.eval(script, Arrays.asList("product_stock_10016"), Arrays.asList("10"));
System.out.println(obj);

在這里插入圖片描述
4.1.1 嘗試加鎖的邏輯
在這里插入圖片描述
上面的org.redisson.RedissonLock#lock()通過調用自己方法內部的lock方法的org.redisson.RedissonLock#tryAcquire方法。之后調用 org.redisson.RedissonLock#tryAcquireAsync:
在這里插入圖片描述
首先調用內部的org.redisson.RedissonLock#tryLockInnerAsync:設置對應的分布式鎖
在這里插入圖片描述
到這里獲取鎖的邏輯就結束了,如果這里沒有獲取到,在Future的回調里面就會直接return,會在外層有一個while true的循環,訂閱釋放鎖的消息準備被喚醒。如果說加鎖成功,就開始執行鎖續命邏輯。
在這里插入圖片描述
4.1.2 鎖續命邏輯
lua腳本最后是以毫秒為單位返回key的剩余過期時間。成功加鎖之后org.redisson.RedissonLock#scheduleExpirationRenewal中將會調用org.redisson.RedissonLock#renewExpiration,這個方法內部就有鎖續命的邏輯,是一個定時任務,等10s執行。
執行的時候嘗試執行的續命邏輯使用的是Lua腳本,當前的鎖有值,就續命,沒有就直接返回0:
在這里插入圖片描述
返回0之后外層會判斷,延時成功就會再次調用自己,否則延時調用結束,不再為當前的鎖續命。所以這里的續命不是一個真正的定時,而是循環調用自己的延時任務。
在這里插入圖片描述
4.1.3 循環間隔搶鎖機制
如果一開始就加鎖成功就直接返回。
如果一開始加鎖失敗,沒搶到鎖的線程就會在while循環中嘗試加鎖,加鎖成功就結束循環,否則等待當前鎖的超時時間之后再次嘗試加鎖。所以實現邏輯默認是非公平鎖:
在這里插入圖片描述
里面有一個subscribe的邏輯,會監聽對應加鎖的key,當鎖釋放之后publish對應的消息,此時如果沒有到達對應的鎖的超時時間,也會嘗試獲取鎖,避免時間浪費。
4.1.4 釋放鎖和喚醒其他線程的邏輯
前面沒有搶到鎖的線程會監聽對應的queue,后面搶到鎖的線程釋放鎖的時候會發送一個消息。
在這里插入圖片描述
訂閱的時候指定收到消息時候的邏輯:會喚醒阻塞之后執行while循環

4.1.5 重入鎖的邏輯
存在對應的鎖,就對對應的hash結構的value直接+1,和Java重入鎖的邏輯是一致的。
在這里插入圖片描述

4.2 RedLock解決非單體項目的Redis主從架構的鎖失效

https://redis.io/docs/manual/patterns/distributed-locks/
查看Redis官方文檔,對于單節點的Redis ,使用setnx和lua del刪除分布式鎖是足夠的,但是主從架構的場景下:鎖先加在一個master節點上,默認是異步同步到從節點,此時master掛了會選擇slave為master,此時又可以加鎖,就會導致超賣。但是如果使用zookeeper來實現的話,由于zk是CP的,所以CP不存在這樣的問題。
Redis文檔中給出了RedLock的解決辦法,使用redLock真的可以解決嗎?
4.2.1 RedLock 原理
基于客戶端的實現,是基于多個獨立的Redis Master節點的一種實現(一般為5)。client依次向各個節點申請鎖,若能從多數個節點中申請鎖成功并滿足一些條件限制,那么client就能獲取鎖成功。它通過獨立的N個Master節點,避免了使用主備異步復制協議的缺陷,只要多數Redis節點正常就能正常工作,顯著提升了分布式鎖的安全性、可用性。
在這里插入圖片描述
注意圖中所有的節點都是master節點。加鎖超過半數成功,就認為是成功。具體流程:
獲取鎖

獲取當前時間T1,作為后續的計時依據;

按順序地,依次向5個獨立的節點來嘗試獲取鎖 SET resource_name my_random_value NX PX 30000;

計算獲取鎖總共花了多少時間,判斷獲取鎖成功與否;

時間:T2-T1;

多數節點的鎖(N/2+1);

當獲取鎖成功后的有效時間,要從初始的時間減去第三步算出來的消耗時間;

如果沒能獲取鎖成功,盡快釋放掉鎖。

釋放鎖

向所有節點發起釋放鎖的操作,不管這些節點有沒有成功設置過。

public String redlock() {String lockKey = "product_001";//這里需要自己實例化不同redis實例的redisson客戶端連接,這里只是偽代碼用一個redisson客戶端簡化了RLock lock1 = redisson.getLock(lockKey);RLock lock2 = redisson.getLock(lockKey);RLock lock3 = redisson.getLock(lockKey);/*** 根據多個 RLock 對象構建 RedissonRedLock (最核心的差別就在這里)*/RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);try {/*** waitTimeout 嘗試獲取鎖的最大等待時間,超過這個值,則認為獲取鎖失敗* leaseTime   鎖的持有時間,超過這個時間鎖會自動失效(值應設置為大于業務處理的時間,確保在鎖有效期內業務能處理完)*/boolean res = redLock.tryLock(10, 30, TimeUnit.SECONDS);if (res) {//成功獲得鎖,在這里處理業務}} catch (Exception e) {throw new RuntimeException("lock fail");} finally {//無論如何, 最后都要解鎖redLock.unlock();}return "end";
}

但是,它的實現建立在一個不安全的系統模型上的,它依賴系統時間,當時鐘發生跳躍時,也可能會出現安全性問題。分布式存儲專家Martin對RedLock的分析文章,Redis作者的也專門寫了一篇文章進行了反駁。

Martin Kleppmann:How to do distributed locking

https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html
Antirez:Is Redlock safe?

http://antirez.com/news/101
4.2.2 RedLock 問題一:持久化機制導致重復加鎖
如果是上面的架構圖,一般生產都不會配置AOF的每一條命令都落磁盤,一般會設置一些間隔時間,比如1s,如果ABC節點加鎖成功,有一個節點C恰好是在1s內加鎖,還沒有落盤,此時掛了,就會導致其他客戶端通過CDE又會加鎖成功。
4.2.3 RedLock 問題二:主從下重復加鎖
在這里插入圖片描述
除非多部署一些節點,但是這樣會導致加鎖時間變長,這樣比較下來效果就不如zk了。
4.2.4 RedLock 問題三:時鐘跳躍導致重復加鎖
C節點發生了時鐘跳躍,導致加上的鎖沒有到達實際的超時時間,就被誤以為超時而釋放,此時其他客戶端就可以重復加鎖了。
4.3 Curator

InterProcessMutex 可重入鎖的分析
在這里插入圖片描述
五、業務中使用分布式鎖的注意點

獲取的鎖要設置有效期,假設我們未設置key自動過期時間,在Set key value NX 后,如果程序crash或者發生網絡分區后無法與Redis節點通信,毫無疑問其他 client 將永遠無法獲得鎖,這將導致死鎖,服務出現中斷。
SETNX和EXPIRE命令去設置key和過期時間,這也是不正確的,因為你無法保證SETNX和EXPIRE命令的原子性。
自己使用 setnx 實現Redis鎖的時候,注意并發情況下不要釋放掉別人的鎖(業務邏輯執行時間超過鎖的過期時間),導致惡性循環。一般:
1)加鎖的時候需要指定value的內容是當前進程中的當前線程的唯一標記,不要使用線程ID作為當前線程的鎖的標記,因為不同實例上的線程ID可能是一樣的。
2)釋放鎖的邏輯會寫在finally ,釋放鎖時候要判斷鎖對應的value,而且要使用lua腳本實現原子 del 操作。因為if邏輯判斷完之后也可能失效導致刪除別人的鎖。
3)針對扣減庫存這個邏輯,lua腳本里面實現Redis比較庫存、扣減庫存操作的原子性。通過判斷Redis Decr命令的返回值即可。此命令會返回扣減后的最新庫存,若小于0則表示超賣。

5.1 自己實現分布式鎖的坑

setnx不關心鎖的順序導致刪除別人的鎖
鎖失效之后,別人加鎖成功,自己把別人的鎖刪了。
我們無法預估程序執行需要的鎖的時間。

public String deductStock() {String lockKey = "lock:product_101";Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "deltaqin");stringRedisTemplate.expire(lockKey, 10, TimeUnit.SECONDS);try {int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")if (stock > 0) {int realStock = stock - 1;stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)System.out.println("扣減成功,剩余庫存:" + realStock);} else {System.out.println("扣減失敗,庫存不足");}} finally {stringRedisTemplate.delete(lockKey);}return "end";
}

setnx關心鎖的順序還是刪除了別人的鎖
并發會卡在各種地方,卡住的時候過期了,就會刪掉別人加的鎖:
錯誤的原因還是因為解鎖的邏輯不是原子性的,這里可以參考Redisson的解鎖邏輯使用lua腳本實現。

public String deductStock() {String lockKey = "lock:product_101";String clientId = UUID.randomUUID().toString();Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 30, TimeUnit.SECONDS); //jedis.setnx(k,v)if (!result) {return "error_code";}try {int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")if (stock > 0) {int realStock = stock - 1;stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)System.out.println("扣減成功,剩余庫存:" + realStock);} else {System.out.println("扣減失敗,庫存不足");}} finally {if (clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))) {// 卡在這里,鎖過期了,其他線程又可以加鎖,此時又把其他線程新加的鎖刪掉了stringRedisTemplate.delete(lockKey);}}return "end";
}

解決辦法
這種問題解決的辦法就是使用鎖續命,比如使用一個定時任務間隔小于鎖的超時時間,每隔一段時間就給鎖續命,除非線程自己主動刪除。這也是Redisson的實現思路。
5.2 鎖優化:分段加鎖邏輯

針對一個商品,要開啟秒殺的時候,會將商品的庫存預先加載到Redis緩存中,比如有100個庫存,此時可以分為5個key,每一個key有20個庫存。可以把分布式鎖的性能提升5倍。
例如:

product_10111_stock = 100product_10111_stock1 = 20product_10111_stock2 = 20product_10111_stock3 = 20product_10111_stock4 = 20product_10111_stock5 = 20

請求來了可以隨機可以輪詢,扣減完之后就標記不要下次再分配到這個庫存。

六、分布式鎖的真相與選擇

6.1 分布式鎖的真相

需要滿足的幾個特性
互斥:不同線程、進程互斥。

超時機制:臨界區代碼耗時導致,網絡原因導致。可以使用額外的線程續命保證。

完備的鎖接口:阻塞的和非阻塞的接口都要有,lock和tryLock。

可重入性:當前請求的節點+ 線程唯一標識。

公平性:鎖喚醒時候,按照順序喚醒。

正確性:進程內的鎖不會因為報錯死鎖,因為崩潰的時候整個進程都會結束。但是多實例部署時死鎖就很容易發生,如果粗暴使用超時機制解決死鎖問題,就默認了下面這個假設:

鎖的超時時間 >> 獲取鎖的時延 + 執行臨界區代碼的時間 + 各種進程的暫停(比如 GC)

但上述假設其實無法保證的。
將分布式鎖定位為,可以容忍非常小概率互斥語義失效場景下的鎖服務。一般來說,一個分布式鎖服務,它的正確性要求越高,性能可能就會越低。
6.2 分布式鎖的選擇

數據庫:db操作性能較差,并且有鎖表的風險,一般不考慮。

優點:實現簡單、易于理解

缺點:對數據庫壓力大

Redis:適用于并發量很大、性能要求很高而可靠性問題可以通過其他方案去彌補的場景。

優點:易于理解

缺點:自己實現、不支持阻塞

Redisson:相對于Jedis其實更多用在分布式的場景。

優點:提供鎖的方法,可阻塞

Zookeeper:適用于高可靠(高可用),而并發量不是太高的場景。

優點:支持阻塞

缺點:需理解Zookeeper、程序復雜

Curator

優點:提供鎖的方法

缺點:Zookeeper,強一致,慢

Etcd:安全和可靠性上有保證,但是比較重。

不推薦自己編寫的分布式鎖,推薦使用Redisson和Curator實現的分布式鎖。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/166593.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/166593.shtml
英文地址,請注明出處:http://en.pswp.cn/news/166593.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

python opencv 放射變換和圖像縮放-實現圖像平移旋轉縮放

python opencv 放射變換和圖像縮放-實現圖像平移旋轉縮放 我們實現這次實驗主要用到cv2.resize和cv2.warpAffine cv2.warpAffine主要是傳入一個圖像矩陣,一個M矩陣,輸出一個dst結果矩陣,計算公式如下: cv2.resize則主要使用fx&…

精益生產中的周轉箱優勢:提升效率與質量的得力利器

在當今競爭激烈的制造業中,企業追求高效生產和卓越質量是至關重要的。精益生產理念提供了一套有效的工具和方法,其中周轉箱作為一個關鍵的組成部分,在優化生產流程、提高效率和質量方面發揮著重要作用。下面談談精益生產中的周轉箱優勢&#…

C++:內存管理

內存分布: 首先我們需要了解的是C/C中內存區域的劃分: 1. 棧又叫堆棧--非靜態局部變量/函數參數/返回值等等,棧是向下增長的:先調用的地址比后調用的地址大。 2. 內存映射段是高效的I/O映射方式,用于裝載一個共享的動…

百度文心一言(千帆大模型)聊天API使用指導

開篇不得不吐槽下百度,百度智能云平臺首頁跳轉千帆大模型平臺的按鈕太多了,不同按鈕跳轉不同的子頁面,不熟悉的,能把人找懵。入口太多,就導致用戶不知道從何開始。本文就從一個前端開發人員的角度,教大家快…

【深度學習】基于深度學習的超分辨率圖像技術一覽

超分辨率(Super-Resolution)即通過硬件或軟件的方法提高原有圖像的分辨率,圖像超分辨率是計算機視覺和圖像處理領域一個非常重要的研究問題,在醫療圖像分析、生物特征識別、視頻監控與安全等實際場景中有著廣泛的應用。 SR取得了顯著進步。一般可以將現有…

為什么,word文件在只讀模式下,仍然能編輯?

Word文檔設置了只讀模式,是可以編輯的,但是當我們進行保存的時候就會發現,word提示需要重命名并選擇新路徑才能夠保存。 這種操作,即使可以編輯文字,但是原文件是不會受到影響的,編輯之后的word文件會保存到…

torch常用和預期輸入輸出

import torch import torch.nn as nn import torch.nn.functional as F nn中定義的是類,functional里面定義的是函數操作。 輸出shape的計算公式: o u t _ s h a p e r o u n d _ m o d e ( i n _ s h a p e 2 ? p a d d i n g ? k e r n e l _ s…

20231124給RK3399的挖掘機開發板在Andorid10下加鼠標右鍵返回

20231124給RK3399的挖掘機開發板在Andorid10下加鼠標右鍵返回 2023/11/24 12:19 百度:RK3399 Android10 右鍵返回 https://blog.csdn.net/danhu/article/details/122467256 android9/android10 鼠標右鍵返回(已驗證) danhu 于 2022-01-13 09:46:42 發布 android10 …

Echarts 大屏注冊自定義地圖解析文件流報錯問題解決

效果圖: 1、首先通過后臺接口獲取到SVG圖片的文件流,postman能夠正確解析出文件流,前端調用api時需要設置返回的響應格式為image/svg+xml格式,否則解析失敗 拿到文件流后是這樣的 <?xml version="1.0" encoding="utf-8"?> <!-- Generator: …

【深度學習】P1 深度學習基礎框架 - 張量 Tensor

深度學習基礎框架 張量 Tensor 張量數據操作導入創建張量獲取張量信息改變張量張量運算 張量與內存 張量 Pytorch 是一個深度學習框架&#xff0c;用于開發和訓練神經網絡模型。 而其核心數據結構&#xff0c;則是張量 Tensor&#xff0c;類似于 Numpy 數組&#xff0c;但是可…

AI制作的《大多數普通女孩的一生》——公開教程和工作流

內容來源&#xff1a;JiamigouCn ?這周由AI制作的《大多數普通女孩的一生》&#xff0c;在抖音爆火&#xff0c;獲得新華網轉發。到目前為止&#xff0c;全網還沒有公開教程和工作流&#xff0c;需要花費800-2000購買。 本著AI社區共享原則&#xff0c;我委托公眾號“楚思智能…

小學生古詩文大會復賽在線模擬新增刷題版和闖關版,幫助孩子沖刺

小學生古詩文大會明天就要開始了&#xff0c;剛剛古詩文大會主辦方也正式發布了通知&#xff0c;總體安排、操作指引和我之前發布的一樣&#xff1a;2023年11月25日小學生古詩文大會復選&#xff08;復賽&#xff09;答題操作手冊 為了幫助參加復選&#xff08;復賽&#xff09…

NFC技術簡介

NFC簡介 NFC(近場通信&#xff0c;Near Field Communication&#xff09;是一種短距高頻的無線電技術&#xff0c;由非接觸式射頻識別(RFID)演變而來。 NFC工作頻率為13.56Hz&#xff0c;通常只有在距離不超過4厘米時才能啟動連接&#xff0c;其傳輸速度有106 Kbit/秒、212 Kb…

從文本生成到數據增強:探索 AI 前沿的開源套件 | 開源專題 No.44

Significant-Gravitas/AutoGPT Stars: 150.4k License: MIT AutoGPT 是開源 AI 代理生態系統的核心工具包。它采用模塊化和可擴展的框架&#xff0c;使您能夠專注于以下方面&#xff1a; 構建 - 為驚人之作打下基礎。測試 - 將您的代理調整到完美狀態。查看 - 觀察進展成果呈…

【Mybatis源碼】反射 - MetaClass

前面我們介紹了Reflector類,Reflector主要完成了Class類中Setter、Getter方法的封裝,可以使用屬性獲取對應的Getter、Setter方法完成方法的調用,同時也可以判斷屬性是否存在,是否存在Getter、Setter方法。 使用Reflector解決了訪問Class類中屬性的問題,但是如果屬性是成員…

HandBrake 1.7 近日發布

導讀HandBrake 1.7 近日發布&#xff0c;作為這個開源、免費和跨平臺視頻轉碼器應用程序的重大更新&#xff0c;適用于 GNU/Linux、macOS 和 Windows 系統。 在 HandBrake 1.6 發布近一年后&#xff0c;HandBrake 1.7 版本為 Linux 用戶提供了許多好處&#xff0c;包括視頻摘要…

C語言第二十八彈--輸入一個非負整數,返回組成它的數字之和

C語言求輸入一個非負整數&#xff0c;返回組成它的數字之和 方法一、遞歸法 思路&#xff1a;設計一個初始條件&#xff0c;通過遞歸獲取非負整數的個位&#xff0c;不斷接近遞歸條件即可。 #define _CRT_SECURE_NO_WARNINGS #include <stdio.h>int DigitSum(int n) {…

PGZ-SBV系列項目介紹、技術選型、技術優勢(AIO內部版)All-In-One

前情提示 項目孵化于2014年,內部正式發行于2015.隨著業務需求,部分內部高級功能逐步對外開放支持多行業(保險、金融、餐飲、旅游、電商、直播等等),多架構,支持AI集成,支持協同,只要你想到的均可集成、均已內置高安全,高可用,多技術解決方案包含av版本、gf低代碼版、…

ubuntu22.04 arrch64版在線安裝maven

腳本 if type -p mvn; thenecho "maven has been installed."elsecd /home/zenglgwget https://dlcdn.apache.org/maven/maven-3/3.9.5/binaries/apache-maven-3.9.5-bin.tar.gz --no-check-certificatetar vxf apache-maven-3.9.5-bin.tar.gz rm -rf /usr/local/mav…

springboot+vue基本微信小程序的劇本殺游戲設計與實現

項目介紹 首先,論文一開始便是清楚的論述了小程序的研究內容。其次,剖析系統需求分析,弄明白“做什么”,分析包括業務分析和業務流程的分析以及用例分析,更進一步明確系統的需求。然后在明白了小程序的需求基礎上需要進一步地設計系統,主要包羅軟件架構模式、整體功能模塊、數…