基于Redis實(shí)現(xiàn)分布式鎖之前氏堤,這些坑你一定得知道

開(kāi)頭

基于Redis的分布式鎖對(duì)大家來(lái)說(shuō)并不陌生腐缤,可是你的分布式鎖有失敗的時(shí)候嗎瘫拣?在失敗的時(shí)候可曾懷疑過(guò)你在用的分布式鎖真的靠譜嗎亿絮?以下是結(jié)合自己的踩坑經(jīng)驗(yàn)總結(jié)的一些經(jīng)驗(yàn)之談。

你真的需要分布式鎖嗎?

用到分布式鎖說(shuō)明遇到了多個(gè)進(jìn)程共同訪(fǎng)問(wèn)同一個(gè)資源的問(wèn)題麸拄, 一般是在兩個(gè)場(chǎng)景下會(huì)防止對(duì)同一個(gè)資源的重復(fù)訪(fǎng)問(wèn):

1.提高效率派昧。比如多個(gè)節(jié)點(diǎn)計(jì)算同一批任務(wù),如果某個(gè)任務(wù)已經(jīng)有節(jié)點(diǎn)在計(jì)算了拢切,那其他節(jié)點(diǎn)就不用重復(fù)計(jì)算了蒂萎,以免浪費(fèi)計(jì)算資源。不過(guò)重復(fù)計(jì)算也沒(méi)事淮椰,不會(huì)造成其他更大的損失五慈。也就是允許偶爾的失敗。

2.保證正確性主穗。這種情況對(duì)鎖的要求就很高了泻拦,如果重復(fù)計(jì)算,會(huì)對(duì)正確性造成影響忽媒。這種不允許失敗争拐。

引入分布式鎖勢(shì)必要引入一個(gè)第三方的基礎(chǔ)設(shè)施,比如MySQL晦雨,Redis架曹,Zookeeper等隘冲,這些實(shí)現(xiàn)分布式鎖的基礎(chǔ)設(shè)施出問(wèn)題了,也會(huì)影響業(yè)務(wù)绑雄,所以在使用分布式鎖前可以考慮下是否可以不用加鎖的方式實(shí)現(xiàn)展辞?不過(guò)這個(gè)不在本文的討論范圍內(nèi),本文假設(shè)加鎖的需求是合理的绳慎,并且偏向于上面的第二種情況纵竖,為什么是偏向?因?yàn)椴淮嬖?00%靠譜的分布式鎖杏愤,看完下面的內(nèi)容就明白了靡砌。

從一個(gè)簡(jiǎn)單的分布式鎖實(shí)現(xiàn)說(shuō)起

分布式鎖的Redis實(shí)現(xiàn)很常見(jiàn),自己實(shí)現(xiàn)和使用第三方庫(kù)都很簡(jiǎn)單珊楼,至少看上去是這樣的通殃,這里就介紹一個(gè)最簡(jiǎn)單靠譜的Redis實(shí)現(xiàn)。

最簡(jiǎn)單的實(shí)現(xiàn)

實(shí)現(xiàn)很經(jīng)典了厕宗,這里只提兩個(gè)要點(diǎn)画舌?

1.加鎖和解鎖的鎖必須是同一個(gè),常見(jiàn)的解決方案是給每個(gè)鎖一個(gè)鑰匙(唯一ID)已慢,加鎖時(shí)生成曲聂,解鎖時(shí)判斷。

2.不能讓一個(gè)資源永久加鎖佑惠。常見(jiàn)的解決方案是給一個(gè)鎖的過(guò)期時(shí)間朋腋。當(dāng)然了還有其他方案,后面再說(shuō)膜楷。

一個(gè)可復(fù)制粘貼的實(shí)現(xiàn)方式如下:

加鎖

public static boolean tryLock(String key, String uniqueId, int seconds) {

? ? return "OK".equals(jedis.set(key, uniqueId, "NX", "EX", seconds));

}

這里其實(shí)是調(diào)用了 SET key value PX milliseoncds NX旭咽,不明白這個(gè)命令的參考下SET key value [EX seconds|PX milliseconds] [NX|XX] [KEEPTTL]

解鎖

public static boolean releaseLock(String key, String uniqueId) {

? ? String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +

? ? ? ? ? ? "return redis.call('del', KEYS[1]) else return 0 end";

? ? return jedis.eval(

? ? ? ? luaScript,

? ? ? ? Collections.singletonList(key),

? ? ? ? Collections.singletonList(uniqueId)

? ? ).equals(1L);

}

這段實(shí)現(xiàn)的精髓在那個(gè)簡(jiǎn)單的lua腳本上,先判斷唯一ID是否相等再操作赌厅。

靠譜嗎?

這樣的實(shí)現(xiàn)有什么問(wèn)題呢穷绵?

1.單點(diǎn)問(wèn)題。上面的實(shí)現(xiàn)只要一個(gè)master節(jié)點(diǎn)就能搞定特愿,這里的單點(diǎn)指的是單master仲墨,就算是個(gè)集群,如果加鎖成功后揍障,鎖從master復(fù)制到slave的時(shí)候掛了宗收,也是會(huì)出現(xiàn)同一資源被多個(gè)client加鎖的。

2.執(zhí)行時(shí)間超過(guò)了鎖的過(guò)期時(shí)間亚兄。上面寫(xiě)到為了不出現(xiàn)一直上鎖的情況混稽,加了一個(gè)兜底的過(guò)期時(shí)間,時(shí)間到了鎖自動(dòng)釋放,但是匈勋,如果在這期間任務(wù)并沒(méi)有做完怎么辦礼旅?由于GC或者網(wǎng)絡(luò)延遲導(dǎo)致的任務(wù)時(shí)間變長(zhǎng),很難保證任務(wù)一定能在鎖的過(guò)期時(shí)間內(nèi)完成洽洁。

如何解決這兩個(gè)問(wèn)題呢痘系?試試看更復(fù)雜的實(shí)現(xiàn)吧。

Redlock算法

對(duì)于第一個(gè)單點(diǎn)問(wèn)題饿自,順著redis的思路汰翠,接下來(lái)想到的肯定是Redlock了。Redlock為了解決單機(jī)的問(wèn)題昭雌,需要多個(gè)(大于2)redis的master節(jié)點(diǎn)复唤,多個(gè)master節(jié)點(diǎn)互相獨(dú)立,沒(méi)有數(shù)據(jù)同步烛卧。

Redlock的實(shí)現(xiàn)如下:

1.獲取當(dāng)前時(shí)間佛纫。

2.依次獲取N個(gè)節(jié)點(diǎn)的鎖。每個(gè)節(jié)點(diǎn)加鎖的實(shí)現(xiàn)方式同上总放。這里有個(gè)細(xì)節(jié)呈宇,就是每次獲取鎖的時(shí)候的過(guò)期時(shí)間都不同,需要減去之前獲取鎖的操作的耗時(shí)局雄,

比如傳入的鎖的過(guò)期時(shí)間為500ms甥啄,

獲取第一個(gè)節(jié)點(diǎn)的鎖花了1ms,那么第一個(gè)節(jié)點(diǎn)的鎖的過(guò)期時(shí)間就是499ms炬搭,

獲取第二個(gè)節(jié)點(diǎn)的鎖花了2ms蜈漓,那么第二個(gè)節(jié)點(diǎn)的鎖的過(guò)期時(shí)間就是497ms

如果鎖的過(guò)期時(shí)間小于等于0了,說(shuō)明整個(gè)獲取鎖的操作超時(shí)了尚蝌,整個(gè)操作失敗

3.判斷是否獲取鎖成功。如果client在上述步驟中獲取到了(N/2 + 1)個(gè)節(jié)點(diǎn)鎖充尉,并且每個(gè)鎖的過(guò)期時(shí)間都是大于0的飘言,則獲取鎖成功,否則失敗驼侠。失敗時(shí)釋放鎖姿鸿。

4.釋放鎖。對(duì)所有節(jié)點(diǎn)發(fā)送釋放鎖的指令倒源,每個(gè)節(jié)點(diǎn)的實(shí)現(xiàn)邏輯和上面的簡(jiǎn)單實(shí)現(xiàn)一樣苛预。為什么要對(duì)所有節(jié)點(diǎn)操作?因?yàn)榉植际綀?chǎng)景下從一個(gè)節(jié)點(diǎn)獲取鎖失敗不代表在那個(gè)節(jié)點(diǎn)上加速失敗笋熬,可能實(shí)際上加鎖已經(jīng)成功了热某,但是返回時(shí)因?yàn)榫W(wǎng)絡(luò)抖動(dòng)超時(shí)了。

以上就是大家常見(jiàn)的redlock實(shí)現(xiàn)的描述了,一眼看上去就是簡(jiǎn)單版本的多master版本昔馋,如果真是這樣就太簡(jiǎn)單了筹吐,接下來(lái)分析下這個(gè)算法在各個(gè)場(chǎng)景下是怎樣被玩壞的。

分布式鎖的坑

高并發(fā)場(chǎng)景下的問(wèn)題

以下問(wèn)題不是說(shuō)在并發(fā)不高的場(chǎng)景下不容易出現(xiàn)秘遏,只是在高并發(fā)場(chǎng)景下出現(xiàn)的概率更高些而已丘薛。性能問(wèn)題。性能問(wèn)題來(lái)自于兩個(gè)方面邦危。

1.獲取鎖的時(shí)間上。如果redlock運(yùn)用在高并發(fā)的場(chǎng)景下,存在N個(gè)master節(jié)點(diǎn)酵镜,一個(gè)一個(gè)去請(qǐng)求痢法,耗時(shí)會(huì)比較長(zhǎng),從而影響性能审丘。這個(gè)好解決吏够。通過(guò)上面描述不難發(fā)現(xiàn),從多個(gè)節(jié)點(diǎn)獲取鎖的操作并不是一個(gè)同步操作滩报,可以是異步操作锅知,這樣可以多個(gè)節(jié)點(diǎn)同時(shí)獲取。即使是并行處理的脓钾,還是得預(yù)估好獲取鎖的時(shí)間售睹,保證鎖的TTL > 獲取鎖的時(shí)間+任務(wù)處理時(shí)間。

2.被加鎖的資源太大可训。加鎖的方案本身就是會(huì)為了正確性而犧牲并發(fā)的昌妹,犧牲和資源大小成正比。這個(gè)時(shí)候可以考慮對(duì)資源做拆分握截,拆分的方式有兩種:

3.從業(yè)務(wù)上將鎖住的資源拆分成多段飞崖,每段分開(kāi)加鎖。比如谨胞,我要對(duì)一個(gè)商戶(hù)做若干個(gè)操作固歪,操作前要鎖住這個(gè)商戶(hù),這時(shí)我可以將若干個(gè)操作拆成多個(gè)獨(dú)立的步驟分開(kāi)加鎖胯努,提高并發(fā)牢裳。

4.用分桶的思想,將一個(gè)資源拆分成多個(gè)桶叶沛,一個(gè)加鎖失敗立即嘗試下一個(gè)蒲讯。比如批量任務(wù)處理的場(chǎng)景,要處理200w個(gè)商戶(hù)的任務(wù)灰署,為了提高處理速度判帮,用多個(gè)線(xiàn)程局嘁,每個(gè)線(xiàn)程取100個(gè)商戶(hù)處理,就得給這100個(gè)商戶(hù)加鎖脊另,如果不加處理导狡,很難保證同一時(shí)刻兩個(gè)線(xiàn)程加鎖的商戶(hù)沒(méi)有重疊,這時(shí)可以按一個(gè)維度偎痛,比如某個(gè)標(biāo)簽旱捧,對(duì)商戶(hù)進(jìn)行分桶,然后一個(gè)任務(wù)處理一個(gè)分桶踩麦,處理完這個(gè)分桶再處理下一個(gè)分桶枚赡,減少競(jìng)爭(zhēng)。

重試的問(wèn)題谓谦。無(wú)論是簡(jiǎn)單實(shí)現(xiàn)還是redlock實(shí)現(xiàn)贫橙,都會(huì)有重試的邏輯。如果直接按上面的算法實(shí)現(xiàn)反粥,是會(huì)存在多個(gè)client幾乎在同一時(shí)刻獲取同一個(gè)鎖卢肃,然后每個(gè)client都鎖住了部分節(jié)點(diǎn),但是沒(méi)有一個(gè)client獲取大多數(shù)節(jié)點(diǎn)的情況才顿。解決的方案也很常見(jiàn)莫湘,在重試的時(shí)候讓多個(gè)節(jié)點(diǎn)錯(cuò)開(kāi),錯(cuò)開(kāi)的方式就是在重試時(shí)間中加一個(gè)隨機(jī)時(shí)間郑气。這樣并不能根治這個(gè)問(wèn)題幅垮,但是可以有效緩解問(wèn)題,親試有效尾组。

節(jié)點(diǎn)宕機(jī)

對(duì)于單master節(jié)點(diǎn)且沒(méi)有做持久化的場(chǎng)景忙芒,宕機(jī)就掛了,這個(gè)就必須在實(shí)現(xiàn)上支持重復(fù)操作讳侨,自己做好冪等呵萨。

對(duì)于多master的場(chǎng)景,比如redlock跨跨,我們來(lái)看這樣一個(gè)場(chǎng)景:

1.假設(shè)有5個(gè)redis的節(jié)點(diǎn):A潮峦、B、C歹叮、D跑杭、E铆帽,沒(méi)有做持久化咆耿。

2.client1從A、B爹橱、C 3個(gè)節(jié)點(diǎn)獲取鎖成功萨螺,那么client1獲取鎖成功窄做。

3.節(jié)點(diǎn)C掛了。

4.client2從C慰技、D椭盏、E獲取鎖成功,client2也獲取鎖成功吻商,那么在同一時(shí)刻client1和client2同時(shí)獲取鎖掏颊,redlock被玩壞了。

怎么解決呢艾帐?最容易想到的方案是打開(kāi)持久化乌叶。持久化可以做到持久化每一條redis命令,但這對(duì)性能影響會(huì)很大柒爸,一般不會(huì)采用准浴,如果不采用這種方式,在節(jié)點(diǎn)掛的時(shí)候肯定會(huì)損失小部分的數(shù)據(jù)捎稚,可能我們的鎖就在其中乐横。 另一個(gè)方案是延遲啟動(dòng)。就是一個(gè)節(jié)點(diǎn)掛了修復(fù)后今野,不立即加入葡公,而是等待一段時(shí)間再加入,等待時(shí)間要大于宕機(jī)那一刻所有鎖的最大TTL腥泥。 但這個(gè)方案依然不能解決問(wèn)題匾南,如果在上述步驟3中B和C都掛了呢,那么只剩A蛔外、D蛆楞、E三個(gè)節(jié)點(diǎn),從D和E獲取鎖成功就可以了夹厌,還是會(huì)出問(wèn)題豹爹。那么只能增加master節(jié)點(diǎn)的總量,緩解這個(gè)問(wèn)題了矛纹。增加master節(jié)點(diǎn)會(huì)提高穩(wěn)定性臂聋,但是也增加了成本,需要在兩者之間權(quán)衡或南。

任務(wù)執(zhí)行時(shí)間超過(guò)鎖的TTL


之前產(chǎn)線(xiàn)上出現(xiàn)過(guò)因?yàn)榫W(wǎng)絡(luò)延遲導(dǎo)致任務(wù)的執(zhí)行時(shí)間遠(yuǎn)超預(yù)期孩等,鎖過(guò)期,被多個(gè)線(xiàn)程執(zhí)行的情況采够。 這個(gè)問(wèn)題是所有分布式鎖都要面臨的問(wèn)題肄方,包括基于zookeeper和DB實(shí)現(xiàn)的分布式鎖,這是鎖過(guò)期了和client不知道鎖過(guò)期了之間的矛盾蹬癌。 在加鎖的時(shí)候权她,我們一般都會(huì)給一個(gè)鎖的TTL虹茶,這是為了防止加鎖后client宕機(jī),鎖無(wú)法被釋放的問(wèn)題隅要。但是所有這種姿勢(shì)的用法都會(huì)面臨同一個(gè)問(wèn)題蝴罪,就是沒(méi)發(fā)保證client的執(zhí)行時(shí)間一定小于鎖的TTL。雖然大多數(shù)程序員都會(huì)樂(lè)觀的認(rèn)為這種情況不可能發(fā)生步清,我也曾經(jīng)這么認(rèn)為要门,直到被現(xiàn)實(shí)一次又一次的打臉。

Martin Kleppmann也質(zhì)疑過(guò)這一點(diǎn)廓啊,這里直接用他的圖:

1.Client1獲取到鎖

2.Client1開(kāi)始任務(wù)暂衡,然后發(fā)生了STW的GC,時(shí)間超過(guò)了鎖的過(guò)期時(shí)間

3.Client2 獲取到鎖崖瞭,開(kāi)始了任務(wù)

4.Client1的GC結(jié)束狂巢,繼續(xù)任務(wù),這個(gè)時(shí)候Client1和Client2都認(rèn)為自己獲取了鎖书聚,都會(huì)處理任務(wù)唧领,從而發(fā)生錯(cuò)誤。

Martin Kleppmann舉的是GC的例子雌续,我碰到的是網(wǎng)絡(luò)延遲的情況斩个。不管是哪種情況,不可否認(rèn)的是這種情況無(wú)法避免驯杜,一旦出現(xiàn)很容易懵逼受啥。

如何解決呢?一種解決方案是不設(shè)置TTL鸽心,而是在獲取鎖成功后滚局,給鎖加一個(gè)watchdog,watchdog會(huì)起一個(gè)定時(shí)任務(wù)顽频,在鎖沒(méi)有被釋放且快要過(guò)期的時(shí)候會(huì)續(xù)期藤肢。這樣說(shuō)有些抽象,下面結(jié)合redisson源碼說(shuō)下:

public class RedissonLock extends RedissonExpirable implements RLock {

? ? ...

? ? @Override

? ? public void lock() {

? ? ? ? try {

? ? ? ? ? ? lockInterruptibly();

? ? ? ? } catch (InterruptedException e) {

? ? ? ? ? ? Thread.currentThread().interrupt();

? ? ? ? }

? ? }

? ? @Override

? ? public void lock(long leaseTime, TimeUnit unit) {

? ? ? ? try {

? ? ? ? ? ? lockInterruptibly(leaseTime, unit);

? ? ? ? } catch (InterruptedException e) {

? ? ? ? ? ? Thread.currentThread().interrupt();

? ? ? ? }

? ? }

? ? ...

}

redisson常用的加鎖api是上面兩個(gè)糯景,一個(gè)是不傳入TTL嘁圈,這時(shí)是redisson自己維護(hù),會(huì)主動(dòng)續(xù)期蟀淮;另外一種是自己傳入TTL最住,這種redisson就不會(huì)幫我們自動(dòng)續(xù)期了,或者自己將leaseTime的值傳成-1怠惶,但是不建議這種方式涨缚,既然已經(jīng)有現(xiàn)成的API了,何必還要用這種奇怪的寫(xiě)法呢甚疟。 接下來(lái)分析下不傳參的方法的加鎖邏輯:

public class RedissonLock extends RedissonExpirable implements RLock {

? ? ...


? ? public static final long LOCK_EXPIRATION_INTERVAL_SECONDS = 30;

? ? protected long internalLockLeaseTime = TimeUnit.SECONDS.toMillis(LOCK_EXPIRATION_INTERVAL_SECONDS);


? ? @Override

? ? public void lock() {

? ? ? ? try {

? ? ? ? ? ? lockInterruptibly();

? ? ? ? } catch (InterruptedException e) {

? ? ? ? ? ? Thread.currentThread().interrupt();

? ? ? ? }

? ? }


? ? @Override

? ? public void lockInterruptibly() throws InterruptedException {

? ? ? ? lockInterruptibly(-1, null);

? ? }


? ? @Override

? ? public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {

? ? ? ? long threadId = Thread.currentThread().getId();

? ? ? ? Long ttl = tryAcquire(leaseTime, unit, threadId);

? ? ? ? // lock acquired

? ? ? ? if (ttl == null) {

? ? ? ? ? ? return;

? ? ? ? }

? ? ? ? RFuture<RedissonLockEntry> future = subscribe(threadId);

? ? ? ? commandExecutor.syncSubscription(future);

? ? ? ? try {

? ? ? ? ? ? while (true) {

? ? ? ? ? ? ? ? ttl = tryAcquire(leaseTime, unit, threadId);

? ? ? ? ? ? ? ? // lock acquired

? ? ? ? ? ? ? ? if (ttl == null) {

? ? ? ? ? ? ? ? ? ? break;

? ? ? ? ? ? ? ? }

? ? ? ? ? ? ? ? // waiting for message

? ? ? ? ? ? ? ? if (ttl >= 0) {

? ? ? ? ? ? ? ? ? ? getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);

? ? ? ? ? ? ? ? } else {

? ? ? ? ? ? ? ? ? ? getEntry(threadId).getLatch().acquire();

? ? ? ? ? ? ? ? }

? ? ? ? ? ? }

? ? ? ? } finally {

? ? ? ? ? ? unsubscribe(future, threadId);

? ? ? ? }

//? ? ? ? get(lockAsync(leaseTime, unit));

? ? }

? ? private Long tryAcquire(long leaseTime, TimeUnit unit, long threadId) {

? ? ? ? return get(tryAcquireAsync(leaseTime, unit, threadId));

? ? }

? ? private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {

? ? ? ? if (leaseTime != -1) {

? ? ? ? ? ? return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);

? ? ? ? }

? ? ? ? RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(LOCK_EXPIRATION_INTERVAL_SECONDS, TimeUnit.SECONDS, threadId, RedisCommands.EVAL_LONG);

? ? ? ? ttlRemainingFuture.addListener(new FutureListener<Long>() {

? ? ? ? ? ? @Override

? ? ? ? ? ? public void operationComplete(Future<Long> future) throws Exception {

? ? ? ? ? ? ? ? if (!future.isSuccess()) {

? ? ? ? ? ? ? ? ? ? return;

? ? ? ? ? ? ? ? }

? ? ? ? ? ? ? ? Long ttlRemaining = future.getNow();

? ? ? ? ? ? ? ? // lock acquired

? ? ? ? ? ? ? ? if (ttlRemaining == null) {

? ? ? ? ? ? ? ? ? ? scheduleExpirationRenewal(threadId);

? ? ? ? ? ? ? ? }

? ? ? ? ? ? }

? ? ? ? });

? ? ? ? return ttlRemainingFuture;

? ? }

? ? private void scheduleExpirationRenewal(final long threadId) {

? ? ? ? if (expirationRenewalMap.containsKey(getEntryName())) {

? ? ? ? ? ? return;

? ? ? ? }

? ? ? ? Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {

? ? ? ? ? ? @Override

? ? ? ? ? ? public void run(Timeout timeout) throws Exception {


? ? ? ? ? ? ? ? RFuture<Boolean> future = commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,

? ? ? ? ? ? ? ? ? ? ? ? "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +

? ? ? ? ? ? ? ? ? ? ? ? ? ? "redis.call('pexpire', KEYS[1], ARGV[1]); " +

? ? ? ? ? ? ? ? ? ? ? ? ? ? "return 1; " +

? ? ? ? ? ? ? ? ? ? ? ? "end; " +

? ? ? ? ? ? ? ? ? ? ? ? "return 0;",

? ? ? ? ? ? ? ? ? ? ? ? ? Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));


? ? ? ? ? ? ? ? future.addListener(new FutureListener<Boolean>() {

? ? ? ? ? ? ? ? ? ? @Override

? ? ? ? ? ? ? ? ? ? public void operationComplete(Future<Boolean> future) throws Exception {

? ? ? ? ? ? ? ? ? ? ? ? expirationRenewalMap.remove(getEntryName());

? ? ? ? ? ? ? ? ? ? ? ? if (!future.isSuccess()) {

? ? ? ? ? ? ? ? ? ? ? ? ? ? log.error("Can't update lock " + getName() + " expiration", future.cause());

? ? ? ? ? ? ? ? ? ? ? ? ? ? return;

? ? ? ? ? ? ? ? ? ? ? ? }


? ? ? ? ? ? ? ? ? ? ? ? if (future.getNow()) {

? ? ? ? ? ? ? ? ? ? ? ? ? ? // reschedule itself

? ? ? ? ? ? ? ? ? ? ? ? ? ? scheduleExpirationRenewal(threadId);

? ? ? ? ? ? ? ? ? ? ? ? }

? ? ? ? ? ? ? ? ? ? }

? ? ? ? ? ? ? ? });

? ? ? ? ? ? }

? ? ? ? }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);

? ? ? ? if (expirationRenewalMap.putIfAbsent(getEntryName(), task) != null) {

? ? ? ? ? ? task.cancel();

? ? ? ? }

? ? }


? ? ...

}

可以看到仗岖,最后加鎖的邏輯會(huì)進(jìn)入到org.redisson.RedissonLock#tryAcquireAsync中,在獲取鎖成功后览妖,會(huì)進(jìn)入scheduleExpirationRenewal轧拄,這里面初始化了一個(gè)定時(shí)器,dely的時(shí)間是internalLockLeaseTime / 3讽膏。在redisson中檩电,internalLockLeaseTime是30s,也就是每隔10s續(xù)期一次府树,每次30s俐末。 如果是基于zookeeper實(shí)現(xiàn)的分布式鎖,可以利用zookeeper檢查節(jié)點(diǎn)是否存活奄侠,從而實(shí)現(xiàn)續(xù)期卓箫,zookeeper分布式鎖沒(méi)用過(guò),不詳細(xì)說(shuō)垄潮。

不過(guò)這種做法也無(wú)法百分百做到同一時(shí)刻只有一個(gè)client獲取到鎖烹卒,如果續(xù)期失敗,比如發(fā)生了Martin Kleppmann所說(shuō)的STW的GC弯洗,或者client和redis集群失聯(lián)了旅急,只要續(xù)期失敗,就會(huì)造成同一時(shí)刻有多個(gè)client獲得鎖了牡整。在我的場(chǎng)景下藐吮,我將鎖的粒度拆小了,redisson的續(xù)期機(jī)制已經(jīng)夠用了逃贝。 如果要做得更嚴(yán)格谣辞,得加一個(gè)續(xù)期失敗終止任務(wù)的邏輯。這種做法在以前Python的代碼中實(shí)現(xiàn)過(guò)沐扳,Java還沒(méi)有碰到這么嚴(yán)格的情況潦闲。

這里也提下Martin Kleppmann的解決方案,我自己覺(jué)得這個(gè)方案并不靠譜迫皱,原因后面會(huì)提到歉闰。 他的方案是讓加鎖的資源自己維護(hù)一套保證不會(huì)因加鎖失敗而導(dǎo)致多個(gè)client在同一時(shí)刻訪(fǎng)問(wèn)同一個(gè)資源的情況。

在客戶(hù)端獲取鎖的同時(shí)卓起,也獲取到一個(gè)資源的token和敬,這個(gè)token是單調(diào)遞增的,每次在寫(xiě)資源時(shí)戏阅,都檢查當(dāng)前的token是否是較老的token昼弟,如果是就不讓寫(xiě)。對(duì)于上面的場(chǎng)景奕筐,Client1獲取鎖的同時(shí)分配一個(gè)33的token舱痘,Client2獲取鎖的時(shí)候分配一個(gè)34的token变骡,在client1 GC期間,Client2已經(jīng)寫(xiě)了資源芭逝,這時(shí)最大的token就是34了塌碌,client1 從GC中回來(lái),再帶著33的token寫(xiě)資源時(shí)旬盯,會(huì)因?yàn)閠oken過(guò)期被拒絕台妆。這種做法需要資源那一邊提供一個(gè)token生成器。 對(duì)于這種fencing的方案胖翰,我有幾點(diǎn)問(wèn)題:

1.無(wú)法保證事務(wù)接剩。示意圖中畫(huà)的只有34訪(fǎng)問(wèn)了storage,但是在實(shí)際場(chǎng)景中萨咳,可能出現(xiàn)在一個(gè)任務(wù)內(nèi)多次訪(fǎng)問(wèn)storage的情況懊缺,而且必須是原子的。如果client1帶著33token在GC前訪(fǎng)問(wèn)過(guò)一次storage培他,然后發(fā)生了GC桐汤。client2獲取到鎖,帶著34的token也訪(fǎng)問(wèn)了storage靶壮,這時(shí)兩個(gè)client寫(xiě)入的數(shù)據(jù)是否還能保證數(shù)據(jù)正確怔毛?如果不能,那么這種方案就有缺陷腾降,除非storage自己有其他機(jī)制可以保證拣度,比如事務(wù)機(jī)制;如果能螃壤,那么這里的token就是多余的抗果,fencing的方案就是多此一舉。

2.高并發(fā)場(chǎng)景不實(shí)用奸晴。因?yàn)槊看沃挥凶畲蟮膖oken能寫(xiě)冤馏,這樣storage的訪(fǎng)問(wèn)就是線(xiàn)性的,在高并發(fā)場(chǎng)景下寄啼,這種方式會(huì)極大的限制吞吐量逮光,而分布式鎖也大多是在這種場(chǎng)景下用的,很矛盾的設(shè)計(jì)墩划。

3.這是所有分布式鎖的問(wèn)題涕刚。這個(gè)方案是一個(gè)通用的方案,可以和Redlock用乙帮,也可以和其他的lock用杜漠。所以我理解僅僅是一個(gè)和Redlock無(wú)關(guān)的解決方案。

系統(tǒng)時(shí)鐘漂移

這個(gè)問(wèn)題只是考慮過(guò),但在實(shí)際項(xiàng)目中并沒(méi)有碰到過(guò)驾茴,因?yàn)槔碚撋鲜强赡艹霈F(xiàn)的盼樟,這里也說(shuō)下。 redis的過(guò)期時(shí)間是依賴(lài)系統(tǒng)時(shí)鐘的锈至,如果時(shí)鐘漂移過(guò)大時(shí)會(huì)影響到過(guò)期時(shí)間的計(jì)算晨缴。

為什么系統(tǒng)時(shí)鐘會(huì)存在漂移呢?先簡(jiǎn)單說(shuō)下系統(tǒng)時(shí)間裹赴,linux提供了兩個(gè)系統(tǒng)時(shí)間:clock realtime和clock monotonicclockrealtime也就是xtime/wall time,這個(gè)時(shí)間時(shí)可以被用戶(hù)改變的诀浪,被NTP改變棋返,gettimeofday拿的就是這個(gè)時(shí)間,redis的過(guò)期計(jì)算用的也是這個(gè)時(shí)間雷猪。 clock monotonic 睛竣,直譯過(guò)來(lái)時(shí)單調(diào)時(shí)間,不會(huì)被用戶(hù)改變求摇,但是會(huì)被NTP改變射沟。

最理想的情況時(shí),所有系統(tǒng)的時(shí)鐘都時(shí)時(shí)刻刻和NTP服務(wù)器保持同步与境,但這顯然時(shí)不可能的验夯。導(dǎo)致系統(tǒng)時(shí)鐘漂移的原因有兩個(gè):

系統(tǒng)的時(shí)鐘和NTP服務(wù)器不同步。這個(gè)目前沒(méi)有特別好的解決方案摔刁,只能相信運(yùn)維同學(xué)了挥转。

clock realtime被人為修改。在實(shí)現(xiàn)分布式鎖時(shí)共屈,不要使用clock realtime绑谣。不過(guò)很可惜,redis使用的就是這個(gè)時(shí)間拗引,我看了下Redis 5.0源碼借宵,使用的還是clock realtime。Antirez說(shuō)過(guò)改成clock monotonic的矾削,不過(guò)大佬還沒(méi)有改壤玫。也就是說(shuō),人為修改redis服務(wù)器的時(shí)間哼凯,就能讓redis出問(wèn)題了垦细。

總結(jié)

本文從一個(gè)簡(jiǎn)單的基于redis的分布式鎖出發(fā),到更復(fù)雜的Redlock的實(shí)現(xiàn)挡逼,介紹了在使用分布式鎖的過(guò)程中才踩過(guò)的一些坑以及解決方案括改。

最后:

面都是自己整理好的!我就把資料貢獻(xiàn)出來(lái)給有需要的人!順便求一波關(guān)注嘱能。

學(xué)習(xí)我們是認(rèn)真的吝梅,拿大廠(chǎng)offer是勢(shì)在必得的。?java(想了解更多點(diǎn)一下哦)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末惹骂,一起剝皮案震驚了整個(gè)濱河市苏携,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌对粪,老刑警劉巖右冻,帶你破解...
    沈念sama閱讀 206,839評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異著拭,居然都是意外死亡纱扭,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)儡遮,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)乳蛾,“玉大人,你說(shuō)我怎么就攤上這事鄙币∷嘁叮” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,116評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵十嘿,是天一觀的道長(zhǎng)因惭。 經(jīng)常有香客問(wèn)我,道長(zhǎng)绩衷,這世上最難降的妖魔是什么筛欢? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,371評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮唇聘,結(jié)果婚禮上版姑,老公的妹妹穿的比我還像新娘。我一直安慰自己迟郎,他們只是感情好剥险,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著宪肖,像睡著了一般表制。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上控乾,一...
    開(kāi)封第一講書(shū)人閱讀 49,111評(píng)論 1 285
  • 那天么介,我揣著相機(jī)與錄音,去河邊找鬼蜕衡。 笑死壤短,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播久脯,決...
    沈念sama閱讀 38,416評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼纳胧,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了帘撰?” 一聲冷哼從身側(cè)響起跑慕,我...
    開(kāi)封第一講書(shū)人閱讀 37,053評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎摧找,沒(méi)想到半個(gè)月后核行,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,558評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蹬耘,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評(píng)論 2 325
  • 正文 我和宋清朗相戀三年芝雪,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片婆赠。...
    茶點(diǎn)故事閱讀 38,117評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡绵脯,死狀恐怖佳励,靈堂內(nèi)的尸體忽然破棺而出休里,到底是詐尸還是另有隱情,我是刑警寧澤赃承,帶...
    沈念sama閱讀 33,756評(píng)論 4 324
  • 正文 年R本政府宣布妙黍,位于F島的核電站,受9級(jí)特大地震影響瞧剖,放射性物質(zhì)發(fā)生泄漏拭嫁。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評(píng)論 3 307
  • 文/蒙蒙 一抓于、第九天 我趴在偏房一處隱蔽的房頂上張望做粤。 院中可真熱鬧,春花似錦捉撮、人聲如沸怕品。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,315評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)肉康。三九已至,卻和暖如春灼舍,著一層夾襖步出監(jiān)牢的瞬間吼和,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,539評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工骑素, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留炫乓,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,578評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像厢岂,于是被迫代替她去往敵國(guó)和親光督。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評(píng)論 2 345

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

  • 簡(jiǎn)述 使用分布式鎖的目的: 提高效率:使用分布式鎖避免不同節(jié)點(diǎn)重復(fù)性的操作塔粒,比如:推送结借、定時(shí)任務(wù)等 保證正確性:避...
    Jerry_06ed閱讀 554評(píng)論 0 4
  • 引言 目前很多系統(tǒng)都是使用redis作為分布式鎖,如果redis是單節(jié)點(diǎn)部署卒茬,基本上不會(huì)出現(xiàn)什么問(wèn)題船老。但如果red...
    落落的博客閱讀 458評(píng)論 0 1
  • 在很多環(huán)境下,多個(gè)不同的進(jìn)程需要以排他的形式使用共享資源圃酵,這是使用分布式鎖機(jī)制是一種傳統(tǒng)但有效的方案柳畔。 有很多的庫(kù)...
    BigFish__閱讀 1,515評(píng)論 0 0
  • 東嶼尋閱讀 172評(píng)論 0 5
  • 喜新厭舊 一個(gè)月前就開(kāi)始在購(gòu)物車(chē)?yán)锊粩嗟募舆M(jìn)去新的東西薪韩,有的是必需品,有的是出于喜歡捌锭,有的是沒(méi)有也可以俘陷,有則更好的...
    子川vs玄冰閱讀 443評(píng)論 4 1