日本高清免费一本视频100禁_在线不卡欧美精品一区二区三区_国产一区二区好的精华液_中文综合在线_国产啊啊啊视频在线观看_大地资源网免费观看高清

中國(guó)專(zhuān)業(yè)IT外包服務(wù)

用心服務(wù)每一天
IT之道-艾銻知道

您當(dāng)前位置: 主頁(yè) > 資訊動(dòng)態(tài) > 艾銻分享 >

服務(wù)器運(yùn)維維護(hù) Redis分布式鎖踩坑不慌,這里有超全的解決方案


2020-04-29 20:13 作者:admin

服務(wù)器運(yùn)維維護(hù) Redis分布式鎖踩坑不慌,這里有超全的解決方案

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

開(kāi)頭

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

你真的需要分布式鎖嗎?

用到分布式鎖說(shuō)明遇到了多個(gè)進(jìn)程共同訪問(wèn)同一個(gè)資源的問(wèn)題,
一般是在兩個(gè)場(chǎng)景下會(huì)防止對(duì)同一個(gè)資源的重復(fù)訪問(wèn):
· 提高效率。比如多個(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ì)造成其他更大的損失。也就是允許偶爾的失敗。
· 保證正確性。這種情況對(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)?
· 加鎖和解鎖的鎖必須是同一個(gè),常見(jiàn)的解決方案是給每個(gè)鎖一個(gè)鑰匙(唯一ID),加鎖時(shí)生成,解鎖時(shí)判斷。
· 不能讓一個(gè)資源永久加鎖。常見(jiàn)的解決方案是給一個(gè)鎖的過(guò)期時(shí)間。當(dāng)然了還有其他方案,后面再說(shuō)。
一個(gè)可復(fù)制粘貼的實(shí)現(xiàn)方式如下:
加鎖
1. public static boolean tryLock(String key, String uniqueId, int seconds) { 
2.     return "OK".equals(jedis.set(key, uniqueId, "NX", "EX", seconds)); 
3. } 
這里其實(shí)是調(diào)用了 SET key value PX milliseoncds NX。
不明白這個(gè)命令的參考下SET key value [EX seconds|PX milliseconds] [NX|XX] [KEEPTTL]:https://redis.io/commands/set
解鎖
1. public static boolean releaseLock(String key, String uniqueId) { 
2.     String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " + 
3.             "return redis.call('del', KEYS[1]) else return 0 end"; 
4.     return jedis.eval( 
5.         luaScript,  
6.         Collections.singletonList(key),  
7.         Collections.singletonList(uniqueId) 
8.     ).equals(1L); 
9. } 
這段實(shí)現(xiàn)的精髓在那個(gè)簡(jiǎn)單的lua腳本上,先判斷唯一ID是否相等再操作。
靠譜嗎?
這樣的實(shí)現(xiàn)有什么問(wèn)題呢?
單點(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加鎖的。
執(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è)方面。
· 獲取鎖的時(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í)間。
· 被加鎖的資源太大。加鎖的方案本身就是會(huì)為了正確性而犧牲并發(fā)的,犧牲和資源大小成正比。這個(gè)時(shí)候可以考慮對(duì)資源做拆分,拆分的方式有兩種:
從業(yè)務(wù)上將鎖住的資源拆分成多段,每段分開(kāi)加鎖。比如,我要對(duì)一個(gè)商戶做若干個(gè)操作,操作前要鎖住這個(gè)商戶,這時(shí)我可以將若干個(gè)操作拆成多個(gè)獨(dú)立的步驟分開(kāi)加鎖,提高并發(fā)。
用分桶的思想,將一個(gè)資源拆分成多個(gè)桶,一個(gè)加鎖失敗立即嘗試下一個(gè)。比如批量任務(wù)處理的場(chǎng)景,要處理200w個(gè)商戶的任務(wù),為了提高處理速度,用多個(gè)線程,每個(gè)線程取100個(gè)商戶處理,就得給這100個(gè)商戶加鎖,如果不加處理,很難保證同一時(shí)刻兩個(gè)線程加鎖的商戶沒(méi)有重疊,這時(shí)可以按一個(gè)維度,比如某個(gè)標(biāo)簽,對(duì)商戶進(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)景:
· 假設(shè)有5個(gè)redis的節(jié)點(diǎn):A、B、C、D、E,沒(méi)有做持久化。
· client1從A、B、C 3個(gè)節(jié)點(diǎn)獲取鎖成功,那么client1獲取鎖成功。
· 節(jié)點(diǎn)C掛了。
· 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)過(guò)因?yàn)榫W(wǎng)絡(luò)延遲導(dǎo)致任務(wù)的執(zhí)行時(shí)間遠(yuǎn)超預(yù)期,鎖過(guò)期,被多個(gè)線程執(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)法保證client的執(zhí)行時(shí)間一定小于鎖的TTL。雖然大多數(shù)程序員都會(huì)樂(lè)觀的認(rèn)為這種情況不可能發(fā)生,我也曾經(jīng)這么認(rèn)為,直到被現(xiàn)實(shí)一次又一次的打臉。
Martin Kleppmann也質(zhì)疑過(guò)這一點(diǎn):
· Client1獲取到鎖;
· Client1開(kāi)始任務(wù),然后發(fā)生了STW的GC,時(shí)間超過(guò)了鎖的過(guò)期時(shí)間;
· Client2 獲取到鎖,開(kāi)始了任務(wù);
· 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ō)下:
1. public class RedissonLock extends RedissonExpirable implements RLock { 
2.     ... 
3.     @Override 
4.     public void lock() { 
5.         try { 
6.             lockInterruptibly(); 
7.         } catch (InterruptedException e) { 
8.             Thread.currentThread().interrupt(); 
9.         } 
10.     } 
11.  
12.     @Override 
13.     public void lock(long leaseTime, TimeUnit unit) { 
14.         try { 
15.             lockInterruptibly(leaseTime, unit); 
16.         } catch (InterruptedException e) { 
17.             Thread.currentThread().interrupt(); 
18.         } 
19.     } 
20.     ... 
21.  } 
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)分析下不傳參的方法的加鎖邏輯:
1. public class RedissonLock extends RedissonExpirable implements RLock { 
2.  
3.     ... 
4.          
5.     public static final long LOCK_EXPIRATION_INTERVAL_SECONDS = 30; 
6.     protected long internalLockLeaseTime = TimeUnit.SECONDS.toMillis(LOCK_EXPIRATION_INTERVAL_SECONDS); 
7.  
8.          
9.     @Override 
10.     public void lock() { 
11.         try { 
12.             lockInterruptibly(); 
13.         } catch (InterruptedException e) { 
14.             Thread.currentThread().interrupt(); 
15.         } 
16.     } 
17.          
18.     @Override 
19.     public void lockInterruptibly() throws InterruptedException { 
20.         lockInterruptibly(-1, null); 
21.     } 
22.      
23.     @Override 
24.     public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException { 
25.         long threadId = Thread.currentThread().getId(); 
26.         Long ttl = tryAcquire(leaseTime, unit, threadId); 
27.         // lock acquired 
28.         if (ttl == null) { 
29.             return; 
30.         } 
31.  
32.         RFuture<RedissonLockEntry> future = subscribe(threadId); 
33.         commandExecutor.syncSubscription(future); 
34.  
35.         try { 
36.             while (true) { 
37.                 ttl = tryAcquire(leaseTime, unit, threadId); 
38.                 // lock acquired 
39.                 if (ttl == null) { 
40.                     break; 
41.                 } 
42.  
43.                 // waiting for message 
44.                 if (ttl >= 0) { 
45.                     getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS); 
46.                 } else { 
47.                     getEntry(threadId).getLatch().acquire(); 
48.                 } 
49.             } 
50.         } finally { 
51.             unsubscribe(future, threadId); 
52.         } 
53. //        get(lockAsync(leaseTime, unit)); 
54.     } 
55.  
56.     private Long tryAcquire(long leaseTime, TimeUnit unit, long threadId) { 
57.         return get(tryAcquireAsync(leaseTime, unit, threadId)); 
58.     } 
59.  
60.     private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) { 
61.         if (leaseTime != -1) { 
62.             return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG); 
63.         } 
64.         RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(LOCK_EXPIRATION_INTERVAL_SECONDS, TimeUnit.SECONDS, threadId, RedisCommands.EVAL_LONG); 
65.         ttlRemainingFuture.addListener(new FutureListener<Long>() { 
66.             @Override 
67.             public void operationComplete(Future<Long> future) throws Exception { 
68.                 if (!future.isSuccess()) { 
69.                     return; 
70.                 } 
71.  
72.                 Long ttlRemaining = future.getNow(); 
73.                 // lock acquired 
74.                 if (ttlRemaining == null) { 
75.                     scheduleExpirationRenewal(threadId); 
76.                 } 
77.             } 
78.         }); 
79.         return ttlRemainingFuture; 
80.     } 
81.  
82.     private void scheduleExpirationRenewal(final long threadId) { 
83.         if (expirationRenewalMap.containsKey(getEntryName())) { 
84.             return; 
85.         } 
86.  
87.         Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() { 
88.             @Override 
89.             public void run(Timeout timeout) throws Exception { 
90.                  
91.                 RFuture<Boolean> future = commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, 
92.                         "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " + 
93.                             "redis.call('pexpire', KEYS[1], ARGV[1]); " + 
94.                             "return 1; " + 
95.                         "end; " + 
96.                         "return 0;", 
97.                           Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId)); 
98.                  
99.                 future.addListener(new FutureListener<Boolean>() { 
100.                     @Override 
101.                     public void operationComplete(Future<Boolean> future) throws Exception { 
102.                         expirationRenewalMap.remove(getEntryName()); 
103.                         if (!future.isSuccess()) { 
104.                             log.error("Can't update lock " + getName() + " expiration", future.cause()); 
105.                             return; 
106.                         } 
107.                          
108.                         if (future.getNow()) { 
109.                             // reschedule itself 
110.                             scheduleExpirationRenewal(threadId); 
111.                         } 
112.                     } 
113.                 }); 
114.             } 
115.         }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS); 
116.  
117.         if (expirationRenewalMap.putIfAbsent(getEntryName(), task) != null) { 
118.             task.cancel(); 
119.         } 
120.     } 
121.  
122.      
123.     ... 
124. } 
可以看到,最后加鎖的邏輯會(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í)刻訪問(wèn)同一個(gè)資源的情況。
 
在客戶端獲取鎖的同時(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)題:
· 無(wú)法保證事務(wù)。示意圖中畫(huà)的只有34訪問(wèn)了storage,但是在實(shí)際場(chǎng)景中,可能出現(xiàn)在一個(gè)任務(wù)內(nèi)多次訪問(wèn)storage的情況,而且必須是原子的。如果client1帶著33token在GC前訪問(wèn)過(guò)一次storage,然后發(fā)生了GC。client2獲取到鎖,帶著34的token也訪問(wèn)了storage,這時(shí)兩個(gè)client寫(xiě)入的數(shù)據(jù)是否還能保證數(shù)據(jù)正確?如果不能,那么這種方案就有缺陷,除非storage自己有其他機(jī)制可以保證,比如事務(wù)機(jī)制;如果能,那么這里的token就是多余的,fencing的方案就是多此一舉。
· 高并發(fā)場(chǎng)景不實(shí)用。因?yàn)槊看沃挥凶畲蟮膖oken能寫(xiě),這樣storage的訪問(wèn)就是線性的,在高并發(fā)場(chǎng)景下,這種方式會(huì)極大的限制吞吐量,而分布式鎖也大多是在這種場(chǎng)景下用的,很矛盾的設(shè)計(jì)。
· 這是所有分布式鎖的問(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í)間是依賴系統(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 monotonic。clock realtime也就是xtime/wall time,這個(gè)時(shí)間可以被用戶改變的,被NTP改變,gettimeofday拿的就是這個(gè)時(shí)間,redis的過(guò)期計(jì)算用的也是這個(gè)時(shí)間。
clock monotonic ,直譯過(guò)來(lái)時(shí)單調(diào)時(shí)間,不會(huì)被用戶改變,但是會(huì)被NTP改變。
· 最理想的情況,所有系統(tǒng)的時(shí)鐘都時(shí)時(shí)刻刻和NTP服務(wù)器保持同步,但這顯然是不可能的。導(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)題了。



相關(guān)文章

IT外包服務(wù)
二維碼 關(guān)閉
主站蜘蛛池模板: 精品99久久_日本人妻人人人澡人人爽_黄色片免费看_黄绝一级毛片_国产精品性色一区二区三区_天天做爰天天爽_性暴力欧美猛交在线播放_久久精品国产亚洲77788 | 精品一区二区三区影院在线午夜_国产成人AV综合亚洲色欲_交换娇妻呻吟hd中文字幕_东北老富婆高潮大叫对白_日日摸夜夜添夜夜躁好吊_97婷婷狠狠成为人免费视频_成人网站网址在线观看播放_波多野结衣av一本一道 国产毛片一线_亚洲性生活片_天天干干夜夜_超碰男人_妞干网国产_3D动漫同人精品无码专区_2021av视频_国产精品久久久久久久久久久痴汉 | 精品一二三四视频_天堂8在线天堂资源BT_草操视频_久久www免费人成看片小草_五月婷婷六月丁香欧美综合_精品二区一国产va在线观看_青青草免费观看视频_极品尤物一区二区三区 | 亚洲国产熟妇无码日韩_国产亚洲欧美日韩国产片_女人天天干夜夜爽视频_日韩在线观看视频一区二区三区_中国XVIDEOS厕所偷窥_狠狠色狠狠色综合日日不卡_在线观看免费亚洲_国产又色又爽又黄的网站在线 | 91视频免费观看网址_黄色大片一区_国产精品一区久久久_亚洲精品色哟哟_最近中文字幕一区_婷婷综合缴情亚洲狠狠小说_国产成年无码AV片在线_日本最新一区 | 成人亚洲a片v一区二区三区_亚洲日本精品国产第一区_噜噜噜私人影院_www.超碰在线观看_亚洲精品久久激情国产片_国产欧美日韩在线视频_精品亚洲一区二区三区在线播放_亚洲国产成人精品无码区 | 免费av看片_69久久精品无码一区二区_亚洲成人一二三区_中国大陆黄色片_一区影视_男人扒开添女人下部免费视频_又大又粗又硬又黄的免费视频_国产字幕 | 大陆一级片_一级成人黄色片_av无码一区二区三区午夜_成人在线黄色_密臀av一区二区三区_在线免费黄色小视频_久久亚洲福利_成人午夜网址 | 黑巨人与欧美精品一区_殴美在线一区二区不卡_极品美女扒开粉嫩小泬图片_少妇系列之白嫩人妻_欧洲色网站_高清自拍亚洲精品二区_日本免费a∨_有坂深雪在线xx99av | 日韩女优一区二区三区_久久久久亚洲AV片无码V_日本国产一区_久久精品国产国产精品四凭_成av免费大片黄在线观看_日韩天堂一区_福利网站视频_国产精品人人妻人人爽久久 | 精国产品一区二区三区_国产亚洲欧美日韩亚洲中文色_欧美日韩a区_在线播放成人_成人a区_国产成人亚洲综合一区_A级毛片100部免费观看_日本一级特黄高潮 | 成人97精品毛片免费看_中国china体内裑精亚洲片_jiujiure国产_宅男在线免费视频_精品视频久久_日本伊人中文字幕_女子被狂揉下部羞羞图片_精品久久香蕉国产线看观看亚洲 | 亚洲久久视频_欧美国产日本_国产成人无码精品久久久免费_久久精品视频网站_边做边流奶水的人妻_国内网站成视频在线观看_色网免费观看_67194熟妇在线直接进入 | 欧美日韩另类一区二区_人人人人人你人人人人人_国产精品久久久久av免费_亚洲精品无码久久千人斩_欧美日韩在线精品一区二区三区激情综合_久久精品视频播放_午夜影院在线免费观看_国产黄片av毛片系列 | 热の无码热の有码热の综合_日本高清高色_日韩免费一级a毛片在线播放一级_好吊色国产_国产片免费福利片永久_91网页视频入口在线观看_精品在线欧美一区二区_狠狠躁夜夜躁av蜜臀少妇 | 成人性生活免费看_日韩区国产_成人精品视频99_2021精品极品国产色在线首页_亚洲中文字幕无码AV永久_久久久91av_1314免费观看www视频_欧洲美女与动性zozozo | 国产精品18久久久_一本一道久久a久久精品_国产精品久久久久久高潮_九九在线国产视频_blacked蜜桃精品一区_亚洲最新av网站_免费av手机在线观看片_成人亚洲视频在线观看 | 日本三级黄色中文字幕_久久国产精品波多野结衣AV_动漫精品一区二区三区_亚洲AV无码一区二区二三区∝_男人午夜av_91网址在线观看_91精品国产一区二区无码_无码专区国产精品一区 | 不卡视频一二三区_爱逼爱操综合网_一区二区在线免费播放_久久伊人av_久久九九国产精品怡红院_男女一边摸一边做爽爽的免费阅读_久久精品无码专区免费青青_91精品啪 | 国产人妻无码区免费九色_开心色av_av在线天_成人av1234567_国内精品伊人久久久久影院麻豆_国产成人精品人人_日韩在线黄色片_年轻的秘书在线 | 亚洲精品TV久久久久久久久久_天堂网资源WWW_日本新janpanese乱熟_午夜666_国产高清视频一区三区_日皮视频免费看_亚洲热在线视频_a人片中文字幕一区二区 | 亚洲一区二区久久久久久_亚洲精品国产高清一线久久_丝袜美女被遭强高潮网站_鲁一鲁操一操_中文字幕精品视频在线观看_精品在线一区_中国成人亚色综合网站_久久久123 | 国产91精品久久久久久_天天摸天天做天天爽天天弄_欧美在线观看视频一区_人人妻人人超人人_日韩无码专区_视频一区二区视频_日日橹狠狠爱欧美二区免费视频_好男人www在线影院官网 | 成年人视频在线看_欧美久久性视频_超碰aⅴ人人做人人爽欧美_噜噜噜91成人网_亚洲欧美丝袜精品久久_国产精品成人无码A片免费网址_91一二区_91久久久精品国产一区二区蜜臀 | 日韩1区在线_久久精品最新_麻豆乱码国产二区三区使用方法_狠狠天天_wwwxxxx中国_国产又猛又黄又爽三男一女_欧美变态另类ZOZO_亚洲成A人片在线观看国产 | 精品av中文字幕在线毛片_中国一级片_日批一级片_高清成人爽a毛片免费_美景之屋5在线观看_欧美乱妇无码毛片_中文亚洲成A人片在线观看_国产成人无码A区在线观看视频不卡 | 亚洲国产精品无码久久久久久曰_91极品反差在线_9999国产精品_99情趣网视频_国产欧美呀洲一区二区_久久亚洲精品无码AV大香_视频免费视频_美女高潮一区二区三区 | 狠狠操综合_99热这里只有精品5_国产aⅴ精品_日本在线视频www色_97夜夜澡人人波多野结衣_欧美一级一区二区三区_国产精品自拍系列_日韩精品免费综合视频在线播放 | av2018天堂网_黄漫网站在线观看_国产高清一_青青草娱乐视频_aaaa大片_最新日韩精品视频_91视频黄色_一区二区三区三州 | 嫩草91_中国三级黄色录像_亚洲欧美在线x视频_GOGOGO免费高清日本TV_亚洲日本VA午夜在线影院_国产精品系列视频_超碰91在线播放_c国产又粗又猛又爽又黄的视频站 | 一级欧美黄色片_在线免费色视频_天天透天天狠天天爱综合97_亚洲国产成人手机在线观看_99久久精品国产免费_亚洲色欲或者高潮影院_国产精品免费一区二区区_成人免费视频国产 | 亚洲精品国产片_精品综合久久_色AV永久无码影院AV_一区一区视频_青青草午夜色影院_农村熟女大胆露脸自拍_黄色毛片a级_在线观看日韩中文字幕 | www视频免费在线观看_女人精69xxx免费网站_国产99久久久国产精品成人免费_人人艹人人射_一级啪啪_好吊妞视频988在线播放_伊人干综合_日韩中文字幕无码一区二区三区 | 色欲午夜无码久久久久久_se亚洲_日本xxxx在线播放_在线精品观看国产_久久黑人_日韩一二三_国产精品网站在线观看免费传媒_在线色影院 | 成年人视频在线看_欧美久久性视频_超碰aⅴ人人做人人爽欧美_噜噜噜91成人网_亚洲欧美丝袜精品久久_国产精品成人无码A片免费网址_91一二区_91久久久精品国产一区二区蜜臀 | 亚洲精品乱码日本按摩久久久久_免费久久精品视频_九九色网站_久久成人秘免费无码_女友的滋味在线观看_极品少妇hdxx麻豆hdxx_伊人婷婷涩六月丁香七月_香港午夜三级A三级三点在线观看 | 欧美日韩中文亚洲_国产有码在线_亚洲精品小视频在线观看_久久精品一级片_丰满人妻一区二区三区视频53_亚洲一区二区av在线_欧美一级片黄色_直接看的69xxx | 奇米777官网_免费99精品国产自在在线_国产精品WWW夜色视频_国产美女无遮挡免费_9999视频_特级做爰片毛片免费看小说_国产白浆二区二区精品视频_欧美成人黑人xx视频免费观看 | 成人亚洲a片v一区二区三区_亚洲日本精品国产第一区_噜噜噜私人影院_www.超碰在线观看_亚洲精品久久激情国产片_国产欧美日韩在线视频_精品亚洲一区二区三区在线播放_亚洲国产成人精品无码区 | 国产熟妇的荡欲午夜视频_韩剧免费观看高清完整_女人色熟女乱_久久精品国产亚_日韩欧美一级视频_欧美xxxxx在线观看_日本免费视频www_欧美黑人牲交videossexeso | 欧美日韩一道本_欧美日韩在线视频不卡一区二区三区_亚州黄色_三区四区在线观看_不卡视频一区二区_亚洲免费观看视频_国产一级黄色录像_欧美男男激情videos高清 |