架構(gòu)師之路(二) 冪等設(shè)計(jì)與分布式鎖設(shè)計(jì)

本專(zhuān)題所寫(xiě)所感所得募谎,來(lái)自轉(zhuǎn)轉(zhuǎn)首席架構(gòu)師和字節(jié)架構(gòu)團(tuán)隊(duì)扶关,此致,敬禮铜异。。

一蚂子、冪等設(shè)計(jì)

1.1 定義

冪等需要考慮請(qǐng)求層面和業(yè)務(wù)層面的冪等。

  • 請(qǐng)求層面

保證請(qǐng)求重復(fù)執(zhí)行和執(zhí)行一次結(jié)果相同董瞻;即f...f(f(x)) = f(x)

  • 業(yè)務(wù)層面

如同一用戶(hù)不重復(fù)下單;商品不超買(mǎi)

1.2 目標(biāo)

  • 請(qǐng)求重試不出問(wèn)題
  • 避免結(jié)果災(zāi)難性(重復(fù)轉(zhuǎn)賬、多交易等)

1.3 冪等范圍

冪等范圍主要是針對(duì)請(qǐng)求對(duì)數(shù)據(jù)造成改變截珍。以下從兩個(gè)維度判斷冪等范圍秋度。

  • 讀/寫(xiě)請(qǐng)求層面:寫(xiě)請(qǐng)求
  • 架構(gòu)層層面:數(shù)據(jù)訪(fǎng)問(wèn)層


二、分布式鎖設(shè)計(jì)

業(yè)務(wù)層面的冪等存在并發(fā)消費(fèi)的可能性,需要轉(zhuǎn)化為串行消費(fèi)兽泣。本質(zhì)上就是分布式鎖的問(wèn)題。

2.1 定義與目的

分布式鎖是在分布式環(huán)境下牵敷,鎖定全局唯一資源,使得請(qǐng)求處理串行化毛肋,實(shí)現(xiàn)類(lèi)似于互斥鎖的效果。
分布式鎖的目的是:

  • 防止重復(fù)下單,解決業(yè)務(wù)層冪等問(wèn)題
  • 解決MQ消息消費(fèi)冪等性問(wèn)題厂财,如發(fā)送消息重復(fù)、消息消費(fèi)端去重等
  • 狀態(tài)的修改行為需要串行處理荚恶,避免出現(xiàn)數(shù)據(jù)錯(cuò)亂

2.2 高可用分布式鎖設(shè)計(jì)

2.2.1 目標(biāo)

  • 強(qiáng)一致性
  • 服務(wù)高可用冗锁、系統(tǒng)穩(wěn)健
  • 鎖自動(dòng)續(xù)約及其自動(dòng)釋放
  • 代碼高度抽象業(yè)務(wù)接入極簡(jiǎn)
  • 可視化管理后臺(tái)茉帅,監(jiān)控及管理

2.2.2 特點(diǎn)

  • 互斥性:和我們本地鎖一樣互斥性是最基本,但是分布式鎖需要保證在不同節(jié)點(diǎn)的不同線(xiàn)程的互斥樱蛤。
  • 可重入性:同一個(gè)節(jié)點(diǎn)上的同一個(gè)線(xiàn)程如果獲取了鎖之后那么也可以再次獲取這個(gè)鎖。
  • 鎖超時(shí):和本地鎖一樣支持鎖超時(shí)便脊,防止死鎖。
  • 高效晌杰,高可用:加鎖和解鎖需要高效,同時(shí)也需要保證高可用防止分布式鎖失效惋啃,可以增加降級(jí)。
  • 支持阻塞和非阻塞:和ReentrantLock一樣支持lock和trylock以及tryLock(long timeOut)称簿。
  • 支持公平鎖和非公平鎖(可選):公平鎖的意思是按照請(qǐng)求加鎖的順序獲得鎖,非公平鎖就相反是無(wú)序的授药。這個(gè)一般來(lái)說(shuō)實(shí)現(xiàn)的比較少。

2.2.3 方案對(duì)比

mysql redis zookeeper etcd
一致性算法 無(wú) 無(wú) paxos raft
CAP cp ap cp cp
高可用 主從 主從 N+1可用(奇數(shù)) N+1可用(奇數(shù))
接口類(lèi)型 sql 客戶(hù)端 客戶(hù)端 http/grpc
實(shí)現(xiàn) select * from update setnx + lua crateephemeral restful api
  • redis是ap模型,無(wú)法保證數(shù)據(jù)一致性
  • zk對(duì)鎖實(shí)現(xiàn)使用創(chuàng)建臨時(shí)節(jié)點(diǎn)和watch機(jī)制趟庄,執(zhí)行效率、拓展能力虑鼎、社區(qū)活躍度都不如etcd

2.3 Mysql分布式鎖

首先來(lái)說(shuō)一下Mysql分布式鎖的實(shí)現(xiàn)原理,相對(duì)來(lái)說(shuō)這個(gè)比較容易理解江兢,畢竟數(shù)據(jù)庫(kù)和我們開(kāi)發(fā)人員在平時(shí)的開(kāi)發(fā)中息息相關(guān)。對(duì)于分布式鎖我們可以創(chuàng)建一個(gè)鎖表:


前面我們所說(shuō)的lock(),trylock(long timeout),trylock()這幾個(gè)方法可以用下面的偽代碼實(shí)現(xiàn)改基。

2.3.1 lock()

lock一般是阻塞式的獲取鎖稠腊,意思就是不獲取到鎖誓不罷休,那么我們可以寫(xiě)一個(gè)死循環(huán)來(lái)執(zhí)行其操作:

mysqlLock.lcok內(nèi)部是一個(gè)sql,為了達(dá)到可重入鎖的效果那么我們應(yīng)該先進(jìn)行查詢(xún),如果有值许昨,那么需要比較node_info是否一致拌喉,這里的node_info可以用機(jī)器IP和線(xiàn)程名字來(lái)表示,如果一致那么就加可重入鎖count的值田藐,如果不一致那么就返回false。如果沒(méi)有值那么直接插入一條數(shù)據(jù)景醇。偽代碼如下:

需要注意的是這一段代碼需要加事務(wù)窜管,必須要保證這一系列操作的原子性获搏。

2.3.2 tryLock()和tryLock(long timeout)

tryLock()是非阻塞獲取鎖谋币,如果獲取不到那么就會(huì)馬上返回,代碼可以如下:

tryLock(long timeout)實(shí)現(xiàn)如下:


mysqlLock.lock和上面一樣,但是要注意的是select ... for update這個(gè)是阻塞的獲取行鎖调炬,如果同一個(gè)資源并發(fā)量較大還是有可能會(huì)退化成阻塞的獲取鎖。

2.3.3 unlock()

unlock的話(huà)如果這里的count為1那么可以刪除棘钞,如果大于1那么需要減去1。


2.3.4 鎖超時(shí)

我們有可能會(huì)遇到我們的機(jī)器節(jié)點(diǎn)掛了,那么這個(gè)鎖就不會(huì)得到釋放叫乌,我們可以啟動(dòng)一個(gè)定時(shí)任務(wù),通過(guò)計(jì)算一般我們處理任務(wù)的一般的時(shí)間膀藐,比如是5ms,那么我們可以稍微擴(kuò)大一點(diǎn),當(dāng)這個(gè)鎖超過(guò)20ms沒(méi)有被釋放我們就可以認(rèn)定是節(jié)點(diǎn)掛了然后將其直接釋放傲醉。

2.3.5 Mysql小結(jié)

  • 適用場(chǎng)景: Mysql分布式鎖一般適用于資源不存在數(shù)據(jù)庫(kù)呻引,如果數(shù)據(jù)庫(kù)存在比如訂單,那么可以直接對(duì)這條數(shù)據(jù)加行鎖童谒,不需要我們上面多的繁瑣的步驟,比如一個(gè)訂單,那么我們可以用select * from order_table where id = 'xxx' for update進(jìn)行加行鎖,那么其他的事務(wù)就不能對(duì)其進(jìn)行修改越除。
  • 優(yōu)點(diǎn):理解起來(lái)簡(jiǎn)單,不需要維護(hù)額外的第三方中間件(比如Redis,Zk)孩擂。
  • 缺點(diǎn):雖然容易理解但是實(shí)現(xiàn)起來(lái)較為繁瑣,需要自己考慮鎖超時(shí),加事務(wù)等等砰琢。性能局限于數(shù)據(jù)庫(kù)训唱,一般對(duì)比緩存來(lái)說(shuō)性能較低况增。對(duì)于高并發(fā)的場(chǎng)景并不是很適合。

2.3.6 樂(lè)觀鎖

前面我們介紹的都是悲觀鎖宴凉,這里想額外提一下樂(lè)觀鎖,在我們實(shí)際項(xiàng)目中也是經(jīng)常實(shí)現(xiàn)樂(lè)觀鎖籽暇,因?yàn)槲覀兗有墟i的性能消耗比較大,通常我們會(huì)對(duì)于一些競(jìng)爭(zhēng)不是那么激烈绸狐,但是其又需要保證我們并發(fā)的順序執(zhí)行使用樂(lè)觀鎖進(jìn)行處理,我們可以對(duì)我們的表加一個(gè)版本號(hào)字段符相,那么我們查詢(xún)出來(lái)一個(gè)版本號(hào)之后,update或者delete的時(shí)候需要依賴(lài)我們查詢(xún)出來(lái)的版本號(hào)孕索,判斷當(dāng)前數(shù)據(jù)庫(kù)和查詢(xún)出來(lái)的版本號(hào)是否相等散怖,如果相等那么就可以執(zhí)行,如果不等那么就不能執(zhí)行欠动。這樣的一個(gè)策略很像我們的CAS(Compare And Swap),比較并交換是一個(gè)原子操作。這樣我們就能避免加select * for update行鎖的開(kāi)銷(xiāo)。

2.4 基于redis分布式鎖

redis是單線(xiàn)程的萤厅,所以能保證線(xiàn)程串行處理,但因?yàn)閞edis分布式鎖是ap模型名挥,不是cp模型,無(wú)法實(shí)現(xiàn)強(qiáng)一致性。但因?qū)崿F(xiàn)簡(jiǎn)單黄刚,接入成本低憔维,如果對(duì)數(shù)據(jù)一致性要求不那么高舒萎,可以選擇此方式章鲤。

2.4.1 Redis分布式鎖簡(jiǎn)單實(shí)現(xiàn)

熟悉Redis的同學(xué)那么肯定對(duì)setNx(set if not exist)方法不陌生帚呼,如果不存在則更新沪哺,其可以很好的用來(lái)實(shí)現(xiàn)我們的分布式鎖酥泛。對(duì)于某個(gè)資源加鎖我們只需要

setNx resourceName value

這里有個(gè)問(wèn)題,加鎖了之后如果機(jī)器宕機(jī)那么這個(gè)鎖就不會(huì)得到釋放所以會(huì)加入過(guò)期時(shí)間,加入過(guò)期時(shí)間需要和setNx同一個(gè)原子操作腥例,在Redis2.8之前我們需要使用Lua腳本達(dá)到我們的目的,但是redis2.8之后redis支持nx和ex操作是同一原子操作构回。

set resourceName value ex 5 nx

2.4.2 Redission

Javaer都知道Jedis,Jedis是Redis的Java實(shí)現(xiàn)的客戶(hù)端,其API提供了比較全面的Redis命令的支持掏愁。Redission也是Redis的客戶(hù)端印蓖,相比于Jedis功能簡(jiǎn)單公浪。Jedis簡(jiǎn)單使用阻塞的I/O和redis交互,Redission通過(guò)Netty支持非阻塞I/O。Jedis最新版本2.9.0是2016年的快3年了沒(méi)有更新憔古,而Redission最新版本是2018.10月更新。

Redission封裝了鎖的實(shí)現(xiàn),其繼承了java.util.concurrent.locks.Lock的接口,讓我們像操作我們的本地Lock一樣去操作Redission的Lock,下面介紹一下其如何實(shí)現(xiàn)分布式鎖壁却。

Redission不僅提供了Java自帶的一些方法(lock,tryLock),還提供了異步加鎖付鹿,對(duì)于異步編程更加方便音婶。 由于內(nèi)部源碼較多寄雀,就不貼源碼了,這里用文字?jǐn)⑹鰜?lái)分析他是如何加鎖的盐肃,這里分析一下tryLock方法:

  • 嘗試加鎖:首先會(huì)嘗試進(jìn)行加鎖谦铃,由于保證操作是原子性师妙,那么就只能使用lua腳本若专,相關(guān)的lua腳本如下:

    可以看見(jiàn)他并沒(méi)有使用我們的sexNx來(lái)進(jìn)行操作趋箩,而是使用的hash結(jié)構(gòu)次乓,我們的每一個(gè)需要鎖定的資源都可以看做是一個(gè)HashMap,鎖定資源的節(jié)點(diǎn)信息是Key,鎖定次數(shù)是value软吐。通過(guò)這種方式可以很好的實(shí)現(xiàn)可重入的效果异旧,只需要對(duì)value進(jìn)行加1操作每篷,就能進(jìn)行可重入鎖无畔。當(dāng)然這里也可以用之前我們說(shuō)的本地計(jì)數(shù)進(jìn)行優(yōu)化诉濒。

  • 如果嘗試加鎖失敗,判斷是否超時(shí),如果超時(shí)則返回false。
  • 如果加鎖失敗之后,沒(méi)有超時(shí),那么需要在名字為redisson_lock__channel+lockName的channel上進(jìn)行訂閱恋昼,用于訂閱解鎖消息容客,然后一直阻塞直到超時(shí)活孩,或者有解鎖消息。
  • 重試步驟以上三步忧额,直到最后獲取到鎖,或者某一步獲取鎖超時(shí)。

對(duì)于我們的unlock方法比較簡(jiǎn)單也是通過(guò)lua腳本進(jìn)行解鎖祈搜,如果是可重入鎖粘舟,只是減1凌节。如果是非加鎖線(xiàn)程解鎖,那么解鎖失敗元潘。


Redission還有公平鎖的實(shí)現(xiàn)文虏,對(duì)于公平鎖其利用了list結(jié)構(gòu)和hashset結(jié)構(gòu)分別用來(lái)保存我們排隊(duì)的節(jié)點(diǎn)宇葱,和我們節(jié)點(diǎn)的過(guò)期時(shí)間焚志,用這兩個(gè)數(shù)據(jù)結(jié)構(gòu)幫助我們實(shí)現(xiàn)公平鎖陨界,這里就不展開(kāi)介紹了,有興趣可以參考源碼帆啃。

2.4.3 RedLock

我們想象一個(gè)這樣的場(chǎng)景當(dāng)機(jī)器A申請(qǐng)到一把鎖之后刊懈,如果Redis主宕機(jī)了这弧,這個(gè)時(shí)候從機(jī)并沒(méi)有同步到這一把鎖娃闲,那么機(jī)器B再次申請(qǐng)的時(shí)候就會(huì)再次申請(qǐng)到這把鎖,為了解決這個(gè)問(wèn)題Redis作者提出了RedLock紅鎖的算法,在Redission中也對(duì)RedLock進(jìn)行了實(shí)現(xiàn)匾浪。



通過(guò)上面的代碼皇帮,我們需要實(shí)現(xiàn)多個(gè)Redis集群,然后進(jìn)行紅鎖的加鎖蛋辈,解鎖属拾。具體的步驟如下:

  • 首先生成多個(gè)Redis集群的Rlock,并將其構(gòu)造成RedLock梯浪。
  • 依次循環(huán)對(duì)三個(gè)集群進(jìn)行加鎖捌年,加鎖的過(guò)程和5.2里面一致。
  • 如果循環(huán)加鎖的過(guò)程中加鎖失敗挂洛,那么需要判斷加鎖失敗的次數(shù)是否超出了最大值礼预,這里的最大值是根據(jù)集群的個(gè)數(shù),比如三個(gè)那么只允許失敗一個(gè)虏劲,五個(gè)的話(huà)只允許失敗兩個(gè)托酸,要保證多數(shù)成功。
  • 加鎖的過(guò)程中需要判斷是否加鎖超時(shí)柒巫,有可能我們?cè)O(shè)置加鎖只能用3ms励堡,第一個(gè)集群加鎖已經(jīng)消耗了3ms了。那么也算加鎖失敗堡掏。
  • 3应结,4步里面加鎖失敗的話(huà),那么就會(huì)進(jìn)行解鎖操作泉唁,解鎖會(huì)對(duì)所有的集群在請(qǐng)求一次解鎖鹅龄。

可以看見(jiàn)RedLock基本原理是利用多個(gè)Redis集群,用多數(shù)的集群加鎖成功亭畜,減少Redis某個(gè)集群出故障扮休,造成分布式鎖出現(xiàn)問(wèn)題的概率。

2.4.4 Redis小結(jié)

  • 優(yōu)點(diǎn):對(duì)于Redis實(shí)現(xiàn)簡(jiǎn)單拴鸵,性能對(duì)比ZK和Mysql較好玷坠。如果不需要特別復(fù)雜的要求,那么自己就可以利用setNx進(jìn)行實(shí)現(xiàn)劲藐,如果自己需要復(fù)雜的需求的話(huà)那么可以利用或者借鑒Redission八堡。對(duì)于一些要求比較嚴(yán)格的場(chǎng)景來(lái)說(shuō)的話(huà)可以使用RedLock。
  • 缺點(diǎn):需要維護(hù)Redis集群聘芜,如果要實(shí)現(xiàn)RedLock那么需要維護(hù)更多的集群兄渺。

2.5 基于ZK分布式鎖

ZooKeeper也是我們常見(jiàn)的實(shí)現(xiàn)分布式鎖方法,相比于數(shù)據(jù)庫(kù)如果沒(méi)了解過(guò)ZooKeeper可能上手比較難一些厉膀。ZooKeeper是以Paxos算法為基礎(chǔ)分布式應(yīng)用程序協(xié)調(diào)服務(wù)溶耘。Zk的數(shù)據(jù)節(jié)點(diǎn)和文件目錄類(lèi)似二拐,所以我們可以用此特性實(shí)現(xiàn)分布式鎖。我們以某個(gè)資源為目錄凳兵,然后這個(gè)目錄下面的節(jié)點(diǎn)就是我們需要獲取鎖的客戶(hù)端百新,未獲取到鎖的客戶(hù)端注冊(cè)需要注冊(cè)Watcher到上一個(gè)客戶(hù)端,可以用下圖表示庐扫。


/lock是我們用于加鎖的目錄,/resource_name是我們鎖定的資源饭望,其下面的節(jié)點(diǎn)按照我們加鎖的順序排列。

2.5.1 Curator

Curator封裝了Zookeeper底層的Api形庭,使我們更加容易方便的對(duì)Zookeeper進(jìn)行操作铅辞,并且它封裝了分布式鎖的功能,這樣我們就不需要再自己實(shí)現(xiàn)了萨醒。

Curator實(shí)現(xiàn)了可重入鎖(InterProcessMutex),也實(shí)現(xiàn)了不可重入鎖(InterProcessSemaphoreMutex)斟珊。在可重入鎖中還實(shí)現(xiàn)了讀寫(xiě)鎖。

2.5.2 InterProcessMutex

InterProcessMutex是Curator實(shí)現(xiàn)的可重入鎖富纸,我們可以通過(guò)下面的一段代碼實(shí)現(xiàn)我們的可重入鎖:


我們利用acuire進(jìn)行加鎖囤踩,release進(jìn)行解鎖。

加鎖的流程具體如下:

  • 首先進(jìn)行可重入的判定:這里的可重入鎖記錄在ConcurrentMap<Thread, LockData> threadData這個(gè)Map里面晓褪,如果threadData.get(currentThread)是有值的那么就證明是可重入鎖堵漱,然后記錄就會(huì)加1。我們之前的Mysql其實(shí)也可以通過(guò)這種方法去優(yōu)化涣仿,可以不需要count字段的值勤庐,將這個(gè)維護(hù)在本地可以提高性能。
  • 然后在我們的資源目錄下創(chuàng)建一個(gè)節(jié)點(diǎn):比如這里創(chuàng)建一個(gè)/0000000002這個(gè)節(jié)點(diǎn)好港,這個(gè)節(jié)點(diǎn)需要設(shè)置為EPHEMERAL_SEQUENTIAL也就是臨時(shí)節(jié)點(diǎn)并且有序愉镰。
  • 獲取當(dāng)前目錄下所有子節(jié)點(diǎn),判斷自己的節(jié)點(diǎn)是否位于子節(jié)點(diǎn)第一個(gè)媚狰。
  • 如果是第一個(gè)岛杀,則獲取到鎖阔拳,那么可以返回崭孤。
  • 如果不是第一個(gè),則證明前面已經(jīng)有人獲取到鎖了糊肠,那么需要獲取自己節(jié)點(diǎn)的前一個(gè)節(jié)點(diǎn)辨宠。/0000000002的前一個(gè)節(jié)點(diǎn)是/0000000001,我們獲取到這個(gè)節(jié)點(diǎn)之后货裹,再上面注冊(cè)Watcher(這里的watcher其實(shí)調(diào)用的是object.notifyAll(),用來(lái)解除阻塞)嗤形。
  • object.wait(timeout)或object.wait():進(jìn)行阻塞等待這里和我們第5步的watcher相對(duì)應(yīng)。

解鎖的具體流程:

  • 首先進(jìn)行可重入鎖的判定:如果有可重入鎖只需要次數(shù)減1即可弧圆,減1之后加鎖次數(shù)為0的話(huà)繼續(xù)下面步驟赋兵,不為0直接返回笔咽。
  • 刪除當(dāng)前節(jié)點(diǎn)。
  • 刪除threadDataMap里面的可重入鎖的數(shù)據(jù)霹期。

2.5.3 讀寫(xiě)鎖

Curator提供了讀寫(xiě)鎖叶组,其實(shí)現(xiàn)類(lèi)是InterProcessReadWriteLock,這里的每個(gè)節(jié)點(diǎn)都會(huì)加上前綴:

private static final String READ_LOCK_NAME  = "__READ__";
private static final String WRITE_LOCK_NAME = "__WRIT__";

根據(jù)不同的前綴區(qū)分是讀鎖還是寫(xiě)鎖历造,對(duì)于讀鎖甩十,如果發(fā)現(xiàn)前面有寫(xiě)鎖,那么需要將watcher注冊(cè)到和自己最近的寫(xiě)鎖吭产。寫(xiě)鎖的邏輯和我們之前2.5.2分析的依然保持不變侣监。

2.5.4鎖超時(shí)

Zookeeper不需要配置鎖超時(shí),由于我們?cè)O(shè)置節(jié)點(diǎn)是臨時(shí)節(jié)點(diǎn)臣淤,我們的每個(gè)機(jī)器維護(hù)著一個(gè)ZK的session橄霉,通過(guò)這個(gè)session,ZK可以判斷機(jī)器是否宕機(jī)邑蒋。如果我們的機(jī)器掛掉的話(huà)酪劫,那么這個(gè)臨時(shí)節(jié)點(diǎn)對(duì)應(yīng)的就會(huì)被刪除,所以我們不需要關(guān)心鎖超時(shí)寺董。

2.5.5 ZK小結(jié)

  • 優(yōu)點(diǎn):ZK可以不需要關(guān)心鎖超時(shí)時(shí)間覆糟,實(shí)現(xiàn)起來(lái)有現(xiàn)成的第三方包,比較方便遮咖,并且支持讀寫(xiě)鎖滩字,ZK獲取鎖會(huì)按照加鎖的順序,所以其是公平鎖御吞。對(duì)于高可用利用ZK集群進(jìn)行保證麦箍。
  • 缺點(diǎn):ZK需要額外維護(hù),增加維護(hù)成本陶珠,性能和Mysql相差不大挟裂,依然比較差。并且需要開(kāi)發(fā)人員了解ZK是什么揍诽。

2.6 基于etcd分布式鎖

2.6.1 機(jī)制

etcd 支持以下功能诀蓉,正是依賴(lài)這些功能來(lái)實(shí)現(xiàn)分布式鎖的:

  • Lease 機(jī)制:即租約機(jī)制(TTL,Time To Live)暑脆,Etcd 可以為存儲(chǔ)的 KV 對(duì)設(shè)置租約渠啤,當(dāng)租約到期,KV 將失效刪除添吗;同時(shí)也支持續(xù)約沥曹,即 KeepAlive。
  • Revision 機(jī)制:每個(gè) key 帶有一個(gè) Revision 屬性值,etcd 每進(jìn)行一次事務(wù)對(duì)應(yīng)的全局 Revision 值都會(huì)加一妓美,因此每個(gè) key 對(duì)應(yīng)的 Revision 屬性值都是全局唯一的僵腺。通過(guò)比較 Revision 的大小就可以知道進(jìn)行寫(xiě)操作的順序。
  • 在實(shí)現(xiàn)分布式鎖時(shí)壶栋,多個(gè)程序同時(shí)搶鎖想邦,根據(jù) Revision 值大小依次獲得鎖,可以避免 “羊群效應(yīng)” (也稱(chēng) “驚群效應(yīng)”)委刘,實(shí)現(xiàn)公平鎖丧没。
  • Prefix 機(jī)制:即前綴機(jī)制,也稱(chēng)目錄機(jī)制锡移∨煌可以根據(jù)前綴(目錄)獲取該目錄下所有的 key 及對(duì)應(yīng)的屬性(包括 key, value 以及 revision 等)。
  • Watch 機(jī)制:即監(jiān)聽(tīng)機(jī)制淆珊,Watch 機(jī)制支持 Watch 某個(gè)固定的 key夺饲,也支持 Watch 一個(gè)目錄(前綴機(jī)制),當(dāng)被 Watch 的 key 或目錄發(fā)生變化施符,客戶(hù)端將收到通知往声。

2.6.2 過(guò)程

實(shí)現(xiàn)過(guò)程:

  • 步驟 1: 準(zhǔn)備

客戶(hù)端連接 Etcd,以 /lock/mylock 為前綴創(chuàng)建全局唯一的 key戳吝,假設(shè)第一個(gè)客戶(hù)端對(duì)應(yīng)的 key="/lock/mylock/UUID1"浩销,第二個(gè)為 key="/lock/mylock/UUID2";客戶(hù)端分別為自己的 key 創(chuàng)建租約 - Lease听哭,租約的長(zhǎng)度根據(jù)業(yè)務(wù)耗時(shí)確定慢洋,假設(shè)為 15s;

  • 步驟 2: 創(chuàng)建定時(shí)任務(wù)作為租約的“心跳”

當(dāng)一個(gè)客戶(hù)端持有鎖期間陆盘,其它客戶(hù)端只能等待普筹,為了避免等待期間租約失效,客戶(hù)端需創(chuàng)建一個(gè)定時(shí)任務(wù)作為“心跳”進(jìn)行續(xù)約隘马。此外太防,如果持有鎖期間客戶(hù)端崩潰,心跳停止酸员,key 將因租約到期而被刪除蜒车,從而鎖釋放,避免死鎖沸呐。

  • 步驟 3: 客戶(hù)端將自己全局唯一的 key 寫(xiě)入 Etcd

進(jìn)行 put 操作醇王,將步驟 1 中創(chuàng)建的 key 綁定租約寫(xiě)入 Etcd呢燥,根據(jù) Etcd 的 Revision 機(jī)制崭添,假設(shè)兩個(gè)客戶(hù)端 put 操作返回的 Revision 分別為 1、2叛氨,客戶(hù)端需記錄 Revision 用以接下來(lái)判斷自己是否獲得鎖呼渣。

  • 步驟 4: 客戶(hù)端判斷是否獲得鎖

客戶(hù)端以前綴 /lock/mylock 讀取 keyValue 列表(keyValue 中帶有 key 對(duì)應(yīng)的 Revision)棘伴,判斷自己 key 的 Revision 是否為當(dāng)前列表中最小的,如果是則認(rèn)為獲得鎖屁置;否則監(jiān)聽(tīng)列表中前一個(gè) Revision 比自己小的 key 的刪除事件焊夸,一旦監(jiān)聽(tīng)到刪除事件或者因租約失效而刪除的事件,則自己獲得鎖蓝角。

  • 步驟 5: 執(zhí)行業(yè)務(wù)

獲得鎖后阱穗,操作共享資源,執(zhí)行業(yè)務(wù)代碼使鹅。

  • 步驟 6: 釋放鎖

完成業(yè)務(wù)流程后揪阶,刪除對(duì)應(yīng)的key釋放鎖。

2.6.3 實(shí)現(xiàn)

自帶的 etcdctl 可以模擬鎖的使用:


// 第一個(gè)終端
$ ./etcdctl lock mutex1
mutex1/326963a02758b52d

// 第二終端
$ ./etcdctl lock mutex1

// 當(dāng)?shù)谝粋€(gè)終端結(jié)束了患朱,第二個(gè)終端會(huì)顯示
mutex1/326963a02758b531

在etcd的clientv3包中鲁僚,實(shí)現(xiàn)了分布式鎖。使用起來(lái)和mutex是類(lèi)似的裁厅,為了了解其中的工作機(jī)制冰沙,這里簡(jiǎn)要的做一下總結(jié)。

etcd分布式鎖的實(shí)現(xiàn)在go.etcd.io/etcd/clientv3/concurrency包中执虹,主要提供了以下幾個(gè)方法:

* func NewMutex(s *Session, pfx string) *Mutex拓挥, 用來(lái)新建一個(gè)mutex
* func (m *Mutex) Lock(ctx context.Context) error,它會(huì)阻塞直到拿到了鎖袋励,并且支持通過(guò)context來(lái)取消獲取鎖撞叽。
* func (m *Mutex) Unlock(ctx context.Context) error,解鎖

因此在使用etcd提供的分布式鎖式非常簡(jiǎn)單插龄,通常就是實(shí)例化一個(gè)mutex愿棋,然后嘗試搶占鎖,之后進(jìn)行業(yè)務(wù)處理均牢,最后解鎖即可糠雨。

demo:

package main

import (  
    "context"
    "fmt"
    "github.com/coreos/etcd/clientv3"
    "github.com/coreos/etcd/clientv3/concurrency"
    "log"
    "os"
    "os/signal"
    "time"
)

func main() {  
    c := make(chan os.Signal)
    signal.Notify(c)

    cli, err := clientv3.New(clientv3.Config{
        Endpoints:   []string{"localhost:2379"},
        DialTimeout: 5 * time.Second,
    })
    if err != nil {
        log.Fatal(err)
    }
    defer cli.Close()

    lockKey := "/lock"

    go func () {
        session, err := concurrency.NewSession(cli)
        if err != nil {
            log.Fatal(err)
        }
        m := concurrency.NewMutex(session, lockKey)
        if err := m.Lock(context.TODO()); err != nil {
            log.Fatal("go1 get mutex failed " + err.Error())
        }
        fmt.Printf("go1 get mutex sucess\n")
        fmt.Println(m)
        time.Sleep(time.Duration(10) * time.Second)
        m.Unlock(context.TODO())
        fmt.Printf("go1 release lock\n")
    }()

    go func() {
        time.Sleep(time.Duration(2) * time.Second)
        session, err := concurrency.NewSession(cli)
        if err != nil {
            log.Fatal(err)
        }
        m := concurrency.NewMutex(session, lockKey)
        if err := m.Lock(context.TODO()); err != nil {
            log.Fatal("go2 get mutex failed " + err.Error())
        }
        fmt.Printf("go2 get mutex sucess\n")
        fmt.Println(m)
        time.Sleep(time.Duration(2) * time.Second)
        m.Unlock(context.TODO())
        fmt.Printf("go2 release lock\n")
    }()

    <-c
}

2.6.4 原理

Lock()函數(shù)的實(shí)現(xiàn)很簡(jiǎn)單:

// Lock locks the mutex with a cancelable context. If the context is canceled
// while trying to acquire the lock, the mutex tries to clean its stale lock entry.
func (m *Mutex) Lock(ctx context.Context) error {
    s := m.s
    client := m.s.Client()

    m.myKey = fmt.Sprintf("%s%x", m.pfx, s.Lease())
    cmp := v3.Compare(v3.CreateRevision(m.myKey), "=", 0)
    // put self in lock waiters via myKey; oldest waiter holds lock
    put := v3.OpPut(m.myKey, "", v3.WithLease(s.Lease()))
    // reuse key in case this session already holds the lock
    get := v3.OpGet(m.myKey)
    // fetch current holder to complete uncontended path with only one RPC
    getOwner := v3.OpGet(m.pfx, v3.WithFirstCreate()...)
    resp, err := client.Txn(ctx).If(cmp).Then(put, getOwner).Else(get, getOwner).Commit()
    if err != nil {
        return err
    }
    m.myRev = resp.Header.Revision
    if !resp.Succeeded {
        m.myRev = resp.Responses[0].GetResponseRange().Kvs[0].CreateRevision
    }
    // if no key on prefix / the minimum rev is key, already hold the lock
    ownerKey := resp.Responses[1].GetResponseRange().Kvs
    if len(ownerKey) == 0 || ownerKey[0].CreateRevision == m.myRev {
        m.hdr = resp.Header
        return nil
    }

    // wait for deletion revisions prior to myKey
    hdr, werr := waitDeletes(ctx, client, m.pfx, m.myRev-1)
    // release lock key if wait failed
    if werr != nil {
        m.Unlock(client.Ctx())
    } else {
        m.hdr = hdr
    }
    return werr
}

首先通過(guò)一個(gè)事務(wù)來(lái)嘗試加鎖,這個(gè)事務(wù)主要包含了4個(gè)操作: cmp徘跪、put甘邀、get、getOwner垮庐。需要注意的是松邪,key是由pfx和Lease()組成的。

  • cmp: 比較加鎖的key的修訂版本是否是0哨查。如果是0就代表這個(gè)鎖不存在逗抑。
  • put: 向加鎖的key中存儲(chǔ)一個(gè)空值,這個(gè)操作就是一個(gè)加鎖的操作,但是這把鎖是有超時(shí)時(shí)間的邮府,超時(shí)的時(shí)間是session的默認(rèn)時(shí)長(zhǎng)荧关。超時(shí)是為了防止鎖沒(méi)有被正常釋放導(dǎo)致死鎖。
  • get: get就是通過(guò)key來(lái)查詢(xún)
  • getOwner: 注意這里是用m.pfx來(lái)查詢(xún)的褂傀,并且?guī)Я瞬樵?xún)參數(shù)WithFirstCreate()忍啤。使用pfx來(lái)查詢(xún)是因?yàn)槠渌膕ession也會(huì)用同樣的pfx來(lái)嘗試加鎖,并且因?yàn)槊總€(gè)LeaseID都不同仙辟,所以第一次肯定會(huì)put成功同波。但是只有最早使用這個(gè)pfx的session才是持有鎖的,所以這個(gè)getOwner的含義就是這樣的叠国。

接下來(lái)才是通過(guò)判斷來(lái)檢查是否持有鎖

m.myRev = resp.Header.Revision
if !resp.Succeeded {
    m.myRev = resp.Responses[0].GetResponseRange().Kvs[0].CreateRevision
}
// if no key on prefix / the minimum rev is key, already hold the lock
ownerKey := resp.Responses[1].GetResponseRange().Kvs
if len(ownerKey) == 0 || ownerKey[0].CreateRevision == m.myRev {
    m.hdr = resp.Header
    return nil
}

m.myRev是當(dāng)前的版本號(hào)参萄,resp.Succeeded是cmp為true時(shí)值為true,否則是false煎饼。這里的判斷表明當(dāng)同一個(gè)session非第一次嘗試加鎖讹挎,當(dāng)前的版本號(hào)應(yīng)該取這個(gè)key的最新的版本號(hào)。

下面是取得鎖的持有者的key吆玖。如果當(dāng)前沒(méi)有人持有這把鎖筒溃,那么默認(rèn)當(dāng)前會(huì)話(huà)獲得了鎖≌闯耍或者鎖持有者的版本號(hào)和當(dāng)前的版本號(hào)一致怜奖, 那么當(dāng)前的會(huì)話(huà)就是鎖的持有者。

// wait for deletion revisions prior to myKey
hdr, werr := waitDeletes(ctx, client, m.pfx, m.myRev-1)
// release lock key if wait failed
if werr != nil {
    m.Unlock(client.Ctx())
} else {
    m.hdr = hdr
}

上面這段代碼就很好理解了翅阵,因?yàn)樽叩竭@里說(shuō)明沒(méi)有獲取到鎖歪玲,那么這里等待鎖的刪除。

waitDeletes方法的實(shí)現(xiàn)也很簡(jiǎn)單掷匠,但是需要注意的是滥崩,這里的getOpts只會(huì)獲取比當(dāng)前會(huì)話(huà)版本號(hào)更低的key,然后去監(jiān)控最新的key的刪除讹语。等這個(gè)key刪除了钙皮,自己也就拿到鎖了。

這種分布式鎖的實(shí)現(xiàn)和我一開(kāi)始的預(yù)想是不同的顽决。它不存在鎖的競(jìng)爭(zhēng)短条,不存在重復(fù)的嘗試加鎖的操作。而是通過(guò)使用統(tǒng)一的前綴pfx來(lái)put才菠,然后根據(jù)各自的版本號(hào)來(lái)排隊(duì)獲取鎖茸时。效率非常的高。避免了驚群效應(yīng)

如圖所示赋访,共有4個(gè)session來(lái)加鎖可都,那么根據(jù)revision來(lái)排隊(duì)缓待,獲取鎖的順序?yàn)閟ession2 -> session3 -> session1 -> session4。

這里面需要注意一個(gè)驚群效應(yīng)汹粤,每一個(gè)client在鎖住/lock這個(gè)path的時(shí)候命斧,實(shí)際都已經(jīng)插入了自己的數(shù)據(jù)田晚,類(lèi)似/lock/LEASE_ID嘱兼,并且返回了各自的index(就是raft算法里面的日志索引),而只有最小的才算是拿到了鎖贤徒,其他的client需要watch等待芹壕。例如client1拿到了鎖,client2和client3在等待接奈,而client2拿到的index比client3的更小踢涌,那么對(duì)于client1刪除鎖之后,client3其實(shí)并不關(guān)心序宦,并不需要去watch睁壁。所以綜上,等待的節(jié)點(diǎn)只需要watch比自己index小并且差距最小的節(jié)點(diǎn)刪除事件即可互捌。

2.6.5 基于 ETCD的選主

2.6.5.1 機(jī)制

etcd有多種使用場(chǎng)景潘明,Master選舉是其中一種。說(shuō)起Master選舉秕噪,過(guò)去常常使用zookeeper钳降,通過(guò)創(chuàng)建EPHEMERAL_SEQUENTIAL節(jié)點(diǎn)(臨時(shí)有序節(jié)點(diǎn)),我們選擇序號(hào)最小的節(jié)點(diǎn)作為Master腌巾,邏輯直觀遂填,實(shí)現(xiàn)簡(jiǎn)單是其優(yōu)勢(shì),但是要實(shí)現(xiàn)一個(gè)高健壯性的選舉并不簡(jiǎn)單澈蝙,同時(shí)zookeeper繁雜的擴(kuò)縮容機(jī)制也是沉重的負(fù)擔(dān)吓坚。

master 選舉根本上也是搶鎖,與zookeeper直觀選舉邏輯相比灯荧,etcd的選舉則需要在我們熟悉它的一系列基本概念后凌唬,調(diào)動(dòng)我們充分的想象力:

  • MVCC,key存在版本屬性漏麦,沒(méi)被創(chuàng)建時(shí)版本號(hào)為0客税;

  • CAS操作,結(jié)合MVCC撕贞,可以實(shí)現(xiàn)競(jìng)選邏輯更耻,if(version == 0) set(key,value),通過(guò)原子操作,確保只有一臺(tái)機(jī)器能set成功捏膨;

  • Lease租約秧均,可以對(duì)key綁定一個(gè)租約食侮,租約到期時(shí)沒(méi)預(yù)約,這個(gè)key就會(huì)被回收目胡;

  • Watch監(jiān)聽(tīng)锯七,監(jiān)聽(tīng)key的變化事件,如果key被刪除誉己,則重新發(fā)起競(jìng)選眉尸。

至此,etcd選舉的邏輯大體清晰了巨双,但這一系列操作與zookeeper相比復(fù)雜很多噪猾,有沒(méi)有已經(jīng)封裝好的庫(kù)可以直接拿來(lái)用?etcd clientv3 concurrency中有對(duì)選舉及分布式鎖的封裝筑累。后面進(jìn)一步發(fā)現(xiàn)袱蜡,etcdctl v3里已經(jīng)有master選舉的實(shí)現(xiàn)了,下面針對(duì)這部分代碼進(jìn)行簡(jiǎn)單注釋?zhuān)谧詈髤⒖歼@部分代碼實(shí)現(xiàn)自己的選舉邏輯慢宗。

2.6.5.2 etcd選主的實(shí)現(xiàn)

官方示例:https://github.com/etcd-io/etcd/blob/master/clientv3/concurrency/example_election_test.go

如crontab 示例:

package main

import (
    "context"
    "fmt"
    "go.etcd.io/etcd/clientv3"
    "go.etcd.io/etcd/clientv3/concurrency"
    "log"
    "time"
)

const prefix = "/election-demo"
const prop = "local"

func main() {
    endpoints := []string{"szth-cce-devops00.szth.baidu.com:8379"}
    cli, err := clientv3.New(clientv3.Config{Endpoints: endpoints})
    if err != nil {
        log.Fatal(err)
    }
    defer cli.Close()

    campaign(cli, prefix, prop)

}

func campaign(c *clientv3.Client, election string, prop string) {
    for {
        // 租約到期時(shí)間:5s
        s, err := concurrency.NewSession(c, concurrency.WithTTL(5))
        if err != nil {
            fmt.Println(err)
            continue
        }
        e := concurrency.NewElection(s, election)
        ctx := context.TODO()

        log.Println("開(kāi)始競(jìng)選")

        err = e.Campaign(ctx, prop)
        if err != nil {
            log.Println("競(jìng)選 leader失敗坪蚁,繼續(xù)")
            switch {
            case err == context.Canceled:
                return
            default:
                continue
            }
        }

        log.Println("獲得leader")
        if err := doCrontab(); err != nil {
            log.Println("調(diào)用主方法失敗,辭去leader镜沽,重新競(jìng)選")
            _ = e.Resign(ctx)
            continue
        }
        return
    }
}

func doCrontab() error {
    for {
        fmt.Println("doCrontab")
        time.Sleep(time.Second * 4)
        //return fmt.Errorf("sss")
    }
}

2.6.5.3 etcd選主的原理

/*
 * 發(fā)起競(jìng)選
 * 未當(dāng)選leader前敏晤,會(huì)一直阻塞在Campaign調(diào)用
 * 當(dāng)選leader后,等待SIGINT淘邻、SIGTERM或session過(guò)期而退出
 * https://github.com/etcd-io/etcd/blob/master/etcdctl/ctlv3/command/elect_command.go
 */

func campaign(c *clientv3.Client, election string, prop string) error {
        //NewSession函數(shù)中創(chuàng)建了一個(gè)lease茵典,默認(rèn)是60s TTL,并會(huì)調(diào)用KeepAlive宾舅,永久為這個(gè)lease自動(dòng)續(xù)約(2/3生命周期的時(shí)候執(zhí)行續(xù)約操作)
    s, err := concurrency.NewSession(c)
    if err != nil {
        return err
    }
    e := concurrency.NewElection(s, election)
    ctx, cancel := context.WithCancel(context.TODO())

    donec := make(chan struct{})
    sigc := make(chan os.Signal, 1)
    signal.Notify(sigc, syscall.SIGINT, syscall.SIGTERM)
    go func() {
        <-sigc
        cancel()
        close(donec)
    }()

    //競(jìng)選邏輯统阿,將展開(kāi)分析
    if err = e.Campaign(ctx, prop); err != nil {
        return err
    }

    // print key since elected
    resp, err := c.Get(ctx, e.Key())
    if err != nil {
        return err
    }
    display.Get(*resp)

    select {
    case <-donec:
    case <-s.Done():
        return errors.New("elect: session expired")
    }

    return e.Resign(context.TODO())
}

/*
 * 類(lèi)似于zookeeper的臨時(shí)有序節(jié)點(diǎn),etcd的選舉也是在相應(yīng)的prefix path下面創(chuàng)建key筹我,該key綁定了lease并根據(jù)lease id進(jìn)行命名扶平,
 * key創(chuàng)建后就有revision號(hào),這樣使得在prefix path下的key也都是按revision有序
 * https://github.com/etcd-io/etcd/blob/master/clientv3/concurrency/election.go
 */

func (e *Election) Campaign(ctx context.Context, val string) error {
    s := e.session
    client := e.session.Client()

    //真正創(chuàng)建的key名為:prefix + lease id
    k := fmt.Sprintf("%s%x", e.keyPrefix, s.Lease())
    //Txn:transaction蔬蕊,依靠Txn進(jìn)行創(chuàng)建key的CAS操作结澄,當(dāng)key不存在時(shí)才會(huì)成功創(chuàng)建
    txn := client.Txn(ctx).If(v3.Compare(v3.CreateRevision(k), "=", 0))
    txn = txn.Then(v3.OpPut(k, val, v3.WithLease(s.Lease())))
    txn = txn.Else(v3.OpGet(k))
    resp, err := txn.Commit()
    if err != nil {
        return err
    }
    e.leaderKey, e.leaderRev, e.leaderSession = k, resp.Header.Revision, s
    //如果key已存在,則創(chuàng)建失敯逗弧麻献;
        //當(dāng)key的value與當(dāng)前value不等時(shí),如果自己為leader猜扮,則不用重新執(zhí)行選舉直接設(shè)置value勉吻;
        //否則報(bào)錯(cuò)。
    if !resp.Succeeded {
        kv := resp.Responses[0].GetResponseRange().Kvs[0]
        e.leaderRev = kv.CreateRevision
        if string(kv.Value) != val {
            if err = e.Proclaim(ctx, val); err != nil {
                e.Resign(ctx)
                return err
            }
        }
    }

    //一直阻塞旅赢,直到確認(rèn)自己的create revision為當(dāng)前path中最小齿桃,從而確認(rèn)自己當(dāng)選為leader
    _, err = waitDeletes(ctx, client, e.keyPrefix, e.leaderRev-1)
    if err != nil {
        // clean up in case of context cancel
        select {
        case <-ctx.Done():
            e.Resign(client.Ctx())
        default:
            e.leaderSession = nil
        }
        return err
    }
    e.hdr = resp.Header

    return nil
}

2.7 分布式鎖的安全問(wèn)題

下面我們來(lái)討論一下分布式鎖的安全問(wèn)題:

  • 長(zhǎng)時(shí)間的GC pause:熟悉Java的同學(xué)肯定對(duì)GC不陌生惑惶,在GC的時(shí)候會(huì)發(fā)生STW(stop-the-world),例如CMS垃圾回收器,他會(huì)有兩個(gè)階段進(jìn)行STW防止引用繼續(xù)進(jìn)行變化短纵。那么有可能會(huì)出現(xiàn)下面圖(引用至Martin反駁Redlock的文章)中這個(gè)情況:


client1獲取了鎖并且設(shè)置了鎖的超時(shí)時(shí)間带污,但是client1之后出現(xiàn)了STW,這個(gè)STW時(shí)間比較長(zhǎng)香到,導(dǎo)致分布式鎖進(jìn)行了釋放鱼冀,client2獲取到了鎖,這個(gè)時(shí)候client1恢復(fù)了鎖养渴,那么就會(huì)出現(xiàn)client1雷绢,2同時(shí)獲取到鎖泛烙,這個(gè)時(shí)候分布式鎖不安全問(wèn)題就出現(xiàn)了理卑。這個(gè)其實(shí)不僅僅局限于RedLock,對(duì)于我們的ZK,Mysql一樣的有同樣的問(wèn)題。

  • 時(shí)鐘發(fā)生跳躍:對(duì)于Redis服務(wù)器如果其時(shí)間發(fā)生了向跳躍蔽氨,那么肯定會(huì)影響我們鎖的過(guò)期時(shí)間藐唠,那么我們的鎖過(guò)期時(shí)間就不是我們預(yù)期的了,也會(huì)出現(xiàn)client1和client2獲取到同一把鎖鹉究,那么也會(huì)出現(xiàn)不安全宇立,這個(gè)對(duì)于Mysql也會(huì)出現(xiàn)。但是ZK由于沒(méi)有設(shè)置過(guò)期時(shí)間自赔,那么發(fā)生跳躍也不會(huì)受影響妈嘹。
  • 長(zhǎng)時(shí)間的網(wǎng)絡(luò)I/O:這個(gè)問(wèn)題和我們的GC的STW很像,也就是我們這個(gè)獲取了鎖之后我們進(jìn)行網(wǎng)絡(luò)調(diào)用绍妨,其調(diào)用時(shí)間由可能比我們鎖的過(guò)期時(shí)間都還長(zhǎng)润脸,那么也會(huì)出現(xiàn)不安全的問(wèn)題,這個(gè)Mysql也會(huì)有他去,ZK也不會(huì)出現(xiàn)這個(gè)問(wèn)題毙驯。

對(duì)于這三個(gè)問(wèn)題,在網(wǎng)上包括Redis作者在內(nèi)發(fā)起了很多討論灾测。

2.7.1 GC的STW

對(duì)于這個(gè)問(wèn)題可以看見(jiàn)基本所有的都會(huì)出現(xiàn)問(wèn)題爆价,對(duì)于ZK這種他會(huì)生成一個(gè)自增的序列,那么我們真正進(jìn)行對(duì)資源操作的時(shí)候媳搪,需要判斷當(dāng)前序列是否是最新铭段,有點(diǎn)類(lèi)似于我們樂(lè)觀鎖。當(dāng)然這個(gè)解法Redis作者進(jìn)行了反駁秦爆,你既然都能生成一個(gè)自增的序列了那么你完全不需要加鎖了序愚,也就是可以按照類(lèi)似于Mysql樂(lè)觀鎖的解法去做。

我自己認(rèn)為這種解法增加了復(fù)雜性鲜结,當(dāng)我們對(duì)資源操作的時(shí)候需要增加判斷序列號(hào)是否是最新展运,無(wú)論用什么判斷方法都會(huì)增加復(fù)雜度活逆。

2.7.2 時(shí)鐘發(fā)生跳躍

RedLock不安全很大的原因也是因?yàn)闀r(shí)鐘的跳躍,因?yàn)殒i過(guò)期強(qiáng)依賴(lài)于時(shí)間拗胜,但是ZK不需要依賴(lài)時(shí)間蔗候,依賴(lài)每個(gè)節(jié)點(diǎn)的Session。Redis作者也給出了解答:對(duì)于時(shí)間跳躍分為人為調(diào)整和NTP自動(dòng)調(diào)整埂软。

  • 人為調(diào)整:人為調(diào)整影響的那么完全可以人為不調(diào)整锈遥,這個(gè)是處于可控的。
  • NTP自動(dòng)調(diào)整:這個(gè)可以通過(guò)一定的優(yōu)化勘畔,把跳躍時(shí)間控制的可控范圍內(nèi)所灸,雖然會(huì)跳躍,但是是完全可以接受的炫七。

2.7.3長(zhǎng)時(shí)間的網(wǎng)絡(luò)I/O

對(duì)于這個(gè)問(wèn)題的優(yōu)化可以控制網(wǎng)絡(luò)調(diào)用的超時(shí)時(shí)間爬立,把所有網(wǎng)絡(luò)調(diào)用的超時(shí)時(shí)間相加,那么我們鎖過(guò)期時(shí)間其實(shí)應(yīng)該大于這個(gè)時(shí)間万哪,當(dāng)然也可以通過(guò)優(yōu)化網(wǎng)絡(luò)調(diào)用比如串行改成并行侠驯,異步化等∞任。可以參考文章: 并行化-你的高并發(fā)大殺器吟策,異步化-你的高并發(fā)大殺器

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市的止,隨后出現(xiàn)的幾起案子檩坚,更是在濱河造成了極大的恐慌,老刑警劉巖诅福,帶你破解...
    沈念sama閱讀 221,548評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件匾委,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡权谁,警方通過(guò)查閱死者的電腦和手機(jī)剩檀,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,497評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)旺芽,“玉大人沪猴,你說(shuō)我怎么就攤上這事〔烧拢” “怎么了运嗜?”我有些...
    開(kāi)封第一講書(shū)人閱讀 167,990評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)悯舟。 經(jīng)常有香客問(wèn)我担租,道長(zhǎng),這世上最難降的妖魔是什么抵怎? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,618評(píng)論 1 296
  • 正文 為了忘掉前任奋救,我火速辦了婚禮岭参,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘尝艘。我一直安慰自己演侯,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,618評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布背亥。 她就那樣靜靜地躺著秒际,像睡著了一般。 火紅的嫁衣襯著肌膚如雪狡汉。 梳的紋絲不亂的頭發(fā)上娄徊,一...
    開(kāi)封第一講書(shū)人閱讀 52,246評(píng)論 1 308
  • 那天,我揣著相機(jī)與錄音盾戴,去河邊找鬼寄锐。 笑死,一個(gè)胖子當(dāng)著我的面吹牛捻脖,可吹牛的內(nèi)容都是我干的锐峭。 我是一名探鬼主播中鼠,決...
    沈念sama閱讀 40,819評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼可婶,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了援雇?” 一聲冷哼從身側(cè)響起矛渴,我...
    開(kāi)封第一講書(shū)人閱讀 39,725評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎惫搏,沒(méi)想到半個(gè)月后具温,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,268評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡筐赔,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,356評(píng)論 3 340
  • 正文 我和宋清朗相戀三年铣猩,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片茴丰。...
    茶點(diǎn)故事閱讀 40,488評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡达皿,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出贿肩,到底是詐尸還是另有隱情峦椰,我是刑警寧澤,帶...
    沈念sama閱讀 36,181評(píng)論 5 350
  • 正文 年R本政府宣布汰规,位于F島的核電站汤功,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏溜哮。R本人自食惡果不足惜滔金,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,862評(píng)論 3 333
  • 文/蒙蒙 一色解、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧餐茵,春花似錦冒签、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,331評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至肠阱,卻和暖如春票唆,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背屹徘。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,445評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工走趋, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人噪伊。 一個(gè)月前我還...
    沈念sama閱讀 48,897評(píng)論 3 376
  • 正文 我出身青樓簿煌,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親鉴吹。 傳聞我的和親對(duì)象是個(gè)殘疾皇子姨伟,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,500評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容