分布式系統(tǒng)互斥性與冪等性問題的分析與解決

原文鏈接
隨著互聯(lián)網(wǎng)信息技術(shù)的飛速發(fā)展胞谈,數(shù)據(jù)量不斷增大尘盼,業(yè)務(wù)邏輯也日趨復(fù)雜,對系統(tǒng)的高并發(fā)訪問烦绳、海量數(shù)據(jù)處理的場景也越來越多卿捎。如何用較低成本實(shí)現(xiàn)系統(tǒng)的高可用、易伸縮径密、可擴(kuò)展等目標(biāo)就顯得越發(fā)重要午阵。為了解決這一系列問題,系統(tǒng)架構(gòu)也在不斷演進(jìn)享扔。傳統(tǒng)的集中式系統(tǒng)已經(jīng)逐漸無法滿足要求底桂,分布式系統(tǒng)被使用在更多的場景中。

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

  • 可擴(kuò)展性:可通過橫向水平擴(kuò)展提高系統(tǒng)的性能和吞吐量氛魁。
  • 高可靠性:高容錯暮顺,即使系統(tǒng)中一臺或幾臺故障,系統(tǒng)仍可提供服務(wù)秀存。
  • 高并發(fā)性:各機(jī)器并行獨(dú)立處理和計算捶码。
  • 廉價高效:多臺小型機(jī)而非單臺高性能機(jī)。

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

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

  • 互斥性問題掀淘。
  • 冪等性問題旬蟋。

今天我們就針對這兩個問題來進(jìn)行分析。

先看兩個常見的例子:

例1:某服務(wù)記錄關(guān)鍵數(shù)據(jù)X革娄,當(dāng)前值為100倾贰。A請求需要將X增加200冕碟;同時,B請求需要將X減100匆浙。 在理想的情況下安寺,A先讀取到X=100,然后X增加200首尼,最后寫入X=300挑庶。B請求接著從讀取X=300,減少100软能,最后寫入X=200迎捺。 然而在真實(shí)情況下,如果不做任何處理查排,則可能會出現(xiàn):A和B同時讀取到X=100凳枝;A寫入之前B讀取到X;B比A先寫入等等情況跋核。

例2:某服務(wù)提供一組任務(wù)岖瑰,A請求隨機(jī)從任務(wù)組中獲取一個任務(wù);B請求隨機(jī)從任務(wù)組中獲取一個任務(wù)砂代。 在理想的情況下蹋订,A從任務(wù)組中挑選一個任務(wù),任務(wù)組刪除該任務(wù)泊藕,B從剩下的的任務(wù)中再挑一個辅辩,任務(wù)組刪除該任務(wù)。 同樣的娃圆,在真實(shí)情況下玫锋,如果不做任何處理,可能會出現(xiàn)A和B挑中了同一個任務(wù)的情況讼呢。

以上的兩個例子撩鹿,都存在操作互斥性的問題≡闷粒互斥性問題用通俗的話來講节沦,就是對共享資源的搶占問題。如果不同的請求對同一個或者同一組資源讀取并修改時础爬,無法保證按序執(zhí)行甫贯,無法保證一個操作的原子性,那么就很有可能會出現(xiàn)預(yù)期外的情況看蚜。因此操作的互斥性問題叫搁,也可以理解為一個需要保證時序性、原子性的問題。

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

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

多線程環(huán)境解決方案及原理

解決方案

《Thinking in Java》書中寫到:

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

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

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

下面來簡單聊一聊兩者的實(shí)現(xiàn)原理炭臭。

原理

ReentrantLock

ReentrantLock主要利用CAS+CLH隊列來實(shí)現(xiàn)永脓。它支持公平鎖和非公平鎖,兩者的實(shí)現(xiàn)類似鞋仍。

  • CAS:Compare and Swap常摧,比較并交換。CAS有3個操作數(shù):內(nèi)存值V威创、預(yù)期值A(chǔ)落午、要修改的新值B。當(dāng)且僅當(dāng)預(yù)期值A(chǔ)和內(nèi)存值V相同時肚豺,將內(nèi)存值V修改為B溃斋,否則什么都不做。該操作是一個原子操作吸申,被廣泛的應(yīng)用在Java的底層實(shí)現(xiàn)中梗劫。在Java中寞奸,CAS主要是由sun.misc.Unsafe這個類通過JNI調(diào)用CPU底層指令實(shí)現(xiàn)。
  • CLH隊列:帶頭結(jié)點(diǎn)的雙向非循環(huán)鏈表(如下圖所示):
image

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

  • 非公平鎖:如果同時還有另一個線程進(jìn)來嘗試獲取,那么有可能會讓這個線程搶先獲榷恃馈彼哼;
  • 公平鎖:如果同時還有另一個線程進(jìn)來嘗試獲取,當(dāng)它發(fā)現(xiàn)自己不是在隊首的話湘今,就會排到隊尾敢朱,由隊首的線程獲取到鎖。

下面分析下兩個片段:

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;
}

在嘗試獲取鎖的時候摩瞎,會先調(diào)用上面的方法旗们。如果狀態(tài)為0上渴,則表明此時無人占有鎖。此時嘗試進(jìn)行set,一旦成功,則成功占有鎖园蝠。如果狀態(tài)不為0,再判斷是否是當(dāng)前線程獲取到鎖。如果是的話,將狀態(tài)+1,因?yàn)榇藭r就是當(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隊尾之后肯骇,如果發(fā)現(xiàn)前序節(jié)點(diǎn)是head骨稿,則CAS再嘗試獲取一次辙浑。否則侠草,則會根據(jù)前序節(jié)點(diǎn)的狀態(tài)判斷是否需要阻塞。如果需要阻塞肥矢,則調(diào)用LockSupport的park方法阻塞該線程抵代。

synchronized

在Java語言中存在兩種內(nèi)建的synchronized語法:synchronized語句荤牍、synchronized方法康吵。 * synchronized語句:當(dāng)源代碼被編譯成字節(jié)碼的時候惭载,會在同步塊的入口位置和退出位置分別插入monitorenter和monitorexit字節(jié)碼指令; * synchronized方法:在Class文件的方法表中將該方法的access_flags字段中的synchronized標(biāo)志位置1颅眶。這個在specification中沒有明確說明。

在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.

每個對象都有一個鎖,也就是監(jiān)視器(monitor)只泼。當(dāng)monitor被占有時就表示它被鎖定剖笙。線程執(zhí)行monitorenter指令時嘗試獲取對象所對應(yīng)的monitor的所有權(quán),過程如下: * 如果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í)行時坦报,monitor的進(jìn)入數(shù)減1,如果減1后進(jìn)入數(shù)為0狂鞋,那線程退出monitor片择,不再是這個monitor的所有者。其他被這個monitor阻塞的線程可以嘗試去獲取這個monitor的所有權(quán)骚揍。

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

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

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

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

信號量在POSIX標(biāo)準(zhǔn)下有兩種,分別為有名信號量和無名信號量牌捷。無名信號量通常保存在共享內(nèi)存中,而有名信號量是與一個特定的文件名稱相關(guān)聯(lián)涡驮。信號量是一個整數(shù)變量暗甥,有計數(shù)信號量和二值信號量兩種。對信號量的操作捉捅,主要是P操作(wait)和V操作(signal)撤防。

  • P操作:先檢查信號量的大小,若值大于零棒口,則將信號量減1寄月,同時進(jìn)程獲得共享資源的訪問權(quán)限,繼續(xù)執(zhí)行无牵;若小于或者等于零漾肮,則該進(jìn)程被阻塞后,進(jìn)入等待隊列茎毁。
  • V操作:該操作將信號量的值加1克懊,如果有進(jìn)程阻塞著等待該信號量,那么其中一個進(jìn)程將被喚醒七蜘。

舉個例子谭溉,設(shè)信號量為1,當(dāng)一個進(jìn)程A在進(jìn)入臨界區(qū)之前橡卤,先進(jìn)行P操作扮念。發(fā)現(xiàn)值大于零,那么就將信號量減為0碧库,進(jìn)入臨界區(qū)執(zhí)行柜与。此時巧勤,若另一個進(jìn)程B也要進(jìn)去臨界區(qū),進(jìn)行P操作旅挤,發(fā)現(xiàn)信號量等于0踢关,則會被阻塞。當(dāng)進(jìn)程A退出臨界區(qū)時粘茄,會進(jìn)行V操作签舞,將信號量的值加1,并喚醒阻塞的進(jìn)程B柒瓣。此時B就可以進(jìn)入臨界區(qū)了儒搭。

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

通過上面的一些了解磺平,我們可以概括出解決互斥性問題魂仍,即資源搶占的基本方式為:

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

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

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

分布式環(huán)境下的解決方案——分布式鎖

首先,我們來看看分布式鎖的基本條件巍耗。

分布式鎖條件

基本條件

再回顧下多線程和多進(jìn)程環(huán)境下的鎖秋麸,可以發(fā)現(xiàn)鎖的實(shí)現(xiàn)有很多共通之處,它們都需要滿足一些最基本的條件: 1. 需要有存儲鎖的空間炬太,并且鎖的空間是可以訪問到的灸蟆。 2. 鎖需要被唯一標(biāo)識。 3. 鎖要有至少兩種狀態(tài)。

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

  • 存儲空間

鎖是一個抽象的概念炒考,鎖的實(shí)現(xiàn)可缚,需要依存于一個可以存儲鎖的空間。在多線程中是內(nèi)存斋枢,在多進(jìn)程中是內(nèi)存或者磁盤帘靡。更重要的是,這個空間是可以被訪問到的瓤帚。多線程中描姚,不同的線程都可以訪問到堆中的成員變量;在多進(jìn)程中戈次,不同的進(jìn)程可以訪問到共享內(nèi)存中的數(shù)據(jù)或者存儲在磁盤中的文件轩勘。但是在分布式環(huán)境中,不同的主機(jī)很難訪問對方的內(nèi)存或磁盤怯邪。這就需要一個都能訪問到的外部空間來作為存儲空間绊寻。

最普遍的外部存儲空間就是數(shù)據(jù)庫了,事實(shí)上也確實(shí)有基于數(shù)據(jù)庫做分布式鎖(行鎖悬秉、version樂觀鎖)澄步,如quartz集群架構(gòu)中就有所使用。除此以外和泌,還有各式緩存如Redis村缸、Tair、Memcached允跑、Mongodb王凑,當(dāng)然還有專門的分布式協(xié)調(diào)服務(wù)Zookeeper搪柑,甚至是另一臺主機(jī)聋丝。只要可以存儲數(shù)據(jù)、鎖在其中可以被多主機(jī)訪問到工碾,那就可以作為分布式鎖的存儲空間弱睦。

  • 唯一標(biāo)識

不同的共享資源,必然需要用不同的鎖進(jìn)行保護(hù)渊额,因此相應(yīng)的鎖必須有唯一的標(biāo)識况木。在多線程環(huán)境中,鎖可以是一個對象旬迹,那么對這個對象的引用便是這個唯一標(biāo)識火惊。多進(jìn)程環(huán)境中,信號量在共享內(nèi)存中也是由引用來作為唯一的標(biāo)識奔垦。但是如果不在內(nèi)存中屹耐,失去了對鎖的引用,如何唯一標(biāo)識它呢椿猎?上文提到的有名信號量惶岭,便是用硬盤中的文件名作為唯一標(biāo)識寿弱。因此,在分布式環(huán)境中按灶,只要給這個鎖設(shè)定一個名稱症革,并且保證這個名稱是全局唯一的,那么就可以作為唯一標(biāo)識鸯旁。

  • 至少兩種狀態(tài)

為了給臨界區(qū)加鎖和解鎖噪矛,需要存儲兩種不同的狀態(tài)。如ReentrantLock中的status羡亩,0表示沒有線程競爭摩疑,大于0表示有線程競爭;信號量大于0表示可以進(jìn)入臨界區(qū)畏铆,小于等于0則表示需要被阻塞雷袋。因此只要在分布式環(huán)境中,鎖的狀態(tài)有兩種或以上:如有鎖辞居、沒鎖楷怒;存在、不存在等等瓦灶,均可以實(shí)現(xiàn)鸠删。

有了這三個條件,基本就可以實(shí)現(xiàn)一個簡單的分布式鎖了贼陶。下面以數(shù)據(jù)庫為例刃泡,實(shí)現(xiàn)一個簡單的分布式鎖: 數(shù)據(jù)庫表,字段為鎖的ID(唯一標(biāo)識)碉怔,鎖的狀態(tài)(0表示沒有被鎖烘贴,1表示被鎖)。 偽代碼為:

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

問題

以上的方式即可以實(shí)現(xiàn)一個粗糙的分布式鎖撮胧,但是這樣的實(shí)現(xiàn)桨踪,有沒有什么問題呢?

  • 問題1:鎖狀態(tài)判斷原子性無法保證 從讀取鎖的狀態(tài)芹啥,到判斷該狀態(tài)是否為被鎖锻离,需要經(jīng)歷兩步操作。如果不能保證這兩步的原子性墓怀,就可能導(dǎo)致不止一個請求獲取到了鎖汽纠,這顯然是不行的。因此傀履,我們需要保證鎖狀態(tài)判斷的原子性虱朵。

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

  • 問題3:無法保證釋放的是自己上鎖的那把鎖 在解決了問題2的情況下再設(shè)想一下蚯涮,假設(shè)持有鎖的主機(jī)A在臨界區(qū)遇到網(wǎng)絡(luò)抖動導(dǎo)致網(wǎng)絡(luò)斷開治专,分布式鎖及時的釋放掉了這把鎖。之后遭顶,另一個主機(jī)B占有了這把鎖张峰,但是此時主機(jī)A網(wǎng)絡(luò)恢復(fù),退出臨界區(qū)時解鎖棒旗。由于都是同一把鎖喘批,所以A就會將B的鎖解開。此時如果有第三個主機(jī)嘗試搶占這把鎖铣揉,也將會成功獲得饶深。因此,我們需要在解鎖時逛拱,確定自己解的這個鎖正是自己鎖上的敌厘。

進(jìn)階條件

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

  1. 可重入:線程中的可重入,指的是外層函數(shù)獲得鎖之后箭窜,內(nèi)層也可以獲得鎖毯焕,ReentrantLock和synchronized都是可重入鎖衍腥;衍生到分布式環(huán)境中磺樱,一般仍然指的是線程的可重入,在絕大多數(shù)分布式環(huán)境中婆咸,都要求分布式鎖是可重入的竹捉。
  2. 驚群效應(yīng)(Herd Effect):在分布式鎖中,驚群效應(yīng)指的是尚骄,在有多個請求等待獲取鎖的時候块差,一旦占有鎖的線程釋放之后,如果所有等待的方都同時被喚醒,嘗試搶占鎖憨闰。但是這樣的情況會造成比較大的開銷状蜗,那么在實(shí)現(xiàn)分布式鎖的時候,應(yīng)該盡量避免驚群效應(yīng)的產(chǎn)生鹉动。
  3. 公平鎖和非公平鎖:不同的需求轧坎,可能需要不同的分布式鎖。非公平鎖普遍比公平鎖開銷小泽示。但是業(yè)務(wù)需求如果必須要鎖的競爭者按順序獲得鎖缸血,那么就需要實(shí)現(xiàn)公平鎖。
  4. 阻塞鎖和自旋鎖:針對不同的使用場景械筛,阻塞鎖和自旋鎖的效率也會有所不同捎泻。阻塞鎖會有上下文切換,如果并發(fā)量比較高且臨界區(qū)的操作耗時比較短埋哟,那么造成的性能開銷就比較大了笆豁。但是如果臨界區(qū)操作耗時比較長,一直保持自旋赤赊,也會對CPU造成更大的負(fù)荷渔呵。

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

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

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

ZooKeeper(以下簡稱“ZK”)中有一種節(jié)點(diǎn)叫做順序節(jié)點(diǎn)扩氢,假如我們在/lock/目錄下創(chuàng)建3個節(jié)點(diǎn),ZK集群會按照發(fā)起創(chuàng)建的順序來創(chuàng)建節(jié)點(diǎn)爷辱,節(jié)點(diǎn)分別為/lock/0000000001录豺、/lock/0000000002、/lock/0000000003饭弓。

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

根據(jù)ZK中節(jié)點(diǎn)是否存在阀趴,可以作為分布式鎖的鎖狀態(tài)昏翰,以此來實(shí)現(xiàn)一個分布式鎖,下面是分布式鎖的基本邏輯: 1. 客戶端調(diào)用create()方法創(chuàng)建名為“/dlm-locks/lockname/lock-”的臨時順序節(jié)點(diǎn)。 2. 客戶端調(diào)用getChildren(“l(fā)ockname”)方法來獲取所有已經(jīng)創(chuàng)建的子節(jié)點(diǎn)。 3. 客戶端獲取到所有子節(jié)點(diǎn)path之后声邦,如果發(fā)現(xiàn)自己在步驟1中創(chuàng)建的節(jié)點(diǎn)是所有節(jié)點(diǎn)中序號最小的婉支,那么就認(rèn)為這個客戶端獲得了鎖。 4. 如果創(chuàng)建的節(jié)點(diǎn)不是所有節(jié)點(diǎn)中需要最小的池磁,那么則監(jiān)視比自己創(chuàng)建節(jié)點(diǎn)的序列號小的最大的節(jié)點(diǎn)扇雕,進(jìn)入等待金拒。直到下次監(jiān)視的子節(jié)點(diǎn)變更的時候码邻,再進(jìn)行子節(jié)點(diǎn)的獲取折剃,判斷是否獲取鎖。

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

開源的基于ZK的Menagerie的源碼就是一個典型的例子:https://github.com/sfines/menagerie 开睡。

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

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

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

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

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

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

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

以下是一種比較普遍但不太完善的Redis分布式鎖的實(shí)現(xiàn)步驟(與下圖一一對應(yīng)): 1. 線程A發(fā)送SETNX lock.orderid <current unix="" time="" +="" lock="" timeout="" 1="" style="box-sizing: border-box;">嘗試獲得鎖历谍,如果鎖不存在,則set并獲得鎖辣垒。 2. 如果鎖存在望侈,則再判斷鎖的值(時間戳)是否大于當(dāng)前時間,如果沒有超時勋桶,則等待一下再重試脱衙。 3. 如果已經(jīng)超時了,在用GETSET lock.{orderid} <current unix="" time="" +="" lock="" timeout="" 1="" style="box-sizing: border-box;">來嘗試獲取鎖例驹,如果這時候拿到的時間戳仍舊超時捐韩,則說明已經(jīng)獲得鎖了。 4. 如果在此之前鹃锈,另一個線程C快一步執(zhí)行了上面的操作荤胁,那么A拿到的時間戳是個未超時的值,這時A沒有如期獲得鎖屎债,需要再次等待或重試寨蹋。</current></current>

image

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

以上是Redis的一種常見的實(shí)現(xiàn)方式,除此以外還可以用SETNX+EXPIRE來實(shí)現(xiàn)召娜。Redisson是一個官方推薦的Redis客戶端并且實(shí)現(xiàn)了很多分布式的功能运褪。它的分布式鎖就提供了一種更完善的解決方案,源碼:https://github.com/mrniko/redisson 玖瘸。

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

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

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

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

  • 強(qiáng)依賴于外部組件:分布式鎖的實(shí)現(xiàn)都需要依賴于外部數(shù)據(jù)存儲如ZK妹孙、Redis等等,因此一旦這些外部組件出現(xiàn)故障获枝,那么分布式鎖就不可用了涕蜂。
  • 無法完全滿足需求:不同分布式鎖的實(shí)現(xiàn),都有相應(yīng)的特點(diǎn)映琳,對于一些需求并不能很好的滿足机隙,如實(shí)現(xiàn)公平鎖、給等待鎖加超時時間等等萨西。

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

Cerberus分布式鎖

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

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

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

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

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

功能需求 Tair ZK
并發(fā)量高 ?
響應(yīng)時間敏感 ?
臨界區(qū)執(zhí)行時間長 ?
公平鎖 ?
非公平鎖 ?
讀寫鎖 ?

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

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

  • void lock(); 獲取鎖披诗,如果鎖被占用撬即,將禁用當(dāng)前線程,并且在獲得鎖之前藤巢,該線程將一直處于阻塞狀態(tài)搞莺。
  • boolean tryLock(); 僅在調(diào)用時鎖為空閑狀態(tài)才獲取該鎖息罗。 如果鎖可用掂咒,則獲取鎖,并立即返回值 true迈喉。如果鎖不可用绍刮,則此方法將立即返回值 false。
  • boolean tryLock(long time, TimeUnit unit) throws InterruptedException; 如果鎖在給定的等待時間內(nèi)空閑挨摸,并且當(dāng)前線程未被中斷孩革,則獲取鎖。 如果在給定時間內(nèi)鎖可用得运,則獲取鎖膝蜈,并立即返回值 true。如果在給定時間內(nèi)鎖一直不可用熔掺,則此方法將立即返回值false饱搏。
  • void lockInterruptibly() throws InterruptedException; 獲取鎖,如果鎖被占用置逻,則一直等待直到線程被中斷或者獲取到鎖推沸。
  • void unlock(); 釋放當(dāng)前持有的鎖。

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

Cerberus提供了實(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)使用方選擇了兩種引擎,平時分布式鎖會工作在主引擎上雇盖。一旦所依賴的主引擎出現(xiàn)故障忿等,那么使用方可以通過自動或者手動方式調(diào)用該切換引擎接口,平滑的將分布式鎖切換到另一個引擎上以將風(fēng)險降到最低崔挖。自動切換方式可以利用Hystrix實(shí)現(xiàn)贸街。手動切換推薦的一個方案則是使用美團(tuán)點(diǎn)評基于Zookeeper的基礎(chǔ)組件MCC庵寞,通過監(jiān)聽MCC配置項(xiàng)更改,來達(dá)到手動將分布式系統(tǒng)所有主機(jī)同步切換引擎的目的薛匪。需要注意的是捐川,切換引擎目前并不會遷移原引擎已有的鎖。這樣做的目的是出于必要性逸尖、系統(tǒng)復(fù)雜度和可靠性的綜合考慮古沥。在實(shí)際情況下,引擎故障到切換引擎娇跟,尤其是手動切換引擎的時間岩齿,要遠(yuǎn)大于分布式鎖的存活時間。作為較輕量級的Cerberus來說苞俘,遷移鎖會帶來不必要的開銷以及較高的系統(tǒng)復(fù)雜度盹沈。鑒于此,如果想要保證在引擎故障后的絕對可靠吃谣,那么則需要結(jié)合其他方案來進(jìn)行處理乞封。

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

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

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

那么我們?yōu)槭裁葱枰涌诰哂袃绲刃阅刂瘢吭O(shè)想一下以下情形: * 在App中下訂單的時候,點(diǎn)擊確認(rèn)之后屁倔,沒反應(yīng)脑又,就又點(diǎn)擊了幾次。在這種情況下,如果無法保證該接口的冪等性问麸,那么將會出現(xiàn)重復(fù)下單問題往衷。 * 在接收消息的時候,消息推送重復(fù)严卖。如果處理消息的接口無法保證冪等席舍,那么重復(fù)消費(fèi)消息產(chǎn)生的影響可能會非常大。

在分布式環(huán)境中哮笆,網(wǎng)絡(luò)環(huán)境更加復(fù)雜来颤,因前端操作抖動、網(wǎng)絡(luò)故障疟呐、消息重復(fù)脚曾、響應(yīng)速度慢等原因东且,對接口的重復(fù)調(diào)用概率會比集中式環(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)境中珊泳,有些接口是天然保證冪等性的鲁冯,如查詢操作。有些對數(shù)據(jù)的修改是一個常量色查,并且無其他記錄和操作薯演,那也可以說是具有冪等性的秧了。其他情況下跨扮,所有涉及對數(shù)據(jù)的修改、狀態(tài)的變更就都有必要防止重復(fù)性操作的發(fā)生验毡。通過間接的實(shí)現(xiàn)接口的冪等性來防止重復(fù)操作所帶來的影響衡创,成為了一種有效的解決方案晶通。

GTIS

GTIS就是這樣的一個解決方案。它是一個輕量的重復(fù)操作關(guān)卡系統(tǒng)树叽,它能夠確保在分布式環(huán)境中操作的唯一性京痢。我們可以用它來間接保證每個操作的冪等性。它具有如下特點(diǎn): * 高效:低延時携茂,單個方法平均響應(yīng)時間在2ms內(nèi)你踩,幾乎不會對業(yè)務(wù)造成影響; * 可靠:提供降級策略讳苦,以應(yīng)對外部存儲引擎故障所造成的影響带膜;提供應(yīng)用鑒權(quán),提供集群配置自定義鸳谜,降低不同業(yè)務(wù)之間的干擾膝藕; * 簡單:接入簡捷方便,學(xué)習(xí)成本低咐扭。只需簡單的配置芭挽,在代碼中進(jìn)行兩個方法的調(diào)用即可完成所有的接入工作; * 靈活:提供多種接口參數(shù)蝗肪、使用策略袜爪,以滿足不同的業(yè)務(wù)需求。

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

基本原理

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

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

  • key:將對于不同的業(yè)務(wù),采用APP_KEY+業(yè)務(wù)操作內(nèi)容特性生成一個唯一標(biāo)識trans_contents。然后對唯一標(biāo)識進(jìn)行加密生成全局ID作為Key法焰。
  • value:current_timestamp + trans_contents秧荆,current_timestamp用于標(biāo)識當(dāng)前的操作線程。

判斷是否重復(fù)埃仪,主要利用Tair的SETNX方法乙濒,如果原來沒有值則set且返回成功,如果已經(jīng)有值則返回失敗卵蛉。

內(nèi)部流程

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

  1. 業(yè)務(wù)方在業(yè)務(wù)操作之前颁股,生成一個能夠唯一標(biāo)識該操作的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é)果確定能否開始進(jìn)行業(yè)務(wù)操作运准;
  5. 若能幌氮,開始進(jìn)行操作缭受;若不能胁澳,則結(jié)束當(dāng)前操作;
  6. 業(yè)務(wù)方將操作結(jié)果和請求結(jié)果傳入GTIS米者,系統(tǒng)進(jìn)行一次請求結(jié)果的檢驗(yàn)韭畸;
  7. 若該次操作成功,GTIS根據(jù)key取出value值蔓搞,跟傳入的返回結(jié)果進(jìn)行比對胰丁,如果兩者相等,則將該全局ID的過期時間改為較長時間喂分;
  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ò)斷開或主機(jī)宕機(jī)等等問題甘萧,同樣需要在GTIS中設(shè)法解決。這里列出幾個典型的場景:

  • 如果操作執(zhí)行失敗梆掸,理想的情況應(yīng)該是另一個相同的操作可以立即進(jìn)行扬卷。因此,需要對業(yè)務(wù)方的操作結(jié)果進(jìn)行判斷酸钦,如果操作失敗怪得,那么就需要立即刪除該全局ID;
  • 如果操作超時或主機(jī)宕機(jī),當(dāng)前的操作無法告知GTIS操作是否成功徒恋。那么我們必須引入超時機(jī)制蚕断,一旦長時間獲取不到業(yè)務(wù)方的操作反饋,那么也需要該全局ID失效入挣;
  • 結(jié)合上兩個場景基括,既然全局ID會失效并且可能會被刪除,那就需要保證刪除的不是另一個相同操作的全局ID财岔。這就需要將特殊的標(biāo)識記錄下來风皿,并由此來判斷。這里所用的標(biāo)識為當(dāng)前時間戳匠璧。

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

使用說明

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

image

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

  • 空間全局性:業(yè)務(wù)方輸入的能夠標(biāo)志操作唯一性的內(nèi)容特性杂彭,可以是唯一性的String類型的ID,也可以是map吓揪、POJO等形式亲怠。如訂單ID等
  • 時間全局性:確定在多長時間內(nèi)不允許重復(fù),1小時內(nèi)還是一個月內(nèi)亦或是永久柠辞。

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

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

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

針對操作互斥性問題荣瑟,常見的做法便是通過分布式鎖來處理對共享資源的搶占治拿。分布式鎖的實(shí)現(xiàn),很大程度借鑒了多線程和多進(jìn)程環(huán)境中的互斥鎖的實(shí)現(xiàn)原理笆焰。只要滿足一些存儲方面的基本條件劫谅,并且能夠解決如網(wǎng)絡(luò)斷開等異常情況,那么就可以實(shí)現(xiàn)一個分布式鎖嚷掠。目前已經(jīng)有基于Zookeeper和Redis等存儲引擎的比較典型的分布式鎖實(shí)現(xiàn)捏检。但是由于單存儲引擎的局限,我們開發(fā)了基于ZooKeeper和Tair的多引擎分布式鎖Cerberus不皆,它具有使用靈活方便等諸多優(yōu)點(diǎn)贯城,還提供了完善的一鍵降級方案。

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

目前Cerberus分布式鎖踩晶、GTIS都已應(yīng)用在生產(chǎn)環(huán)境并平穩(wěn)運(yùn)行。兩者提供的解決方案已經(jīng)能夠解決大多數(shù)分布式環(huán)境中的操作互斥性和冪等性的問題枕磁。值得一提的是渡蜻,分布式鎖和GTIS都不是萬能的,它們對外部存儲系統(tǒng)的強(qiáng)依賴使得在環(huán)境不那么穩(wěn)定的情況下透典,對可靠性會造成一定的影響晴楔。在并發(fā)量過高的情況下,如果不能很好的控制鎖的粒度峭咒,那么使用分布式鎖也是不太合適的〖退辏總的來說凑队,分布式環(huán)境下的業(yè)務(wù)場景紛繁復(fù)雜,要解決互斥性和冪等性問題還需要結(jié)合當(dāng)前系統(tǒng)架構(gòu)幔翰、業(yè)務(wù)需求和未來演進(jìn)綜合考慮漩氨。Cerberus分布式鎖和GTIS也會持續(xù)不斷地迭代更新,提供更多的引擎選擇遗增、更高效可靠的實(shí)現(xiàn)方式叫惊、更簡捷的接入流程,以期滿足更復(fù)雜的使用場景和業(yè)務(wù)需求做修。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末霍狰,一起剝皮案震驚了整個濱河市抡草,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蔗坯,老刑警劉巖康震,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異宾濒,居然都是意外死亡腿短,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進(jìn)店門绘梦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來橘忱,“玉大人,你說我怎么就攤上這事卸奉○懈叮” “怎么了?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵择卦,是天一觀的道長敲长。 經(jīng)常有香客問我,道長秉继,這世上最難降的妖魔是什么祈噪? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮尚辑,結(jié)果婚禮上辑鲤,老公的妹妹穿的比我還像新娘。我一直安慰自己杠茬,他們只是感情好月褥,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著瓢喉,像睡著了一般宁赤。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上栓票,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天决左,我揣著相機(jī)與錄音,去河邊找鬼走贪。 笑死佛猛,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的坠狡。 我是一名探鬼主播继找,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼逃沿!你這毒婦竟也來了婴渡?” 一聲冷哼從身側(cè)響起幻锁,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎缩搅,沒想到半個月后越败,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡硼瓣,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年究飞,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片堂鲤。...
    茶點(diǎn)故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡亿傅,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出瘟栖,到底是詐尸還是另有隱情葵擎,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布半哟,位于F島的核電站酬滤,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏寓涨。R本人自食惡果不足惜盯串,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望戒良。 院中可真熱鬧体捏,春花似錦、人聲如沸糯崎。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽沃呢。三九已至年栓,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間樟插,已是汗流浹背韵洋。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留黄锤,地道東北人。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓食拜,卻偏偏與公主長得像鸵熟,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子负甸,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評論 2 353

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