最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會員登陸 & 注冊

分布式鎖實現(xiàn)原理與最佳實踐

2023-07-24 16:30 作者:阿里云  | 我要投稿

在單體的應(yīng)用開發(fā)場景中涉及并發(fā)同步時,大家往往采用Synchronized(同步)或同一個JVM內(nèi)Lock機制來解決多線程間的同步問題。而在分布式集群工作的開發(fā)場景中,就需要一種更加高級的鎖機制來處理跨機器的進程之間的數(shù)據(jù)同步問題,這種跨機器的鎖就是分布式鎖。接下來本文將為大家分享分布式鎖的最佳實踐。

01?超賣問題復(fù)現(xiàn)

1.1 現(xiàn)象

存在如下的幾張表:

  • 商品表

  • 訂單表

  • 訂單item表

商品的庫存為1,但是并發(fā)高的時候有多筆訂單。

? 錯誤案例一:數(shù)據(jù)庫update相互覆蓋

直接在內(nèi)存中判斷是否有庫存,計算扣減之后的值更新數(shù)據(jù)庫,并發(fā)的情況下會導(dǎo)致相互覆蓋發(fā)生:

? 錯誤案例二:扣減串行執(zhí)行,但是庫存被扣減為負(fù)數(shù)

在 SQL 中加入運算避免值的相互覆蓋,但是庫存的數(shù)量變?yōu)樨?fù)數(shù),因為校驗庫存是否足夠還是在內(nèi)存中執(zhí)行的,并發(fā)情況下都會讀到有庫存:

? 錯誤案例三:使用 synchronized 實現(xiàn)內(nèi)存中串行校驗,但是依舊扣減為負(fù)數(shù)

因為我們使用的是事務(wù)的注解,synchronized加在方法上,方法執(zhí)行結(jié)束的時候鎖就會釋放,此時的事務(wù)還沒有提交,另一個線程拿到這把鎖之后就會有一次扣減,導(dǎo)致負(fù)數(shù)。

1.2 解決辦法

從上面造成問題的原因來看,只要是扣減庫存的動作,不是原子性的。多個線程同時操作就會有問題。

  • 單體應(yīng)用:使用本地鎖 + 數(shù)據(jù)庫中的行鎖解決

  • 分布式應(yīng)用:

    • 使用數(shù)據(jù)庫中的樂觀鎖,加一個 version 字段,利用CAS來實現(xiàn),會導(dǎo)致大量的 update 失敗

    • 使用數(shù)據(jù)庫維護一張鎖的表 + 悲觀鎖 select,使用 select for update 實現(xiàn)

    • 使用Redis 的 setNX實現(xiàn)分布式鎖

    • 使用zookeeper的watcher + 有序臨時節(jié)點來實現(xiàn)可阻塞的分布式鎖

    • 使用Redisson框架內(nèi)的分布式鎖來實現(xiàn)

    • 使用curator 框架內(nèi)的分布式鎖來實現(xiàn)

02?單體應(yīng)用解決超賣的問題

正確示例:將事務(wù)包含在鎖的控制范圍內(nèi)

保證在鎖釋放之前,事務(wù)已經(jīng)提交。

正確示例:使用synchronized的代碼塊

正確示例:使用Lock

03?常見分布式鎖的使用

上面使用的方法只能解決單體項目,當(dāng)部署多臺機器的時候就會失效,因為鎖本身就是單機的鎖,所以需要使用分布式鎖來實現(xiàn)。

3.1 數(shù)據(jù)庫樂觀鎖

數(shù)據(jù)庫中的樂觀鎖,加一個version字段,利用CAS來實現(xiàn),樂觀鎖的方式支持多臺機器并發(fā)安全。但是并發(fā)量大的時候會導(dǎo)致大量的update失敗

3.2 數(shù)據(jù)庫分布式鎖

db操作性能較差,并且有鎖表的風(fēng)險,一般不考慮。

? 3.2.1 簡單的數(shù)據(jù)庫鎖

? select for update

直接在數(shù)據(jù)庫新建一張表:

鎖的code預(yù)先寫到數(shù)據(jù)庫中,搶鎖的時候,使用select for update查詢鎖對應(yīng)的key,也就是這里的code,阻塞就說明別人在使用鎖。

使用唯一鍵作為限制,插入一條數(shù)據(jù),其他待執(zhí)行的SQL就會失敗,當(dāng)數(shù)據(jù)刪除之后再去獲取鎖 ,這是利用了唯一索引的排他性。

? insert lock

直接維護一張鎖表:

3.3 Redis setNx

Redis 原生支持的,保證只有一個會話可以設(shè)置成功,因為Redis自己就是單線程串行執(zhí)行的。

封裝一個鎖對象:

每次獲取的時候,自己線程需要new對應(yīng)的RedisLock:

3.4 zookeeper 瞬時znode節(jié)點 + watcher監(jiān)聽機制

臨時節(jié)點具備數(shù)據(jù)自動刪除的功能。當(dāng)client與ZooKeeper連接和session斷掉時,相應(yīng)的臨時節(jié)點就會被刪除。zk有瞬時和持久節(jié)點,瞬時節(jié)點不可以有子節(jié)點。會話結(jié)束之后瞬時節(jié)點就會消失,基于zk的瞬時有序節(jié)點實現(xiàn)分布式鎖:

  • 多線程并發(fā)創(chuàng)建瞬時節(jié)點的時候,得到有序的序列,序號最小的線程可以獲得鎖;

  • 其他的線程監(jiān)聽自己序號的前一個序號。前一個線程執(zhí)行結(jié)束之后刪除自己序號的節(jié)點;

  • 下一個序號的線程得到通知,繼續(xù)執(zhí)行;

  • 以此類推,創(chuàng)建節(jié)點的時候,就確認(rèn)了線程執(zhí)行的順序。

zk 的觀察器只可以監(jiān)控一次,數(shù)據(jù)發(fā)生變化之后可以發(fā)送給客戶端,之后需要再次設(shè)置監(jiān)控。exists、create、getChildren三個方法都可以添加watcher ,也就是在調(diào)用方法的時候傳遞true就是添加監(jiān)聽。注意這里L(fēng)ock 實現(xiàn)了Watcher和AutoCloseable:

當(dāng)前線程創(chuàng)建的節(jié)點是第一個節(jié)點就獲得鎖,否則就監(jiān)聽自己的前一個節(jié)點的事件:

3.5 zookeeper curator

在實際的開發(fā)中,不建議去自己“重復(fù)造輪子”,而建議直接使用Curator客戶端中的各種官方實現(xiàn)的分布式鎖,例如其中的InterProcessMutex可重入鎖。

框架已經(jīng)實現(xiàn)了分布式鎖。zk的Java客戶端升級版。使用的時候直接指定重試的策略就可以。

官網(wǎng)中分布式鎖的實現(xiàn)是在curator-recipes依賴中,不要引用錯了。

3.6 Redission

重新實現(xiàn)了Java并發(fā)包下處理并發(fā)的類,讓其可以跨JVM使用,例如CHM等。

? 3.6.1 非SpringBoot項目引入

https://redisson.org/

引入Redisson的依賴,然后配置對應(yīng)的XML即可:

編寫相應(yīng)的redisson.xml

配置對應(yīng)@ImportResource("classpath*:redisson.xml")資源文件。

? 3.6.2?SpringBoot項目引入

或者直接使用springBoot的starter即可。

修改application.properties即可:#spring.redis.host=

? 3.6.3 設(shè)置配置類

? 3.6.4 使用

04?常見分布式鎖的原理

4.1 Redisson

Redis 2.6之后才可以執(zhí)行l(wèi)ua腳本,比起管道而言,這是原子性的,模擬一個商品減庫存的原子操作:

? 4.1.1 嘗試加鎖的邏輯

上面的org.redisson.RedissonLock#lock()通過調(diào)用自己方法內(nèi)部的lock方法的org.redisson.RedissonLock#tryAcquire方法。之后調(diào)用 org.redisson.RedissonLock#tryAcquireAsync:

首先調(diào)用內(nèi)部的org.redisson.RedissonLock#tryLockInnerAsync:設(shè)置對應(yīng)的分布式鎖

到這里獲取鎖的邏輯就結(jié)束了,如果這里沒有獲取到,在Future的回調(diào)里面就會直接return,會在外層有一個while true的循環(huán),訂閱釋放鎖的消息準(zhǔn)備被喚醒。如果說加鎖成功,就開始執(zhí)行鎖續(xù)命邏輯。

? 4.1.2?鎖續(xù)命邏輯

lua腳本最后是以毫秒為單位返回key的剩余過期時間。成功加鎖之后org.redisson.RedissonLock#scheduleExpirationRenewal中將會調(diào)用org.redisson.RedissonLock#renewExpiration,這個方法內(nèi)部就有鎖續(xù)命的邏輯,是一個定時任務(wù),等10s執(zhí)行。

執(zhí)行的時候嘗試執(zhí)行的續(xù)命邏輯使用的是Lua腳本,當(dāng)前的鎖有值,就續(xù)命,沒有就直接返回0:

返回0之后外層會判斷,延時成功就會再次調(diào)用自己,否則延時調(diào)用結(jié)束,不再為當(dāng)前的鎖續(xù)命。所以這里的續(xù)命不是一個真正的定時,而是循環(huán)調(diào)用自己的延時任務(wù)。

? 4.1.3?循環(huán)間隔搶鎖機制

如果一開始就加鎖成功就直接返回。

如果一開始加鎖失敗,沒搶到鎖的線程就會在while循環(huán)中嘗試加鎖,加鎖成功就結(jié)束循環(huán),否則等待當(dāng)前鎖的超時時間之后再次嘗試加鎖。所以實現(xiàn)邏輯默認(rèn)是非公平鎖:

里面有一個subscribe的邏輯,會監(jiān)聽對應(yīng)加鎖的key,當(dāng)鎖釋放之后publish對應(yīng)的消息,此時如果沒有到達對應(yīng)的鎖的超時時間,也會嘗試獲取鎖,避免時間浪費。

? 4.1.4?釋放鎖和喚醒其他線程的邏輯

前面沒有搶到鎖的線程會監(jiān)聽對應(yīng)的queue,后面搶到鎖的線程釋放鎖的時候會發(fā)送一個消息。

訂閱的時候指定收到消息時候的邏輯:會喚醒阻塞之后執(zhí)行while循環(huán)

? 4.1.5?重入鎖的邏輯

存在對應(yīng)的鎖,就對對應(yīng)的hash結(jié)構(gòu)的value直接+1,和Java重入鎖的邏輯是一致的。

4.2 RedLock解決非單體項目的Redis主從架構(gòu)的鎖失效

查看Redis官方文檔,對于單節(jié)點的Redis ,使用setnx和lua del刪除分布式鎖是足夠的,但是主從架構(gòu)的場景下:鎖先加在一個master節(jié)點上,默認(rèn)是異步同步到從節(jié)點,此時master掛了會選擇slave為master,此時又可以加鎖,就會導(dǎo)致超賣。但是如果使用zookeeper來實現(xiàn)的話,由于zk是CP的,所以CP不存在這樣的問題。

Redis文檔中給出了RedLock的解決辦法,使用redLock真的可以解決嗎?

? 4.2.1 RedLock 原理

基于客戶端的實現(xiàn),是基于多個獨立的Redis Master節(jié)點的一種實現(xiàn)(一般為5)。client依次向各個節(jié)點申請鎖,若能從多數(shù)個節(jié)點中申請鎖成功并滿足一些條件限制,那么client就能獲取鎖成功。它通過獨立的N個Master節(jié)點,避免了使用主備異步復(fù)制協(xié)議的缺陷,只要多數(shù)Redis節(jié)點正常就能正常工作,顯著提升了分布式鎖的安全性、可用性。

注意圖中所有的節(jié)點都是master節(jié)點。加鎖超過半數(shù)成功,就認(rèn)為是成功。具體流程:

  • 獲取鎖

    • 獲取當(dāng)前時間T1,作為后續(xù)的計時依據(jù);

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

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

    • 時間:T2-T1;

    • 多數(shù)節(jié)點的鎖(N/2+1);

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

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

  • 釋放鎖

    • 向所有節(jié)點發(fā)起釋放鎖的操作,不管這些節(jié)點有沒有成功設(shè)置過。


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

Martin Kleppmann:How to do distributed locking

Antirez:Is Redlock safe?

? 4.2.2 RedLock 問題一:持久化機制導(dǎo)致重復(fù)加鎖

如果是上面的架構(gòu)圖,一般生產(chǎn)都不會配置AOF的每一條命令都落磁盤,一般會設(shè)置一些間隔時間,比如1s,如果ABC節(jié)點加鎖成功,有一個節(jié)點C恰好是在1s內(nèi)加鎖,還沒有落盤,此時掛了,就會導(dǎo)致其他客戶端通過CDE又會加鎖成功。

? 4.2.3 RedLock 問題二:主從下重復(fù)加鎖


除非多部署一些節(jié)點,但是這樣會導(dǎo)致加鎖時間變長,這樣比較下來效果就不如zk了。

? 4.2.4 RedLock 問題三:時鐘跳躍導(dǎo)致重復(fù)加鎖

C節(jié)點發(fā)生了時鐘跳躍,導(dǎo)致加上的鎖沒有到達實際的超時時間,就被誤以為超時而釋放,此時其他客戶端就可以重復(fù)加鎖了。

4.3 Curator

? InterProcessMutex 可重入鎖的分析


05?業(yè)務(wù)中使用分布式鎖的注意點

獲取的鎖要設(shè)置有效期,假設(shè)我們未設(shè)置key自動過期時間,在Set key value NX 后,如果程序crash或者發(fā)生網(wǎng)絡(luò)分區(qū)后無法與Redis節(jié)點通信,毫無疑問其他 client 將永遠(yuǎn)無法獲得鎖,這將導(dǎo)致死鎖,服務(wù)出現(xiàn)中斷。

SETNX和EXPIRE命令去設(shè)置key和過期時間,這也是不正確的,因為你無法保證SETNX和EXPIRE命令的原子性。

自己使用 setnx 實現(xiàn)Redis鎖的時候,注意并發(fā)情況下不要釋放掉別人的鎖(業(yè)務(wù)邏輯執(zhí)行時間超過鎖的過期時間),導(dǎo)致惡性循環(huán)。一般:

1)加鎖的時候需要指定value的內(nèi)容是當(dāng)前進程中的當(dāng)前線程的唯一標(biāo)記,不要使用線程ID作為當(dāng)前線程的鎖的標(biāo)記,因為不同實例上的線程ID可能是一樣的。

2)釋放鎖的邏輯會寫在finally ,釋放鎖時候要判斷鎖對應(yīng)的value,而且要使用lua腳本實現(xiàn)原子 del 操作。因為if邏輯判斷完之后也可能失效導(dǎo)致刪除別人的鎖

3)針對扣減庫存這個邏輯,lua腳本里面實現(xiàn)Redis比較庫存、扣減庫存操作的原子性。通過判斷Redis Decr命令的返回值即可。此命令會返回扣減后的最新庫存,若小于0則表示超賣。

5.1 自己實現(xiàn)分布式鎖的坑

? setnx不關(guān)心鎖的順序?qū)е聞h除別人的鎖

鎖失效之后,別人加鎖成功,自己把別人的鎖刪了。

我們無法預(yù)估程序執(zhí)行需要的鎖的時間。

? setnx關(guān)心鎖的順序還是刪除了別人的鎖

并發(fā)會卡在各種地方,卡住的時候過期了,就會刪掉別人加的鎖:

錯誤的原因還是因為解鎖的邏輯不是原子性的,這里可以參考Redisson的解鎖邏輯使用lua腳本實現(xiàn)。

? 解決辦法

這種問題解決的辦法就是使用鎖續(xù)命,比如使用一個定時任務(wù)間隔小于鎖的超時時間,每隔一段時間就給鎖續(xù)命,除非線程自己主動刪除。這也是Redisson的實現(xiàn)思路。

5.2 鎖優(yōu)化:分段加鎖邏輯

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

例如:

  • product_10111_stock = 100

    • product_10111_stock1 = 20

    • product_10111_stock2 = 20

    • product_10111_stock3 = 20

    • product_10111_stock4 = 20

    • product_10111_stock5 = 20

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

06?分布式鎖的真相與選擇

6.1 分布式鎖的真相

需要滿足的幾個特性

  • 互斥:不同線程、進程互斥。

  • 超時機制:臨界區(qū)代碼耗時導(dǎo)致,網(wǎng)絡(luò)原因?qū)е???梢允褂妙~外的線程續(xù)命保證。

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

  • 可重入性:當(dāng)前請求的節(jié)點+ 線程唯一標(biāo)識。

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

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

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

    • 但上述假設(shè)其實無法保證的。

將分布式鎖定位為,可以容忍非常小概率互斥語義失效場景下的鎖服務(wù)。一般來說,一個分布式鎖服務(wù),它的正確性要求越高,性能可能就會越低。

6.2 分布式鎖的選擇

  • 數(shù)據(jù)庫:db操作性能較差,并且有鎖表的風(fēng)險,一般不考慮。

    • 優(yōu)點:實現(xiàn)簡單、易于理解

    • 缺點:對數(shù)據(jù)庫壓力大

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

    • 優(yōu)點:易于理解

    • 缺點:自己實現(xiàn)、不支持阻塞

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

      • 優(yōu)點:提供鎖的方法,可阻塞

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

    • 優(yōu)點:支持阻塞

    • 缺點:需理解Zookeeper、程序復(fù)雜

  • Curator

    • 優(yōu)點:提供鎖的方法

    • 缺點:Zookeeper,強一致,慢

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

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


分布式鎖實現(xiàn)原理與最佳實踐的評論 (共 條)

分享到微博請遵守國家法律
镇康县| 信宜市| 色达县| 陵水| 乐都县| 罗平县| 牡丹江市| 布拖县| 高安市| 廊坊市| 沈阳市| 留坝县| 舒城县| 陇南市| 石台县| 婺源县| 乐平市| 玉溪市| 叙永县| 德江县| 芦溪县| 东源县| 陕西省| 龙胜| 乌海市| 黄山市| 雷州市| 岑巩县| 抚宁县| 抚顺市| 营口市| 合肥市| 商河县| 阿坝县| 南康市| 申扎县| 思南县| 宁化县| 高雄市| 临邑县| 克山县|