本文轉(zhuǎn)自:http://www./archives/1716 目前幾乎很多大型網(wǎng)站及應(yīng)用都是分布式部署的,分布式場景中的數(shù)據(jù)一致性問題一直是一個比較重要的話題。分布式的CAP理論告訴我們“任何一個分布式系統(tǒng)都無法同時滿足一致性(Consistency)、可用性(Availability)和分區(qū)容錯性(Partition tolerance),最多只能同時滿足兩項。”所以,很多系統(tǒng)在設(shè)計之初就要對這三者做出取舍。在互聯(lián)網(wǎng)領(lǐng)域的絕大多數(shù)的場景中,都需要犧牲強一致性來換取系統(tǒng)的高可用性,系統(tǒng)往往只需要保證“最終一致性”,只要這個最終時間是在用戶可以接受的范圍內(nèi)即可。 在很多場景中,我們?yōu)榱吮WC數(shù)據(jù)的最終一致性,需要很多的技術(shù)方案來支持,比如分布式事務(wù)、分布式鎖等。有的時候,我們需要保證一個方法在同一時間內(nèi)只能被同一個線程執(zhí)行。在單機環(huán)境中,Java中其實提供了很多并發(fā)處理相關(guān)的API,但是這些API在分布式場景中就無能為力了。也就是說單純的Java Api并不能提供分布式鎖的能力。所以針對分布式鎖的實現(xiàn)目前有多種方案。 針對分布式鎖的實現(xiàn),目前比較常用的有以下幾種方案:
在分析這幾種實現(xiàn)方案之前我們先來想一下,我們需要的分布式鎖應(yīng)該是怎么樣的?(這里以方法鎖為例,資源鎖同理)
基于數(shù)據(jù)庫實現(xiàn)分布式鎖基于數(shù)據(jù)庫表要實現(xiàn)分布式鎖,最簡單的方式可能就是直接創(chuàng)建一張鎖表,然后通過操作該表中的數(shù)據(jù)來實現(xiàn)了。 當我們要鎖住某個方法或資源時,我們就在該表中增加一條記錄,想要釋放鎖的時候就刪除這條記錄。 創(chuàng)建這樣一張數(shù)據(jù)庫表: 當我們想要鎖住某個方法時,執(zhí)行以下SQL:
因為我們對 當方法執(zhí)行完畢之后,想要釋放鎖的話,需要執(zhí)行以下Sql: 上面這種簡單的實現(xiàn)有以下幾個問題:
當然,我們也可以有其他方式解決上面的問題。
基于數(shù)據(jù)庫排他鎖除了可以通過增刪操作數(shù)據(jù)表中的記錄以外,其實還可以借助數(shù)據(jù)中自帶的鎖來實現(xiàn)分布式的鎖。 我們還用剛剛創(chuàng)建的那張數(shù)據(jù)庫表??梢酝ㄟ^數(shù)據(jù)庫的排他鎖來實現(xiàn)分布式鎖。 基于MySql的InnoDB引擎,可以使用以下方法來實現(xiàn)加鎖操作:
在查詢語句后面增加 我們可以認為獲得排它鎖的線程即可獲得分布式鎖,當獲取到鎖之后,可以執(zhí)行方法的業(yè)務(wù)邏輯,執(zhí)行完方法之后,再通過以下方法解鎖:
通過 這種方法可以有效的解決上面提到的無法釋放鎖和阻塞鎖的問題。
但是還是無法直接解決數(shù)據(jù)庫單點和可重入問題。
這里還可能存在另外一個問題,雖然我們對 還有一個問題,就是我們要使用排他鎖來進行分布式鎖的lock,那么一個排他鎖長時間不提交,就會占用數(shù)據(jù)庫連接。一旦類似的連接變得多了,就可能把數(shù)據(jù)庫連接池撐爆 總結(jié)總結(jié)一下使用數(shù)據(jù)庫來實現(xiàn)分布式鎖的方式,這兩種方式都是依賴數(shù)據(jù)庫的一張表,一種是通過表中的記錄的存在情況確定當前是否有鎖存在,另外一種是通過數(shù)據(jù)庫的排他鎖來實現(xiàn)分布式鎖。 數(shù)據(jù)庫實現(xiàn)分布式鎖的優(yōu)點 直接借助數(shù)據(jù)庫,容易理解。 數(shù)據(jù)庫實現(xiàn)分布式鎖的缺點 會有各種各樣的問題,在解決問題的過程中會使整個方案變得越來越復(fù)雜。 操作數(shù)據(jù)庫需要一定的開銷,性能問題需要考慮。 使用數(shù)據(jù)庫的行級鎖并不一定靠譜,尤其是當我們的鎖表并不大的時候。 基于緩存實現(xiàn)分布式鎖相比較于基于數(shù)據(jù)庫實現(xiàn)分布式鎖的方案來說,基于緩存來實現(xiàn)在性能方面會表現(xiàn)的更好一點。而且很多緩存是可以集群部署的,可以解決單點問題。 目前有很多成熟的緩存產(chǎn)品,包括Redis,memcached以及我們公司內(nèi)部的Tair。 這里以Tair為例來分析下使用緩存實現(xiàn)分布式鎖的方案。關(guān)于Redis和memcached在網(wǎng)絡(luò)上有很多相關(guān)的文章,并且也有一些成熟的框架及算法可以直接使用。
基于Tair的實現(xiàn)分布式鎖其實和Redis類似,其中主要的實現(xiàn)方式是使用 以上實現(xiàn)方式同樣存在幾個問題:
當然,同樣有方式可以解決。
但是,失效時間我設(shè)置多長時間為好?如何設(shè)置的失效時間太短,方法沒等執(zhí)行完,鎖就自動釋放了,那么就會產(chǎn)生并發(fā)問題。如果設(shè)置的時間太長,其他獲取鎖的線程就可能要平白的多等一段時間。這個問題使用數(shù)據(jù)庫實現(xiàn)分布式鎖同樣存在 總結(jié)可以使用緩存來代替數(shù)據(jù)庫來實現(xiàn)分布式鎖,這個可以提供更好的性能,同時,很多緩存服務(wù)都是集群部署的,可以避免單點問題。并且很多緩存服務(wù)都提供了可以用來實現(xiàn)分布式鎖的方法,比如Tair的put方法,redis的setnx方法等。并且,這些緩存服務(wù)也都提供了對數(shù)據(jù)的過期自動刪除的支持,可以直接設(shè)置超時時間來控制鎖的釋放。 使用緩存實現(xiàn)分布式鎖的優(yōu)點 性能好,實現(xiàn)起來較為方便。 使用緩存實現(xiàn)分布式鎖的缺點 通過超時時間來控制鎖的失效時間并不是十分的靠譜。 基于Zookeeper實現(xiàn)分布式鎖基于zookeeper臨時有序節(jié)點可以實現(xiàn)的分布式鎖。 大致思想即為:每個客戶端對某個方法加鎖時,在zookeeper上的與該方法對應(yīng)的指定節(jié)點的目錄下,生成一個唯一的瞬時有序節(jié)點。 判斷是否獲取鎖的方式很簡單,只需要判斷有序節(jié)點中序號最小的一個。 當釋放鎖的時候,只需將這個瞬時節(jié)點刪除即可。同時,其可以避免服務(wù)宕機導(dǎo)致的鎖無法釋放,而產(chǎn)生的死鎖問題。 來看下Zookeeper能不能解決前面提到的問題。
可以直接使用zookeeper第三方庫Curator客戶端,這個客戶端中封裝了一個可重入的鎖服務(wù)。 Curator提供的InterProcessMutex是分布式鎖的實現(xiàn)。acquire方法用戶獲取鎖,release方法用于釋放鎖。 使用ZK實現(xiàn)的分布式鎖好像完全符合了本文開頭我們對一個分布式鎖的所有期望。但是,其實并不是,Zookeeper實現(xiàn)的分布式鎖其實存在一個缺點,那就是性能上可能并沒有緩存服務(wù)那么高。因為每次在創(chuàng)建鎖和釋放鎖的過程中,都要動態(tài)創(chuàng)建、銷毀瞬時節(jié)點來實現(xiàn)鎖功能。ZK中創(chuàng)建和刪除節(jié)點只能通過Leader服務(wù)器來執(zhí)行,然后將數(shù)據(jù)同不到所有的Follower機器上。 其實,使用Zookeeper也有可能帶來并發(fā)問題,只是并不常見而已??紤]這樣的情況,由于網(wǎng)絡(luò)抖動,客戶端可ZK集群的session連接斷了,那么zk以為客戶端掛了,就會刪除臨時節(jié)點,這時候其他客戶端就可以獲取到分布式鎖了。就可能產(chǎn)生并發(fā)問題。這個問題不常見是因為zk有重試機制,一旦zk集群檢測不到客戶端的心跳,就會重試,Curator客戶端支持多種重試策略。多次重試之后還不行的話才會刪除臨時節(jié)點。(所以,選擇一個合適的重試策略也比較重要,要在鎖的粒度和并發(fā)之間找一個平衡。) 總結(jié)使用Zookeeper實現(xiàn)分布式鎖的優(yōu)點 有效的解決單點問題,不可重入問題,非阻塞問題以及鎖無法釋放的問題。實現(xiàn)起來較為簡單。 使用Zookeeper實現(xiàn)分布式鎖的缺點 性能上不如使用緩存實現(xiàn)分布式鎖。 需要對ZK的原理有所了解。 三種方案的比較上面幾種方式,哪種方式都無法做到完美。就像CAP一樣,在復(fù)雜性、可靠性、性能等方面無法同時滿足,所以,根據(jù)不同的應(yīng)用場景選擇最適合自己的才是王道。 從理解的難易程度角度(從低到高)數(shù)據(jù)庫 > 緩存 > Zookeeper 從實現(xiàn)的復(fù)雜性角度(從低到高)Zookeeper >= 緩存 > 數(shù)據(jù)庫 從性能角度(從高到低)緩存 > Zookeeper >= 數(shù)據(jù)庫 從可靠性角度(從高到低)Zookeeper > 緩存 > 數(shù)據(jù)庫 |
|