一、CAP理論
目前幾乎很多大型網站及應用都是分布式部署的,分布式場景中的數據一致性問題一直是一個比較重要的話題。分布式的CAP理論告訴我們“任何一個分布式系統(tǒng)都無法同時滿足一致性(Consistency)、可用性(Availability)和分區(qū)容錯性(Partition tolerance),最多只能同時滿足兩項?!彼?,很多系統(tǒng)在設計之初就要對這三者做出取舍。在互聯(lián)網領域的絕大多數的場景中,都需要犧牲強一致性來換取系統(tǒng)的高可用性,系統(tǒng)往往只需要保證“最終一致性”,只要這個最終時間是在用戶可以接受的范圍內即可。
二、分布式鎖的幾種實現方式
在很多場景中,我們?yōu)榱吮WC數據的最終一致性,需要很多的技術方案來支持,比如分布式事務、分布式鎖等。有的時候,我們需要保證一個方法在同一時間內只能被同一個線程執(zhí)行。在單機環(huán)境中,Java中其實提供了很多并發(fā)處理相關的API,但是這些API在分布式場景中就無能為力了。也就是說單純的Java Api并不能提供分布式鎖的能力。所以針對分布式鎖的實現目前有多種方案。
針對分布式鎖的實現,目前比較常用的有以下幾種方案:
基于數據庫實現分布式鎖、基于緩存(redis,memcached,tair)實現分布式鎖、 基于Zookeeper實現分布式鎖
在分析這幾種實現方案之前我們先來想一下,我們需要的分布式鎖應該是怎么樣的?(這里以方法鎖為例,資源鎖同理)
- 可以保證在分布式部署的應用集群中,同一個方法在同一時間只能被一臺機器上的一個線程執(zhí)行。
- 這把鎖要是一把可重入鎖(避免死鎖)
- 這把鎖最好是一把阻塞鎖(根據業(yè)務需求考慮要不要這條)
- 有高可用的獲取鎖和釋放鎖功能
- 獲取鎖和釋放鎖的性能要好
三、基于數據庫實現的分布式鎖
基于數據庫表
要實現分布式鎖,最簡單的方式可能就是直接創(chuàng)建一張鎖表,然后通過操作該表中的數據來實現了。
當我們要鎖住某個方法或資源時,我們就在該表中增加一條記錄,想要釋放鎖的時候就刪除這條記錄。
上面這種簡單的實現有以下幾個問題:
- 這把鎖強依賴數據庫的可用性,數據庫是一個單點,一旦數據庫掛掉,會導致業(yè)務系統(tǒng)不可用。
- 這把鎖沒有失效時間,一旦解鎖操作失敗,就會導致鎖記錄一直在數據庫中,其他線程無法再獲得到鎖。
- 這把鎖只能是非阻塞的,因為數據的insert操作,一旦插入失敗就會直接報錯。沒有獲得鎖的線程并不會進入排隊隊列,要想再次獲得鎖就要再次觸發(fā)獲得鎖操作。
- 這把鎖是非重入的,同一個線程在沒有釋放鎖之前無法再次獲得該鎖。因為數據中數據已經存在了。
當然,我們也可以有其他方式解決上面的問題。
- 數據庫是單點?搞兩個數據庫,數據之前雙向同步。一旦掛掉快速切換到備庫上。
- 沒有失效時間?只要做一個定時任務,每隔一定時間把數據庫中的超時數據清理一遍。
- 非阻塞的?搞一個while循環(huán),直到insert成功再返回成功。
- 非重入的?在數據庫表中加個字段,記錄當前獲得鎖的機器的主機信息和線程信息,那么下次再獲取鎖的時候先查詢數據庫,如果當前機器的主機信息和線程信息在數據庫可以查到的話,直接把鎖分配給他就可以了。
基于數據庫排他鎖
除了可以通過增刪操作數據表中的記錄以外,其實還可以借助數據中自帶的鎖來實現分布式的鎖。
我們還用剛剛創(chuàng)建的那張數據庫表。可以通過數據庫的排他鎖來實現分布式鎖。 基于MySql的InnoDB引擎,可以使用以下方法來實現加鎖操作:

在查詢語句后面增加for update,數據庫會在查詢過程中給數據庫表增加排他鎖(這里再多提一句,InnoDB引擎在加鎖的時候,只有通過索引進行檢索的時候才會使用行級鎖,否則會使用表級鎖。這里我們希望使用行級鎖,就要給method_name添加索引,值得注意的是,這個索引一定要創(chuàng)建成唯一索引,否則會出現多個重載方法之間無法同時被訪問的問題。重載方法的話建議把參數類型也加上。)。當某條記錄被加上排他鎖之后,其他線程無法再在該行記錄上增加排他鎖。
我們可以認為獲得排它鎖的線程即可獲得分布式鎖,當獲取到鎖之后,可以執(zhí)行方法的業(yè)務邏輯,執(zhí)行完方法之后,再通過以下方法解鎖:

通過connection.commit()操作來釋放鎖。
這種方法可以有效的解決上面提到的無法釋放鎖和阻塞鎖的問題。
- 阻塞鎖? for update語句會在執(zhí)行成功后立即返回,在執(zhí)行失敗時一直處于阻塞狀態(tài),直到成功。
- 鎖定之后服務宕機,無法釋放?使用這種方式,服務宕機之后數據庫會自己把鎖釋放掉。
但是還是無法直接解決數據庫單點和可重入問題。
這里還可能存在另外一個問題,雖然我們對method_name 使用了唯一索引,并且顯示使用for update來使用行級鎖。但是,MySql會對查詢進行優(yōu)化,即便在條件中使用了索引字段,但是否使用索引來檢索數據是由 MySQL 通過判斷不同執(zhí)行計劃的代價來決定的,如果 MySQL 認為全表掃效率更高,比如對一些很小的表,它就不會使用索引,這種情況下 InnoDB 將使用表鎖,而不是行鎖。如果發(fā)生這種情況就悲劇了。。。
還有一個問題,就是我們要使用排他鎖來進行分布式鎖的lock,那么一個排他鎖長時間不提交,就會占用數據庫連接。一旦類似的連接變得多了,就可能把數據庫連接池撐爆。
總結
總結一下使用數據庫來實現分布式鎖的方式,這兩種方式都是依賴數據庫的一張表,一種是通過表中的記錄的存在情況確定當前是否有鎖存在,另外一種是通過數據庫的排他鎖來實現分布式鎖。
數據庫實現分布式鎖的優(yōu)點:直接借助數據庫,容易理解。
數據庫實現分布式鎖的缺點:
- 會有各種各樣的問題,在解決問題的過程中會使整個方案變得越來越復雜。
- 操作數據庫需要一定的開銷,性能問題需要考慮。
- 使用數據庫的行級鎖并不一定靠譜,尤其是當我們的鎖表并不大的時候。
四、基于緩存實現分布式鎖
相比較于基于數據庫實現分布式鎖的方案來說,基于緩存來實現在性能方面會表現的更好一點。而且很多緩存是可以集群部署的,可以解決單點問題。
目前有很多成熟的緩存產品,包括Redis,memcached以及Tair。
這里以Tair為例來分析下使用緩存實現分布式鎖的方案。關于Redis和memcached在網絡上有很多相關的文章,并且也有一些成熟的框架及算法可以直接使用。
基于Tair的實現分布式鎖其實和Redis類似,其中主要的實現方式是使用TairManager.put方法來實現。
總結
可以使用緩存來代替數據庫來實現分布式鎖,這個可以提供更好的性能,同時,很多緩存服務都是集群部署的,可以避免單點問題。并且很多緩存服務都提供了可以用來實現分布式鎖的方法,比如Tair的put方法,redis的setnx方法等。并且,這些緩存服務也都提供了對數據的過期自動刪除的支持,可以直接設置超時時間來控制鎖的釋放。
使用緩存實現分布式鎖的優(yōu)點:
- 性能好,實現起來較為方便。
- 使用緩存實現分布式鎖的缺點
- 通過超時時間來控制鎖的失效時間并不是十分的靠譜。
五、基于Zookeeper實現分布式鎖
基于zookeeper臨時有序節(jié)點可以實現的分布式鎖。
大致思想即為:每個客戶端對某個方法加鎖時,在zookeeper上的與該方法對應的指定節(jié)點的目錄下,生成一個唯一的瞬時有序節(jié)點。 判斷是否獲取鎖的方式很簡單,只需要判斷有序節(jié)點中序號最小的一個。 當釋放鎖的時候,只需將這個瞬時節(jié)點刪除即可。同時,其可以避免服務宕機導致的鎖無法釋放,而產生的死鎖問題。
來看下Zookeeper能不能解決前面提到的問題。
- 鎖無法釋放?使用Zookeeper可以有效的解決鎖無法釋放的問題,因為在創(chuàng)建鎖的時候,客戶端會在ZK中創(chuàng)建一個臨時節(jié)點,一旦客戶端獲取到鎖之后突然掛掉(Session連接斷開),那么這個臨時節(jié)點就會自動刪除掉。其他客戶端就可以再次獲得鎖。
- 非阻塞鎖?使用Zookeeper可以實現阻塞的鎖,客戶端可以通過在ZK中創(chuàng)建順序節(jié)點,并且在節(jié)點上綁定監(jiān)聽器,一旦節(jié)點有變化,Zookeeper會通知客戶端,客戶端可以檢查自己創(chuàng)建的節(jié)點是不是當前所有節(jié)點中序號最小的,如果是,那么自己就獲取到鎖,便可以執(zhí)行業(yè)務邏輯了。
- 不可重入?使用Zookeeper也可以有效的解決不可重入的問題,客戶端在創(chuàng)建節(jié)點的時候,把當前客戶端的主機信息和線程信息直接寫入到節(jié)點中,下次想要獲取鎖的時候和當前最小的節(jié)點中的數據比對一下就可以了。如果和自己的信息一樣,那么自己直接獲取到鎖,如果不一樣就再創(chuàng)建一個臨時的順序節(jié)點,參與排隊。
- 單點問題?使用Zookeeper可以有效的解決單點問題,ZK是集群部署的,只要集群中有半數以上的機器存活,就可以對外提供服務。
可以直接使用zookeeper第三方庫Curator客戶端,這個客戶端中封裝了一個可重入的鎖服務。

Curator提供的InterProcessMutex是分布式鎖的實現。acquire方法用戶獲取鎖,release方法用于釋放鎖。
使用ZK實現的分布式鎖好像完全符合了本文開頭我們對一個分布式鎖的所有期望。但是,其實并不是,Zookeeper實現的分布式鎖其實存在一個缺點,那就是性能上可能并沒有緩存服務那么高。因為每次在創(chuàng)建鎖和釋放鎖的過程中,都要動態(tài)創(chuàng)建、銷毀瞬時節(jié)點來實現鎖功能。ZK中創(chuàng)建和刪除節(jié)點只能通過Leader服務器來執(zhí)行,然后將數據同步到所有的Follower機器上。
其實,使用Zookeeper也有可能帶來并發(fā)問題,只是并不常見而已??紤]這樣的情況,由于網絡抖動,客戶端可ZK集群的session連接斷了,那么zk以為客戶端掛了,就會刪除臨時節(jié)點,這時候其他客戶端就可以獲取到分布式鎖了。就可能產生并發(fā)問題。這個問題不常見是因為zk有重試機制,一旦zk集群檢測不到客戶端的心跳,就會重試,Curator客戶端支持多種重試策略。多次重試之后還不行的話才會刪除臨時節(jié)點。(所以,選擇一個合適的重試策略也比較重要,要在鎖的粒度和并發(fā)之間找一個平衡。)
總結
使用Zookeeper實現分布式鎖的優(yōu)點:
- 有效的解決單點問題,不可重入問題,非阻塞問題以及鎖無法釋放的問題。實現起來較為簡單。
- 使用Zookeeper實現分布式鎖的缺點:
- 性能上不如使用緩存實現分布式鎖。 需要對ZK的原理有所了解。
六、三種方案的比較
上面幾種方式,哪種方式都無法做到完美。就像CAP一樣,在復雜性、可靠性、性能等方面無法同時滿足,所以,根據不同的應用場景選擇最適合自己的才是王道。
從理解的難易程度角度(從低到高)
數據庫 > 緩存 > Zookeeper
從實現的復雜性角度(從低到高)
Zookeeper >= 緩存 > 數據庫
從性能角度(從高到低)
緩存 > Zookeeper >= 數據庫
從可靠性角度(從高到低)
Zookeeper > 緩存 > 數據庫