<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          分布式環(huán)境下,互斥性與冪等性問(wèn)題,分析與解決思路

          共 17272字,需瀏覽 35分鐘

           ·

          2022-02-20 12:46

          點(diǎn)擊下方“IT牧場(chǎng)”,選擇“設(shè)為星標(biāo)”

          隨著互聯(lián)網(wǎng)信息技術(shù)的飛速發(fā)展,數(shù)據(jù)量不斷增大,業(yè)務(wù)邏輯也日趨復(fù)雜,對(duì)系統(tǒng)的高并發(fā)訪問(wèn)、海量數(shù)據(jù)處理的場(chǎng)景也越來(lái)越多。如何用較低成本實(shí)現(xiàn)系統(tǒng)的高可用、易伸縮、可擴(kuò)展等目標(biāo)就顯得越發(fā)重要。

          為了解決這一系列問(wèn)題,系統(tǒng)架構(gòu)也在不斷演進(jìn)。傳統(tǒng)的集中式系統(tǒng)已經(jīng)逐漸無(wú)法滿足要求,分布式系統(tǒng)被使用在更多的場(chǎng)景中。

          分布式系統(tǒng)由獨(dú)立的服務(wù)器通過(guò)網(wǎng)絡(luò)松散耦合組成。在這個(gè)系統(tǒng)中每個(gè)服務(wù)器都是一臺(tái)獨(dú)立的主機(jī),服務(wù)器之間通過(guò)內(nèi)部網(wǎng)絡(luò)連接。分布式系統(tǒng)有以下幾個(gè)特點(diǎn):

          • 可擴(kuò)展性:可通過(guò)橫向水平擴(kuò)展提高系統(tǒng)的性能和吞吐量。

          • 高可靠性:高容錯(cuò),即使系統(tǒng)中一臺(tái)或幾臺(tái)故障,系統(tǒng)仍可提供服務(wù)。

          • 高并發(fā)性:各機(jī)器并行獨(dú)立處理和計(jì)算。

          • 廉價(jià)高效:多臺(tái)小型機(jī)而非單臺(tái)高性能機(jī)。


          然而,在分布式系統(tǒng)中,其環(huán)境的復(fù)雜度、網(wǎng)絡(luò)的不確定性會(huì)造成諸如時(shí)鐘不一致、“拜占庭將軍問(wèn)題”(Byzantine failure)等。存在于集中式系統(tǒng)中的機(jī)器宕機(jī)、消息丟失等問(wèn)題也會(huì)在分布式環(huán)境中變得更加復(fù)雜。

          基于分布式系統(tǒng)的這些特征,有兩種問(wèn)題逐漸成為了分布式環(huán)境中需要重點(diǎn)關(guān)注和解決的典型問(wèn)題:

          • 互斥性問(wèn)題。

          • 冪等性問(wèn)題。


          今天我們就針對(duì)這兩個(gè)問(wèn)題來(lái)進(jìn)行分析。


          -     互斥性問(wèn)題    -


          先看兩個(gè)常見(jiàn)的例子:

          例1:某服務(wù)記錄關(guān)鍵數(shù)據(jù)X,當(dāng)前值為100。A請(qǐng)求需要將X增加200;同時(shí),B請(qǐng)求需要將X減100。

          在理想的情況下,A先讀取到X=100,然后X增加200,最后寫(xiě)入X=300。B請(qǐng)求接著從讀取X=300,減少100,最后寫(xiě)入X=200。

          然而在真實(shí)情況下,如果不做任何處理,則可能會(huì)出現(xiàn):A和B同時(shí)讀取到X=100;A寫(xiě)入之前B讀取到X;B比A先寫(xiě)入等情況。

          例2:某服務(wù)提供一組任務(wù),A請(qǐng)求隨機(jī)從任務(wù)組中獲取一個(gè)任務(wù);B請(qǐng)求隨機(jī)從任務(wù)組中獲取一個(gè)任務(wù)。

          在理想的情況下,A從任務(wù)組中挑選一個(gè)任務(wù),任務(wù)組刪除該任務(wù),B從剩下的的任務(wù)中再挑一個(gè),任務(wù)組刪除該任務(wù)。

          同樣的,在真實(shí)情況下,如果不做任何處理,可能會(huì)出現(xiàn)A和B挑中了同一個(gè)任務(wù)的情況。

          以上的兩個(gè)例子,都存在操作互斥性的問(wèn)題。互斥性問(wèn)題用通俗的話來(lái)講,就是對(duì)共享資源的搶占問(wèn)題。如果不同的請(qǐng)求對(duì)同一個(gè)或者同一組資源讀取并修改時(shí),無(wú)法保證按序執(zhí)行,無(wú)法保證一個(gè)操作的原子性,那么就很有可能會(huì)出現(xiàn)預(yù)期外的情況。因此操作的互斥性問(wèn)題,也可以理解為一個(gè)需要保證時(shí)序性、原子性的問(wèn)題。

          在傳統(tǒng)的基于數(shù)據(jù)庫(kù)的架構(gòu)中,對(duì)于數(shù)據(jù)的搶占問(wèn)題往往是通過(guò)數(shù)據(jù)庫(kù)事務(wù)(ACID)來(lái)保證的。在分布式環(huán)境中,出于對(duì)性能以及一致性敏感度的要求,使得分布式鎖成為了一種比較常見(jiàn)而高效的解決方案。

          事實(shí)上,操作互斥性問(wèn)題也并非分布式環(huán)境所獨(dú)有,在傳統(tǒng)的多線程、多進(jìn)程情況下已經(jīng)有了很好的解決方案。因此在研究分布式鎖之前,我們先來(lái)分析下這兩種情況的解決方案,以期能夠?qū)Ψ植际芥i的解決方案提供一些實(shí)現(xiàn)思路。


          -     多線程解決方案及原理    -


          《Thinking in Java》書(shū)中寫(xiě)到:

          基本上所有的并發(fā)模式在解決線程沖突問(wèn)題的時(shí)候,都是采用序列化訪問(wèn)共享資源的方案。

          在多線程環(huán)境中,線程之間因?yàn)楣靡恍┐鎯?chǔ)空間,沖突問(wèn)題時(shí)有發(fā)生。解決沖突問(wèn)題最普遍的方式就是用互斥鎖把該資源或?qū)υ撡Y源的操作保護(hù)起來(lái)。

          Java JDK中提供了兩種互斥鎖Lock和synchronized。不同的線程之間對(duì)同一資源進(jìn)行搶占,該資源通常表現(xiàn)為某個(gè)類的普通成員變量。因此,利用ReentrantLock或者synchronized將共享的變量及其操作鎖住,即可基本解決資源搶占的問(wèn)題。

          下面來(lái)簡(jiǎn)單聊一聊兩者的實(shí)現(xiàn)原理。


          -     原理    -


          ReentrantLock

          ReentrantLock主要利用CAS+CLH隊(duì)列來(lái)實(shí)現(xiàn)。它支持公平鎖和非公平鎖,兩者的實(shí)現(xiàn)類似。

          • CAS:Compare and Swap,比較并交換。CAS有3個(gè)操作數(shù):內(nèi)存值V、預(yù)期值A(chǔ)、要修改的新值B。當(dāng)且僅當(dāng)預(yù)期值A(chǔ)和內(nèi)存值V相同時(shí),將內(nèi)存值V修改為B,否則什么都不做。該操作是一個(gè)原子操作,被廣泛的應(yīng)用在Java的底層實(shí)現(xiàn)中。在Java中,CAS主要是由sun.misc.Unsafe這個(gè)類通過(guò)JNI調(diào)用CPU底層指令實(shí)現(xiàn)。

          • CLH隊(duì)列:帶頭結(jié)點(diǎn)的雙向非循環(huán)鏈表(如下圖所示):

          ReentrantLock的基本實(shí)現(xiàn)可以概括為:先通過(guò)CAS嘗試獲取鎖。如果此時(shí)已經(jīng)有線程占據(jù)了鎖,那就加入CLH隊(duì)列并且被掛起。當(dāng)鎖被釋放之后,排在CLH隊(duì)列隊(duì)首的線程會(huì)被喚醒,然后CAS再次嘗試獲取鎖。在這個(gè)時(shí)候,如果:

          • 非公平鎖:如果同時(shí)還有另一個(gè)線程進(jìn)來(lái)嘗試獲取,那么有可能會(huì)讓這個(gè)線程搶先獲取;

          • 公平鎖:如果同時(shí)還有另一個(gè)線程進(jìn)來(lái)嘗試獲取,當(dāng)它發(fā)現(xiàn)自己不是在隊(duì)首的話,就會(huì)排到隊(duì)尾,由隊(duì)首的線程獲取到鎖。


          下面分析下兩個(gè)片段:

          final boolean nonfairTryAcquire(int acquires) {
             final Thread current = Thread.currentThread();
             int c = getState();
             if (c == 0) {
                 if (compareAndSetState(0, acquires)) {
                     setExclusiveOwnerThread(current);
                     return true;
                 }
             }
             else if (current == getExclusiveOwnerThread()) {
                 int nextc = c + acquires;
                 if (nextc < 0// overflow
                     throw new Error("Maximum lock count exceeded");
                 setState(nextc);
                 return true;
             }
             return false;
          }


          在嘗試獲取鎖的時(shí)候,會(huì)先調(diào)用上面的方法。如果狀態(tài)為0,則表明此時(shí)無(wú)人占有鎖。此時(shí)嘗試進(jìn)行set,一旦成功,則成功占有鎖。如果狀態(tài)不為0,再判斷是否是當(dāng)前線程獲取到鎖。如果是的話,將狀態(tài)+1,因?yàn)榇藭r(shí)就是當(dāng)前線程,所以不用CAS。這也就是可重入鎖的實(shí)現(xiàn)原理。

          final boolean acquireQueued(final Node node, int arg) {
             boolean failed = true;
             try {
                 boolean interrupted = false;
                 for (;;) {
                     final Node p = node.predecessor();
                     if (p == head && tryAcquire(arg)) {
                         setHead(node);
                         p.next = null// help GC
                         failed = false;
                         return interrupted;
                     }
                     if (shouldParkAfterFailedAcquire(p, node) &&
                         parkAndCheckInterrupt())
                         interrupted = true;
                 }
             } finally {
                 if (failed)
                     cancelAcquire(node);
             }
          }
          private final boolean parkAndCheckInterrupt() {
             LockSupport.park(this);
             return Thread.interrupted();
          }


          該方法是在嘗試獲取鎖失敗加入CHL隊(duì)尾之后,如果發(fā)現(xiàn)前序節(jié)點(diǎn)是head,則CAS再嘗試獲取一次。否則,則會(huì)根據(jù)前序節(jié)點(diǎn)的狀態(tài)判斷是否需要阻塞。如果需要阻塞,則調(diào)用LockSupport的park方法阻塞該線程。


          -     synchronized    -


          在Java語(yǔ)言中存在兩種內(nèi)建的synchronized語(yǔ)法:synchronized語(yǔ)句、synchronized方法。

          • synchronized語(yǔ)句:當(dāng)源代碼被編譯成字節(jié)碼的時(shí)候,會(huì)在同步塊的入口位置和退出位置分別插入monitorenter和monitorexit字節(jié)碼指令;

          • synchronized方法:在Class文件的方法表中將該方法的access_flags字段中的synchronized標(biāo)志位置1。這個(gè)在specification中沒(méi)有明確說(shuō)明。


          在Java虛擬機(jī)的specification中,有關(guān)于monitorenter和monitorexit字節(jié)碼指令的詳細(xì)描述:

          http://docs.oracle.com/Javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.monitorenter。

          monitorenter

          The objectref must be of type reference.

          Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:

          • If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.

          • If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.

          • If another thread already owns the monitor associated with objectref, the thread blocks until the monitor’s entry count is zero, then tries again to gain ownership.

          每個(gè)對(duì)象都有一個(gè)鎖,也就是監(jiān)視器(monitor)。當(dāng)monitor被占有時(shí)就表示它被鎖定。線程執(zhí)行monitorenter指令時(shí)嘗試獲取對(duì)象所對(duì)應(yīng)的monitor的所有權(quán),過(guò)程如下:

          • 如果monitor的進(jìn)入數(shù)為0,則該線程進(jìn)入monitor,然后將進(jìn)入數(shù)設(shè)置為1,該線程即為monitor的所有者;

          • 如果線程已經(jīng)擁有了該monitor,只是重新進(jìn)入,則進(jìn)入monitor的進(jìn)入數(shù)加1;

          • 如果其他線程已經(jīng)占用了monitor,則該線程進(jìn)入阻塞狀態(tài),直到monitor的進(jìn)入數(shù)為0,再重新嘗試獲取monitor的所有權(quán)。


          monitorexit

          The objectref must be of type reference.

          The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.

          The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.

          執(zhí)行monitorexit的線程必須是相應(yīng)的monitor的所有者。 
          指令執(zhí)行時(shí),monitor的進(jìn)入數(shù)減1,如果減1后進(jìn)入數(shù)為0,那線程退出monitor,不再是這個(gè)monitor的所有者。其他被這個(gè)monitor阻塞的線程可以嘗試去獲取這個(gè)monitor的所有權(quán)。

          在JDK1.6及其之前的版本中monitorenter和monitorexit字節(jié)碼依賴于底層的操作系統(tǒng)的Mutex Lock來(lái)實(shí)現(xiàn)的,但是由于使用Mutex Lock需要將當(dāng)前線程掛起并從用戶態(tài)切換到內(nèi)核態(tài)來(lái)執(zhí)行,這種切換的代價(jià)是非常昂貴的。然而在現(xiàn)實(shí)中的大部分情況下,同步方法是運(yùn)行在單線程環(huán)境(無(wú)鎖競(jìng)爭(zhēng)環(huán)境)。如果每次都調(diào)用Mutex Lock將嚴(yán)重的影響程序的性能。因此在JDK 1.6之后的版本中對(duì)鎖的實(shí)現(xiàn)做了大量的優(yōu)化,這些優(yōu)化在很大程度上減少或避免了Mutex Lock的使用。


          -     多進(jìn)程的解決方案     -


          在多道程序系統(tǒng)中存在許多進(jìn)程,它們共享各種資源,然而有很多資源一次只能供一個(gè)進(jìn)程使用,這便是臨界資源。多進(jìn)程中的臨界資源大致上可以分為兩類,一類是物理上的真實(shí)資源,如打印機(jī);一類是硬盤(pán)或內(nèi)存中的共享數(shù)據(jù),如共享內(nèi)存等。而進(jìn)程內(nèi)互斥訪問(wèn)臨界資源的代碼被稱為臨界區(qū)。

          針對(duì)臨界資源的互斥訪問(wèn),JVM層面的鎖就已經(jīng)失去效力了。在多進(jìn)程的情況下,主要還是利用操作系統(tǒng)層面的進(jìn)程間通信原理來(lái)解決臨界資源的搶占問(wèn)題。比較常見(jiàn)的一種方法便是使用信號(hào)量(Semaphores)。

          信號(hào)量在POSIX標(biāo)準(zhǔn)下有兩種,分別為有名信號(hào)量和無(wú)名信號(hào)量。無(wú)名信號(hào)量通常保存在共享內(nèi)存中,而有名信號(hào)量是與一個(gè)特定的文件名稱相關(guān)聯(lián)。信號(hào)量是一個(gè)整數(shù)變量,有計(jì)數(shù)信號(hào)量和二值信號(hào)量?jī)煞N。對(duì)信號(hào)量的操作,主要是P操作(wait)和V操作(signal)。

          • P操作:先檢查信號(hào)量的大小,若值大于零,則將信號(hào)量減1,同時(shí)進(jìn)程獲得共享資源的訪問(wèn)權(quán)限,繼續(xù)執(zhí)行;若小于或者等于零,則該進(jìn)程被阻塞后,進(jìn)入等待隊(duì)列。

          • V操作:該操作將信號(hào)量的值加1,如果有進(jìn)程阻塞著等待該信號(hào)量,那么其中一個(gè)進(jìn)程將被喚醒。


          舉個(gè)例子,設(shè)信號(hào)量為1,當(dāng)一個(gè)進(jìn)程A在進(jìn)入臨界區(qū)之前,先進(jìn)行P操作。發(fā)現(xiàn)值大于零,那么就將信號(hào)量減為0,進(jìn)入臨界區(qū)執(zhí)行。此時(shí),若另一個(gè)進(jìn)程B也要進(jìn)去臨界區(qū),進(jìn)行P操作,發(fā)現(xiàn)信號(hào)量等于0,則會(huì)被阻塞。當(dāng)進(jìn)程A退出臨界區(qū)時(shí),會(huì)進(jìn)行V操作,將信號(hào)量的值加1,并喚醒阻塞的進(jìn)程B。此時(shí)B就可以進(jìn)入臨界區(qū)了。

          這種方式,其實(shí)和多線程環(huán)境下的加解鎖非常類似。因此用信號(hào)量處理臨界資源搶占,也可以簡(jiǎn)單地理解為對(duì)臨界區(qū)進(jìn)行加鎖。

          通過(guò)上面的一些了解,我們可以概括出解決互斥性問(wèn)題,即資源搶占的基本方式為:

          對(duì)共享資源的操作前后(進(jìn)入退出臨界區(qū))加解鎖,保證不同線程或進(jìn)程可以互斥有序的操作資源。

          加解鎖方式,有顯式的加解鎖,如ReentrantLock或信號(hào)量;也有隱式的加解鎖,如synchronized。那么在分布式環(huán)境中,為了保證不同JVM不同主機(jī)間不會(huì)出現(xiàn)資源搶占,那么同樣只要對(duì)臨界區(qū)加解鎖就可以了。

          然而在多線程和多進(jìn)程中,鎖已經(jīng)有比較完善的實(shí)現(xiàn),直接使用即可。但是在分布式環(huán)境下,就需要我們自己來(lái)實(shí)現(xiàn)分布式鎖。


          -     分布式鎖    -

          首先,我們來(lái)看看分布式鎖的基本條件。

          分布式鎖條件

          基本條件

          再回顧下多線程和多進(jìn)程環(huán)境下的鎖,可以發(fā)現(xiàn)鎖的實(shí)現(xiàn)有很多共通之處,它們都需要滿足一些最基本的條件:

          1. 需要有存儲(chǔ)鎖的空間,并且鎖的空間是可以訪問(wèn)到的。

          2. 鎖需要被唯一標(biāo)識(shí)。

          3. 鎖要有至少兩種狀態(tài)。


          仔細(xì)分析這三個(gè)條件:

          存儲(chǔ)空間


          鎖是一個(gè)抽象的概念,鎖的實(shí)現(xiàn),需要依存于一個(gè)可以存儲(chǔ)鎖的空間。在多線程中是內(nèi)存,在多進(jìn)程中是內(nèi)存或者磁盤(pán)。更重要的是,這個(gè)空間是可以被訪問(wèn)到的。多線程中,不同的線程都可以訪問(wèn)到堆中的成員變量;在多進(jìn)程中,不同的進(jìn)程可以訪問(wèn)到共享內(nèi)存中的數(shù)據(jù)或者存儲(chǔ)在磁盤(pán)中的文件。但是在分布式環(huán)境中,不同的主機(jī)很難訪問(wèn)對(duì)方的內(nèi)存或磁盤(pán)。這就需要一個(gè)都能訪問(wèn)到的外部空間來(lái)作為存儲(chǔ)空間。

          最普遍的外部存儲(chǔ)空間就是數(shù)據(jù)庫(kù)了,事實(shí)上也確實(shí)有基于數(shù)據(jù)庫(kù)做分布式鎖(行鎖、version樂(lè)觀鎖),如quartz集群架構(gòu)中就有所使用。除此以外,還有各式緩存如Redis、Tair、Memcached、MongoDB,當(dāng)然還有專門(mén)的分布式協(xié)調(diào)服務(wù)Zookeeper,甚至是另一臺(tái)主機(jī)。只要可以存儲(chǔ)數(shù)據(jù)、鎖在其中可以被多主機(jī)訪問(wèn)到,那就可以作為分布式鎖的存儲(chǔ)空間。

          唯一標(biāo)識(shí)


          不同的共享資源,必然需要用不同的鎖進(jìn)行保護(hù),因此相應(yīng)的鎖必須有唯一的標(biāo)識(shí)。在多線程環(huán)境中,鎖可以是一個(gè)對(duì)象,那么對(duì)這個(gè)對(duì)象的引用便是這個(gè)唯一標(biāo)識(shí)。多進(jìn)程環(huán)境中,信號(hào)量在共享內(nèi)存中也是由引用來(lái)作為唯一的標(biāo)識(shí)。但是如果不在內(nèi)存中,失去了對(duì)鎖的引用,如何唯一標(biāo)識(shí)它呢?上文提到的有名信號(hào)量,便是用硬盤(pán)中的文件名作為唯一標(biāo)識(shí)。因此,在分布式環(huán)境中,只要給這個(gè)鎖設(shè)定一個(gè)名稱,并且保證這個(gè)名稱是全局唯一的,那么就可以作為唯一標(biāo)識(shí)。

          至少兩種狀態(tài)


          為了給臨界區(qū)加鎖和解鎖,需要存儲(chǔ)兩種不同的狀態(tài)。如ReentrantLock中的status,0表示沒(méi)有線程競(jìng)爭(zhēng),大于0表示有線程競(jìng)爭(zhēng);信號(hào)量大于0表示可以進(jìn)入臨界區(qū),小于等于0則表示需要被阻塞。因此只要在分布式環(huán)境中,鎖的狀態(tài)有兩種或以上:如有鎖、沒(méi)鎖;存在、不存在等,均可以實(shí)現(xiàn)。

          有了這三個(gè)條件,基本就可以實(shí)現(xiàn)一個(gè)簡(jiǎn)單的分布式鎖了。下面以數(shù)據(jù)庫(kù)為例,實(shí)現(xiàn)一個(gè)簡(jiǎn)單的分布式鎖: 
          數(shù)據(jù)庫(kù)表,字段為鎖的ID(唯一標(biāo)識(shí)),鎖的狀態(tài)(0表示沒(méi)有被鎖,1表示被鎖)。

          偽代碼為:

          lock = mysql.get(id);
          while(lock.status == 1) {
             sleep(100);
          }
          mysql.update(lock.status = 1);
          doSomething();
          mysql.update(lock.status = 0);


          問(wèn)題

          以上的方式即可以實(shí)現(xiàn)一個(gè)粗糙的分布式鎖,但是這樣的實(shí)現(xiàn),有沒(méi)有什么問(wèn)題呢?

          問(wèn)題1:鎖狀態(tài)判斷原子性無(wú)法保證 

          從讀取鎖的狀態(tài),到判斷該狀態(tài)是否為被鎖,需要經(jīng)歷兩步操作。如果不能保證這兩步的原子性,就可能導(dǎo)致不止一個(gè)請(qǐng)求獲取到了鎖,這顯然是不行的。因此,我們需要保證鎖狀態(tài)判斷的原子性。

          問(wèn)題2:網(wǎng)絡(luò)斷開(kāi)或主機(jī)宕機(jī),鎖狀態(tài)無(wú)法清除 

          假設(shè)在主機(jī)已經(jīng)獲取到鎖的情況下,突然出現(xiàn)了網(wǎng)絡(luò)斷開(kāi)或者主機(jī)宕機(jī),如果不做任何處理該鎖將仍然處于被鎖定的狀態(tài)。那么之后所有的請(qǐng)求都無(wú)法再成功搶占到這個(gè)鎖。因此,我們需要在持有鎖的主機(jī)宕機(jī)或者網(wǎng)絡(luò)斷開(kāi)的時(shí)候,及時(shí)的釋放掉這把鎖。

          問(wèn)題3:無(wú)法保證釋放的是自己上鎖的那把鎖 

          在解決了問(wèn)題2的情況下再設(shè)想一下,假設(shè)持有鎖的主機(jī)A在臨界區(qū)遇到網(wǎng)絡(luò)抖動(dòng)導(dǎo)致網(wǎng)絡(luò)斷開(kāi),分布式鎖及時(shí)的釋放掉了這把鎖。之后,另一個(gè)主機(jī)B占有了這把鎖,但是此時(shí)主機(jī)A網(wǎng)絡(luò)恢復(fù),退出臨界區(qū)時(shí)解鎖。由于都是同一把鎖,所以A就會(huì)將B的鎖解開(kāi)。此時(shí)如果有第三個(gè)主機(jī)嘗試搶占這把鎖,也將會(huì)成功獲得。因此,我們需要在解鎖時(shí),確定自己解的這個(gè)鎖正是自己鎖上的。

          進(jìn)階條件

          如果分布式鎖的實(shí)現(xiàn),還能再解決上面的三個(gè)問(wèn)題,那么就可以算是一個(gè)相對(duì)完整的分布式鎖了。然而,在實(shí)際的系統(tǒng)環(huán)境中,還會(huì)對(duì)分布式鎖有更高級(jí)的要求。

          1. 可重入:線程中的可重入,指的是外層函數(shù)獲得鎖之后,內(nèi)層也可以獲得鎖,ReentrantLock和synchronized都是可重入鎖;衍生到分布式環(huán)境中,一般仍然指的是線程的可重入,在絕大多數(shù)分布式環(huán)境中,都要求分布式鎖是可重入的。

          2. 驚群效應(yīng)(Herd Effect):在分布式鎖中,驚群效應(yīng)指的是,在有多個(gè)請(qǐng)求等待獲取鎖的時(shí)候,一旦占有鎖的線程釋放之后,如果所有等待的方都同時(shí)被喚醒,嘗試搶占鎖。但是這樣的情況會(huì)造成比較大的開(kāi)銷,那么在實(shí)現(xiàn)分布式鎖的時(shí)候,應(yīng)該盡量避免驚群效應(yīng)的產(chǎn)生。

          3. 公平鎖和非公平鎖:不同的需求,可能需要不同的分布式鎖。非公平鎖普遍比公平鎖開(kāi)銷小。但是業(yè)務(wù)需求如果必須要鎖的競(jìng)爭(zhēng)者按順序獲得鎖,那么就需要實(shí)現(xiàn)公平鎖。

          4. 阻塞鎖和自旋鎖:針對(duì)不同的使用場(chǎng)景,阻塞鎖和自旋鎖的效率也會(huì)有所不同。阻塞鎖會(huì)有上下文切換,如果并發(fā)量比較高且臨界區(qū)的操作耗時(shí)比較短,那么造成的性能開(kāi)銷就比較大了。但是如果臨界區(qū)操作耗時(shí)比較長(zhǎng),一直保持自旋,也會(huì)對(duì)CPU造成更大的負(fù)荷。

          保留以上所有問(wèn)題和條件,我們接下來(lái)看一些比較典型的實(shí)現(xiàn)方案。


          -     典型實(shí)現(xiàn)    -


          ZooKeeper的實(shí)現(xiàn)

          ZooKeeper(以下簡(jiǎn)稱“ZK”)中有一種節(jié)點(diǎn)叫做順序節(jié)點(diǎn),假如我們?cè)?lock/目錄下創(chuàng)建3個(gè)節(jié)點(diǎn),ZK集群會(huì)按照發(fā)起創(chuàng)建的順序來(lái)創(chuàng)建節(jié)點(diǎn),節(jié)點(diǎn)分別為/lock/0000000001、/lock/0000000002、/lock/0000000003。

          ZK中還有一種名為臨時(shí)節(jié)點(diǎn)的節(jié)點(diǎn),臨時(shí)節(jié)點(diǎn)由某個(gè)客戶端創(chuàng)建,當(dāng)客戶端與ZK集群斷開(kāi)連接,則該節(jié)點(diǎn)自動(dòng)被刪除。EPHEMERAL_SEQUENTIAL為臨時(shí)順序節(jié)點(diǎn)。

          根據(jù)ZK中節(jié)點(diǎn)是否存在,可以作為分布式鎖的鎖狀態(tài),以此來(lái)實(shí)現(xiàn)一個(gè)分布式鎖,下面是分布式鎖的基本邏輯:

          • 客戶端調(diào)用create()方法創(chuàng)建名為“/dlm-locks/lockname/lock-”的臨時(shí)順序節(jié)點(diǎn)。

          • 客戶端調(diào)用getChildren(“l(fā)ockname”)方法來(lái)獲取所有已經(jīng)創(chuàng)建的子節(jié)點(diǎn)。

          • 客戶端獲取到所有子節(jié)點(diǎn)path之后,如果發(fā)現(xiàn)自己在步驟1中創(chuàng)建的節(jié)點(diǎn)是所有節(jié)點(diǎn)中序號(hào)最小的,那么就認(rèn)為這個(gè)客戶端獲得了鎖。

          • 如果創(chuàng)建的節(jié)點(diǎn)不是所有節(jié)點(diǎn)中需要最小的,那么則監(jiān)視比自己創(chuàng)建節(jié)點(diǎn)的序列號(hào)小的最大的節(jié)點(diǎn),進(jìn)入等待。直到下次監(jiān)視的子節(jié)點(diǎn)變更的時(shí)候,再進(jìn)行子節(jié)點(diǎn)的獲取,判斷是否獲取鎖。


          釋放鎖的過(guò)程相對(duì)比較簡(jiǎn)單,就是刪除自己創(chuàng)建的那個(gè)子節(jié)點(diǎn)即可,不過(guò)也仍需要考慮刪除節(jié)點(diǎn)失敗等異常情況。

          開(kāi)源的基于ZK的Menagerie的源碼就是一個(gè)典型的例子:

          https://github.com/sfines/menagerie 。

          Menagerie中的lock首先實(shí)現(xiàn)了可重入鎖,利用ThreadLocal存儲(chǔ)進(jìn)入的次數(shù),每次加鎖次數(shù)加1,每次解鎖次數(shù)減1。如果判斷出是當(dāng)前線程持有鎖,就不用走獲取鎖的流程。

          通過(guò)tryAcquireDistributed方法嘗試獲取鎖,循環(huán)判斷前序節(jié)點(diǎn)是否存在,如果存在則監(jiān)視該節(jié)點(diǎn)并且返回獲取失敗。如果前序節(jié)點(diǎn)不存在,則再判斷更前一個(gè)節(jié)點(diǎn)。如果判斷出自己是第一個(gè)節(jié)點(diǎn),則返回獲取成功。

          為了在別的線程占有鎖的時(shí)候阻塞,代碼中使用JUC的condition來(lái)完成。如果獲取嘗試鎖失敗,則進(jìn)入等待且放棄localLock,等待前序節(jié)點(diǎn)喚醒。而localLock是一個(gè)本地的公平鎖,使得condition可以公平的進(jìn)行喚醒,配合循環(huán)判斷前序節(jié)點(diǎn),實(shí)現(xiàn)了一個(gè)公平鎖。

          這種實(shí)現(xiàn)方式非常類似于ReentrantLock的CHL隊(duì)列,而且zk的臨時(shí)節(jié)點(diǎn)可以直接避免網(wǎng)絡(luò)斷開(kāi)或主機(jī)宕機(jī),鎖狀態(tài)無(wú)法清除的問(wèn)題,順序節(jié)點(diǎn)可以避免驚群效應(yīng)。這些特性都使得利用ZK實(shí)現(xiàn)分布式鎖成為了最普遍的方案之一。

          Redis的實(shí)現(xiàn)

          Redis的分布式緩存特性使其成為了分布式鎖的一種基礎(chǔ)實(shí)現(xiàn)。通過(guò)Redis中是否存在某個(gè)鎖ID,則可以判斷是否上鎖。為了保證判斷鎖是否存在的原子性,保證只有一個(gè)線程獲取同一把鎖,Redis有SETNX(即SET if Not 
          eXists)和GETSET(先寫(xiě)新值,返回舊值,原子性操作,可以用于分辨是不是首次操作)操作。

          為了防止主機(jī)宕機(jī)或網(wǎng)絡(luò)斷開(kāi)之后的死鎖,Redis沒(méi)有ZK那種天然的實(shí)現(xiàn)方式,只能依賴設(shè)置超時(shí)時(shí)間來(lái)規(guī)避。

          以下是一種比較普遍但不太完善的Redis分布式鎖的實(shí)現(xiàn)步驟(與下圖一一對(duì)應(yīng)):

          • 線程A發(fā)送SETNX lock.orderid嘗試獲得鎖,如果鎖不存在,則set并獲得鎖。

          • 如果鎖存在,則再判斷鎖的值(時(shí)間戳)是否大于當(dāng)前時(shí)間,如果沒(méi)有超時(shí),則等待一下再重試。

          • 如果已經(jīng)超時(shí)了,在用GETSET lock.{orderid}來(lái)嘗試獲取鎖,如果這時(shí)候拿到的時(shí)間戳仍舊超時(shí),則說(shuō)明已經(jīng)獲得鎖了。

          • 如果在此之前,另一個(gè)線程C快一步執(zhí)行了上面的操作,那么A拿到的時(shí)間戳是個(gè)未超時(shí)的值,這時(shí)A沒(méi)有如期獲得鎖,需要再次等待或重試。


          該實(shí)現(xiàn)還有一個(gè)需要考慮的問(wèn)題是全局時(shí)鐘問(wèn)題,由于生產(chǎn)環(huán)境主機(jī)時(shí)鐘不能保證完全同步,對(duì)時(shí)間戳的判斷也可能會(huì)產(chǎn)生誤差。

          以上是Redis的一種常見(jiàn)的實(shí)現(xiàn)方式,除此以外還可以用SETNX+EXPIRE來(lái)實(shí)現(xiàn)。Redisson是一個(gè)官方推薦的Redis客戶端并且實(shí)現(xiàn)了很多分布式的功能。它的分布式鎖就提供了一種更完善的解決方案,源碼:

          https://github.com/mrniko/redisson。

          Tair的實(shí)現(xiàn)

          Tair和Redis的實(shí)現(xiàn)類似,Tair客戶端封裝了一個(gè)expireLock的方法:通過(guò)鎖狀態(tài)和過(guò)期時(shí)間戳來(lái)共同判斷鎖是否存在,只有鎖已經(jīng)存在且沒(méi)有過(guò)期的狀態(tài)才判定為有鎖狀態(tài)。在有鎖狀態(tài)下,不能加鎖,能通過(guò)大于或等于過(guò)期時(shí)間的時(shí)間戳進(jìn)行解鎖。

          采用這樣的方式,可以不用在Value中存儲(chǔ)時(shí)間戳,并且保證了判斷是否有鎖的原子性。更值得注意的是,由于超時(shí)時(shí)間是由Tair判斷,所以避免了不同主機(jī)時(shí)鐘不一致的情況。

          以上的幾種分布式鎖實(shí)現(xiàn)方式,都是比較常見(jiàn)且有些已經(jīng)在生產(chǎn)環(huán)境中應(yīng)用。隨著應(yīng)用環(huán)境越來(lái)越復(fù)雜,這些實(shí)現(xiàn)可能仍然會(huì)遇到一些挑戰(zhàn)。

          強(qiáng)依賴于外部組件:分布式鎖的實(shí)現(xiàn)都需要依賴于外部數(shù)據(jù)存儲(chǔ)如ZK、Redis等,因此一旦這些外部組件出現(xiàn)故障,那么分布式鎖就不可用了。


          無(wú)法完全滿足需求:不同分布式鎖的實(shí)現(xiàn),都有相應(yīng)的特點(diǎn),對(duì)于一些需求并不能很好的滿足,如實(shí)現(xiàn)公平鎖、給等待鎖加超時(shí)時(shí)間等。

          基于以上問(wèn)題,結(jié)合多種實(shí)現(xiàn)方式,我們開(kāi)發(fā)了Cerberus(得名自希臘神話里守衛(wèi)地獄的猛犬),致力于提供靈活可靠的分布式鎖。

          Cerberus分布式鎖

          Cerberus有以下幾個(gè)特點(diǎn)。

          特點(diǎn)一:一套接口多種引擎

          Cerberus分布式鎖使用了多種引擎實(shí)現(xiàn)方式(Tair、ZK、未來(lái)支持Redis),支持使用方自主選擇所需的一種或多種引擎。這樣可以結(jié)合引擎特點(diǎn),選擇符合實(shí)際業(yè)務(wù)需求和系統(tǒng)架構(gòu)的方式。

          Cerberus分布式鎖將不同引擎的接口抽象為一套,屏蔽了不同引擎的實(shí)現(xiàn)細(xì)節(jié)。使得使用方可以專注于業(yè)務(wù)邏輯,也可以任意選擇并切換引擎而不必更改任何的業(yè)務(wù)代碼。

          如果使用方選擇了一種以上的引擎,那么以配置順序來(lái)區(qū)分主副引擎。以下是使用主引擎的推薦:


          特點(diǎn)二:使用靈活、學(xué)習(xí)成本低


          下面是Cerberus的lock方法,這些方法和JUC的ReentrantLock的方式保持一致,使用非常靈活且不需要額外的學(xué)習(xí)時(shí)間。

          void lock(); 

          獲取鎖,如果鎖被占用,將禁用當(dāng)前線程,并且在獲得鎖之前,該線程將一直處于阻塞狀態(tài)。

          boolean tryLock(); 

          僅在調(diào)用時(shí)鎖為空閑狀態(tài)才獲取該鎖。 
          如果鎖可用,則獲取鎖,并立即返回值true。如果鎖不可用,則此方法將立即返回值false。

          boolean tryLock(long time, TimeUnit unit) throws InterruptedException; 

          如果鎖在給定的等待時(shí)間內(nèi)空閑,并且當(dāng)前線程未被中斷,則獲取鎖。 
          如果在給定時(shí)間內(nèi)鎖可用,則獲取鎖,并立即返回值true。如果在給定時(shí)間內(nèi)鎖一直不可用,則此方法將立即返回值false。

          • void lockInterruptibly() throws InterruptedException; 
            獲取鎖,如果鎖被占用,則一直等待直到線程被中斷或者獲取到鎖。

          • void unlock(); 
            釋放當(dāng)前持有的鎖。

          特點(diǎn)三:支持一鍵降級(jí)

          Cerberus提供了實(shí)時(shí)切換引擎的接口:

          • String switchEngine() 
            轉(zhuǎn)換分布式鎖引擎,按配置的引擎的順序循環(huán)轉(zhuǎn)換。 
            返回值:返回當(dāng)前的engine名字,如:”zk”。

          • String switchEngine(String engineName) 
            轉(zhuǎn)換分布式鎖引擎,切換為指定的引擎。 
            參數(shù):engineName - 引擎的名字,同配置bean的名字,”zk”/”tair”。 返回值:返回當(dāng)前的engine名字,如:”zk”。

          當(dāng)使用方選擇了兩種引擎,平時(shí)分布式鎖會(huì)工作在主引擎上。一旦所依賴的主引擎出現(xiàn)故障,那么使用方可以通過(guò)自動(dòng)或者手動(dòng)方式調(diào)用該切換引擎接口,平滑的將分布式鎖切換到另一個(gè)引擎上以將風(fēng)險(xiǎn)降到最低。自動(dòng)切換方式可以利用Hystrix實(shí)現(xiàn)。手動(dòng)切換推薦的一個(gè)方案則是使用美團(tuán)點(diǎn)評(píng)基于Zookeeper的基礎(chǔ)組件MCC,通過(guò)監(jiān)聽(tīng)MCC配置項(xiàng)更改,來(lái)達(dá)到手動(dòng)將分布式系統(tǒng)所有主機(jī)同步切換引擎的目的。需要注意的是,切換引擎目前并不會(huì)遷移原引擎已有的鎖。

          這樣做的目的是出于必要性、系統(tǒng)復(fù)雜度和可靠性的綜合考慮。在實(shí)際情況下,引擎故障到切換引擎,尤其是手動(dòng)切換引擎的時(shí)間,要遠(yuǎn)大于分布式鎖的存活時(shí)間。作為較輕量級(jí)的Cerberus來(lái)說(shuō),遷移鎖會(huì)帶來(lái)不必要的開(kāi)銷以及較高的系統(tǒng)復(fù)雜度。鑒于此,如果想要保證在引擎故障后的絕對(duì)可靠,那么則需要結(jié)合其他方案來(lái)進(jìn)行處理。

          除此以外,Cerberus還提供了內(nèi)置公用集群,免去搭建和配置集群的煩惱。Cerberus也有一套完善的應(yīng)用授權(quán)機(jī)制,以此防止業(yè)務(wù)方未經(jīng)評(píng)估使用,對(duì)集群造成影響。

          目前,Cerberus分布式鎖已經(jīng)持續(xù)迭代了8個(gè)版本,先后在美團(tuán)點(diǎn)評(píng)多個(gè)項(xiàng)目中穩(wěn)定運(yùn)行。


          -     冪等性問(wèn)題    -


          所謂冪等,簡(jiǎn)單地說(shuō),就是對(duì)接口的多次調(diào)用所產(chǎn)生的結(jié)果和調(diào)用一次是一致的。擴(kuò)展一下,這里的接口,可以理解為對(duì)外發(fā)布的HTTP接口或者Thrift接口,也可以是接收消息的內(nèi)部接口,甚至是一個(gè)內(nèi)部方法或操作。

          那么我們?yōu)槭裁葱枰涌诰哂袃绲刃阅兀吭O(shè)想一下以下情形:

          1. 在App中下訂單的時(shí)候,點(diǎn)擊確認(rèn)之后,沒(méi)反應(yīng),就又點(diǎn)擊了幾次。在這種情況下,如果無(wú)法保證該接口的冪等性,那么將會(huì)出現(xiàn)重復(fù)下單問(wèn)題。

          2. 在接收消息的時(shí)候,消息推送重復(fù)。如果處理消息的接口無(wú)法保證冪等,那么重復(fù)消費(fèi)消息產(chǎn)生的影響可能會(huì)非常大。


          在分布式環(huán)境中,網(wǎng)絡(luò)環(huán)境更加復(fù)雜,因前端操作抖動(dòng)、網(wǎng)絡(luò)故障、消息重復(fù)、響應(yīng)速度慢等原因,對(duì)接口的重復(fù)調(diào)用概率會(huì)比集中式環(huán)境下更大,尤其是重復(fù)消息在分布式環(huán)境中很難避免。Tyler Treat也在《You Cannot Have Exactly-Once Delivery》一文中提到:

          Within the context of a distributed system, you cannot have exactly-once message delivery.

          分布式環(huán)境中,有些接口是天然保證冪等性的,如查詢操作。有些對(duì)數(shù)據(jù)的修改是一個(gè)常量,并且無(wú)其他記錄和操作,那也可以說(shuō)是具有冪等性的。其他情況下,所有涉及對(duì)數(shù)據(jù)的修改、狀態(tài)的變更就都有必要防止重復(fù)性操作的發(fā)生。通過(guò)間接的實(shí)現(xiàn)接口的冪等性來(lái)防止重復(fù)操作所帶來(lái)的影響,成為了一種有效的解決方案。

          GTIS

          GTIS就是這樣的一個(gè)解決方案。它是一個(gè)輕量的重復(fù)操作關(guān)卡系統(tǒng),它能夠確保在分布式環(huán)境中操作的唯一性。我們可以用它來(lái)間接保證每個(gè)操作的冪等性。它具有如下特點(diǎn):

          • 高效:低延時(shí),單個(gè)方法平均響應(yīng)時(shí)間在2ms內(nèi),幾乎不會(huì)對(duì)業(yè)務(wù)造成影響;

          • 可靠:提供降級(jí)策略,以應(yīng)對(duì)外部存儲(chǔ)引擎故障所造成的影響;提供應(yīng)用鑒權(quán),提供集群配置自定義,降低不同業(yè)務(wù)之間的干擾;

          • 簡(jiǎn)單:接入簡(jiǎn)捷方便,學(xué)習(xí)成本低。只需簡(jiǎn)單的配置,在代碼中進(jìn)行兩個(gè)方法的調(diào)用即可完成所有的接入工作;

          • 靈活:提供多種接口參數(shù)、使用策略,以滿足不同的業(yè)務(wù)需求。

          實(shí)現(xiàn)原理

          基本原理

          GTIS的實(shí)現(xiàn)思路是將每一個(gè)不同的業(yè)務(wù)操作賦予其唯一性。這個(gè)唯一性是通過(guò)對(duì)不同操作所對(duì)應(yīng)的唯一的內(nèi)容特性生成一個(gè)唯一的全局ID來(lái)實(shí)現(xiàn)的。基本原則為:相同的操作生成相同的全局ID;不同的操作生成不同的全局ID。

          生成的全局ID需要存儲(chǔ)在外部存儲(chǔ)引擎中,數(shù)據(jù)庫(kù)、Redis亦或是Tair等均可實(shí)現(xiàn)。考慮到Tair天生分布式和持久化的優(yōu)勢(shì),目前的GTIS存儲(chǔ)在Tair中。其相應(yīng)的key和value如下:

          • key:將對(duì)于不同的業(yè)務(wù),采用APP_KEY+業(yè)務(wù)操作內(nèi)容特性生成一個(gè)唯一標(biāo)識(shí)trans_contents。然后對(duì)唯一標(biāo)識(shí)進(jìn)行加密生成全局ID作為Key。

          • value:current_timestamp + trans_contents,current_timestamp用于標(biāo)識(shí)當(dāng)前的操作線程。


          判斷是否重復(fù),主要利用Tair的SETNX方法,如果原來(lái)沒(méi)有值則set且返回成功,如果已經(jīng)有值則返回失敗。

          內(nèi)部流程

          GTIS的內(nèi)部實(shí)現(xiàn)流程為:

          1. 業(yè)務(wù)方在業(yè)務(wù)操作之前,生成一個(gè)能夠唯一標(biāo)識(shí)該操作的transContents,傳入GTIS;

          2. GTIS根據(jù)傳入的transContents,用MD5生成全局ID;

          3. GTIS將全局ID作為key,current_timestamp+transContents作為value放入Tair進(jìn)行setNx,將結(jié)果返回給業(yè)務(wù)方;

          4. 業(yè)務(wù)方根據(jù)返回結(jié)果確定能否開(kāi)始進(jìn)行業(yè)務(wù)操作;

          5. 若能,開(kāi)始進(jìn)行操作;若不能,則結(jié)束當(dāng)前操作;

          6. 業(yè)務(wù)方將操作結(jié)果和請(qǐng)求結(jié)果傳入GTIS,系統(tǒng)進(jìn)行一次請(qǐng)求結(jié)果的檢驗(yàn);

          7. 若該次操作成功,GTIS根據(jù)key取出value值,跟傳入的返回結(jié)果進(jìn)行比對(duì),如果兩者相等,則將該全局ID的過(guò)期時(shí)間改為較長(zhǎng)時(shí)間;

          8. GTIS返回最終結(jié)果


          實(shí)現(xiàn)難點(diǎn)

          GTIS的實(shí)現(xiàn)難點(diǎn)在于如何保證其判斷重復(fù)的可靠性。由于分布式環(huán)境的復(fù)雜度和業(yè)務(wù)操作的不確定性,在上一章節(jié)分布式鎖的實(shí)現(xiàn)中考慮的網(wǎng)絡(luò)斷開(kāi)或主機(jī)宕機(jī)等問(wèn)題,同樣需要在GTIS中設(shè)法解決。這里列出幾個(gè)典型的場(chǎng)景:

          1. 如果操作執(zhí)行失敗,理想的情況應(yīng)該是另一個(gè)相同的操作可以立即進(jìn)行。因此,需要對(duì)業(yè)務(wù)方的操作結(jié)果進(jìn)行判斷,如果操作失敗,那么就需要立即刪除該全局ID;

          2. 如果操作超時(shí)或主機(jī)宕機(jī),當(dāng)前的操作無(wú)法告知GTIS操作是否成功。那么我們必須引入超時(shí)機(jī)制,一旦長(zhǎng)時(shí)間獲取不到業(yè)務(wù)方的操作反饋,那么也需要該全局ID失效;

          3. 結(jié)合上兩個(gè)場(chǎng)景,既然全局ID會(huì)失效并且可能會(huì)被刪除,那就需要保證刪除的不是另一個(gè)相同操作的全局ID。這就需要將特殊的標(biāo)識(shí)記錄下來(lái),并由此來(lái)判斷。這里所用的標(biāo)識(shí)為當(dāng)前時(shí)間戳。

          可以看到,解決這些問(wèn)題的思路,也和上一章節(jié)中的實(shí)現(xiàn)有很多類似的地方。除此以外,還有更多的場(chǎng)景需要考慮和解決,所有分支流程如下:

          使用說(shuō)明

          使用時(shí),業(yè)務(wù)方只需要在操作的前后調(diào)用GTIS的前置方法和后置方法,如下圖所示。如果前置方法返回可進(jìn)行操作,則說(shuō)明此時(shí)無(wú)重復(fù)操作,可以進(jìn)行。否則則直接結(jié)束操作。

          使用方需要考慮的主要是下面兩個(gè)參數(shù):

          1. 空間全局性:業(yè)務(wù)方輸入的能夠標(biāo)志操作唯一性的內(nèi)容特性,可以是唯一性的String類型的ID,也可以是map、POJO等形式。如訂單ID等

          2. 時(shí)間全局性:確定在多長(zhǎng)時(shí)間內(nèi)不允許重復(fù),1小時(shí)內(nèi)還是一個(gè)月內(nèi)亦或是永久。


          此外,GTIS還提供了不同的故障處理策略和重試機(jī)制,以此來(lái)降低外部存儲(chǔ)引擎異常對(duì)系統(tǒng)造成的影響。

          目前,GTIS已經(jīng)持續(xù)迭代了7個(gè)版本,距離第一個(gè)版本有近1年之久,先后在美團(tuán)點(diǎn)評(píng)多個(gè)項(xiàng)目中穩(wěn)定運(yùn)行。


          -     總結(jié)    -


          在分布式環(huán)境中,操作互斥性問(wèn)題和冪等性問(wèn)題非常普遍。經(jīng)過(guò)分析,我們找出了解決這兩個(gè)問(wèn)題的基本思路和實(shí)現(xiàn)原理,給出了具體的解決方案。

          針對(duì)操作互斥性問(wèn)題,常見(jiàn)的做法便是通過(guò)分布式鎖來(lái)處理對(duì)共享資源的搶占。分布式鎖的實(shí)現(xiàn),很大程度借鑒了多線程和多進(jìn)程環(huán)境中的互斥鎖的實(shí)現(xiàn)原理。只要滿足一些存儲(chǔ)方面的基本條件,并且能夠解決如網(wǎng)絡(luò)斷開(kāi)等異常情況,那么就可以實(shí)現(xiàn)一個(gè)分布式鎖。

          目前已經(jīng)有基于Zookeeper和Redis等存儲(chǔ)引擎的比較典型的分布式鎖實(shí)現(xiàn)。但是由于單存儲(chǔ)引擎的局限,我們開(kāi)發(fā)了基于ZooKeeper和Tair的多引擎分布式鎖Cerberus,它具有使用靈活方便等諸多優(yōu)點(diǎn),還提供了完善的一鍵降級(jí)方案。

          針對(duì)操作冪等性問(wèn)題,我們可以通過(guò)防止重復(fù)操作來(lái)間接的實(shí)現(xiàn)接口的冪等性。GTIS提供了一套可靠的解決方法:依賴于存儲(chǔ)引擎,通過(guò)對(duì)不同操作所對(duì)應(yīng)的唯一的內(nèi)容特性生成一個(gè)唯一的全局ID來(lái)防止操作重復(fù)。

          目前Cerberus分布式鎖、GTIS都已應(yīng)用在生產(chǎn)環(huán)境并平穩(wěn)運(yùn)行。兩者提供的解決方案已經(jīng)能夠解決大多數(shù)分布式環(huán)境中的操作互斥性和冪等性的問(wèn)題。值得一提的是,分布式鎖和GTIS都不是萬(wàn)能的,它們對(duì)外部存儲(chǔ)系統(tǒng)的強(qiáng)依賴使得在環(huán)境不那么穩(wěn)定的情況下,對(duì)可靠性會(huì)造成一定的影響。在并發(fā)量過(guò)高的情況下,如果不能很好的控制鎖的粒度,那么使用分布式鎖也是不太合適的。

          總的來(lái)說(shuō),分布式環(huán)境下的業(yè)務(wù)場(chǎng)景紛繁復(fù)雜,要解決互斥性和冪等性問(wèn)題還需要結(jié)合當(dāng)前系統(tǒng)架構(gòu)、業(yè)務(wù)需求和未來(lái)演進(jìn)綜合考慮。Cerberus分布式鎖和GTIS也會(huì)持續(xù)不斷地迭代更新,提供更多的引擎選擇、更高效可靠的實(shí)現(xiàn)方式、更簡(jiǎn)捷的接入流程,以期滿足更復(fù)雜的使用場(chǎng)景和業(yè)務(wù)需求。




          原文:blog.csdn.net/zdy0_2004/article/details/52760404

          版權(quán)申明:內(nèi)容來(lái)源網(wǎng)絡(luò),版權(quán)歸原創(chuàng)者所有。除非無(wú)法確認(rèn),我們都會(huì)標(biāo)明作者及出處,如有侵權(quán)煩請(qǐng)告知,我們會(huì)立即刪除并表示歉意。謝謝!

          干貨分享

          最近將個(gè)人學(xué)習(xí)筆記整理成冊(cè),使用PDF分享。關(guān)注我,回復(fù)如下代碼,即可獲得百度盤(pán)地址,無(wú)套路領(lǐng)取!

          ?001:《Java并發(fā)與高并發(fā)解決方案》學(xué)習(xí)筆記;?002:《深入JVM內(nèi)核——原理、診斷與優(yōu)化》學(xué)習(xí)筆記;?003:《Java面試寶典》?004:《Docker開(kāi)源書(shū)》?005:《Kubernetes開(kāi)源書(shū)》?006:《DDD速成(領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)速成)》?007:全部?008:加技術(shù)群討論

          加個(gè)關(guān)注不迷路

          喜歡就點(diǎn)個(gè)"在看"唄^_^

          瀏覽 66
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  黄色性爱在线播放 | 欧美成在线 | 国产视频福利 | 豆花视频在线观看视频 | 秋霞网在线伦日伦 |