springboot整合curator實(shí)現(xiàn)分布式鎖過程
springboot curator實(shí)現(xiàn)分布式鎖
理論篇:
Curator是Netflix開源的一套ZooKeeper客戶端框架. Netflix在使用ZooKeeper的過程中發(fā)現(xiàn)ZooKeeper自帶的客戶端太底層, 應(yīng)用方在使用的時候需要自己處理很多事情, 于是在它的基礎(chǔ)上包裝了一下, 提供了一套更好用的客戶端框架. Netflix在用ZooKeeper的過程中遇到的問題, 我們也遇到了, 所以開始研究一下, 首先從他在github上的源碼, wiki文檔以及Netflix的技術(shù)blog入手.
看完官方的文檔之后, 發(fā)現(xiàn)Curator主要解決了三類問題:
- 封裝ZooKeeper client與ZooKeeper server之間的連接處理;
- 提供了一套Fluent風(fēng)格的操作API;
- 提供ZooKeeper各種應(yīng)用場景(recipe, 比如共享鎖服務(wù), 集群領(lǐng)導(dǎo)選舉機(jī)制)的抽象封裝.
Curator列舉的ZooKeeper使用過程中的幾個問題
- 初始化連接的問題: 在client與server之間握手建立連接的過程中, 如果握手失敗, 執(zhí)行所有的同步方法(比如create, getData等)將拋出異常
- 自動恢復(fù)(failover)的問題: 當(dāng)client與一臺server的連接丟失,并試圖去連接另外一臺server時, client將回到初始連接模式
- session過期的問題: 在極端情況下, 出現(xiàn)ZooKeeper session過期, 客戶端需要自己去監(jiān)聽該狀態(tài)并重新創(chuàng)建ZooKeeper實(shí)例 .
- 對可恢復(fù)異常的處理:當(dāng)在server端創(chuàng)建一個有序ZNode, 而在將節(jié)點(diǎn)名返回給客戶端時崩潰, 此時client端拋出可恢復(fù)的異常, 用戶需要自己捕獲這些異常并進(jìn)行重試
- 使用場景的問題:Zookeeper提供了一些標(biāo)準(zhǔn)的使用場景支持, 但是ZooKeeper對這些功能的使用說明文檔很少, 而且很容易用錯. 在一些極端場景下如何處理, zk并沒有給出詳細(xì)的文檔說明. 比如共享鎖服務(wù), 當(dāng)服務(wù)器端創(chuàng)建臨時順序節(jié)點(diǎn)成功, 但是在客戶端接收到節(jié)點(diǎn)名之前掛掉了, 如果不能很好的處理這種情況, 將導(dǎo)致死鎖.
Curator主要從以下幾個方面降低了zk使用的復(fù)雜性:
- 重試機(jī)制:提供可插拔的重試機(jī)制, 它將給捕獲所有可恢復(fù)的異常配置一個重試策略, 并且內(nèi)部也提供了幾種標(biāo)準(zhǔn)的重試策略(比如指數(shù)補(bǔ)償).
- 連接狀態(tài)監(jiān)控: Curator初始化之后會一直的對zk連接進(jìn)行監(jiān)聽, 一旦發(fā)現(xiàn)連接狀態(tài)發(fā)生變化, 將作出相應(yīng)的處理.
- zk客戶端實(shí)例管理:Curator對zk客戶端到server集群連接進(jìn)行管理. 并在需要的情況, 重建zk實(shí)例, 保證與zk集群的可靠連接
- 各種使用場景支持:Curator實(shí)現(xiàn)zk支持的大部分使用場景支持(甚至包括zk自身不支持的場景), 這些實(shí)現(xiàn)都遵循了zk的最佳實(shí)踐, 并考慮了各種極端情況.
Curator通過以上的處理, 讓用戶專注于自身的業(yè)務(wù)本身, 而無需花費(fèi)更多的精力在zk本身.
實(shí)操篇:
CuratorFrameworkFactory類提供了兩個方法, 一個工廠方法newClient, 一個構(gòu)建方法build. 使用工廠方法newClient可以創(chuàng)建一個默認(rèn)的實(shí)例, 而build構(gòu)建方法可以對實(shí)例進(jìn)行定制. 當(dāng)CuratorFramework實(shí)例構(gòu)建完成, 緊接著調(diào)用start()方法, 在應(yīng)用結(jié)束的時候, 需要調(diào)用close()方法. CuratorFramework是線程安全的. 在一個應(yīng)用中可以共享同一個zk集群的CuratorFramework.
核心對象CuratorFramework的創(chuàng)建如下:
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000,3); CuratorFramework client = CuratorFrameworkFactory.builder() ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? .connectString("") ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? .sessionTimeoutMs(5000) ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? .connectionTimeoutMs(5000) ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? .retryPolicy(retryPolicy) ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? .build(); client.start();
需要使用分布式鎖的地方,代碼如下:
String lockOn= "test"; InterProcessMutex mutex = new InterProcessMutex(curatorFramework,lockOn); boolean locked =mutex.acquire(0,TimeUnit.SECONDS); //finally部分 ? mutex.release();
分布式鎖常用于定時任務(wù),使用自定義注解,使用spring aspect around, 在真正的代碼執(zhí)行之前嘗試獲取鎖,獲取不到直接退出,獲取到鎖的,執(zhí)行具體業(yè)務(wù),代碼如下:
@Aspect public class DistributedLockAspect{ ? ? @Pointcut("@annotation(com.**.**.DistributedLock") ? ? public void methodAspect(){}; ? ? ?? ? ? @Around("methodAspect()") ? ? public Object execute(ProceedingJoinPoint joinPoint) throws Exception{ ? ?? ? ? String lockPath = "/opt/zookeeper/lock"; ? ? InterProcessMutex mutex = new InterProcessMutex(cruatorFramework,lockPath); ? ? try{ ? ? ? ?boolean locked = mutex.acquire(0,TimeUnit.SECONDS); ? ? ? ?if(!locked){ ? ? ? ? ? return null; ? ? ? }else{ ? ? ? ? return joinPoint.proceed(); ? ? ? } ? ?}catch(Exception e){ ? ? ? ?e.printStackTrace(); ? ?}finally{ ? ? ? ?mutex.release(); ? ?} ?} }?
自定義注解:
?@Target(ElementType.METHOD) ?@Retention(RetentionPolicy.RUNTIME) ?public @interface DistributedLock{ ? ? String lockPath(); ? ?}
注意事項(xiàng):
1.CuratorFramework對象建議在應(yīng)用中做單例處理,在具體使用處 注入使用, 并在應(yīng)用結(jié)束前銷毀,代碼如下:
@Configration public class CuratorConfigration{ ? ? @Bean ? ? ? ? public CuratorFramework initCuratorFramework(){ ? ? ? ? //忽略? ? ? ? ?// 參照前面 CuratorFramework 對象創(chuàng)建部分 ? ? } ? ? }
2.在aspect部分將curatorFramework對象進(jìn)行關(guān)閉
@PreDestroy public void destroy(){ ? ?CloseableUtils.closeQuietly(curatorFramework); }
項(xiàng)目實(shí)際應(yīng)用中分布式鎖介紹
鎖的介紹
1、悲觀鎖
顧名思義,很悲觀,就是每次拿數(shù)據(jù)的時候都認(rèn)為別的線程會修改數(shù)據(jù),所以在每次拿的時候都會給數(shù)據(jù)上鎖。上鎖之后,當(dāng)別的線程想要拿數(shù)據(jù)時,就會阻塞,直到給數(shù)據(jù)上鎖的線程將事務(wù)提交或者回滾。傳統(tǒng)的關(guān)系型數(shù)據(jù)庫里就用到了很多這種鎖機(jī)制,比如行鎖,表鎖,共享鎖,排他鎖等,都是在做操作之前先上鎖。
2、行鎖
通過select for update語句給sid = 1的數(shù)據(jù)行上了鎖
3、表鎖
select * from student for update;
4、頁鎖
行鎖鎖指定行,表鎖鎖整張表,頁鎖是折中實(shí)現(xiàn),即一次鎖定相鄰的一組記錄。
5、共享鎖
共享鎖又稱為讀鎖,一個線程給數(shù)據(jù)加上共享鎖后,其他線程只能讀數(shù)據(jù),不能修改。
6、排他鎖
排他鎖又稱為寫鎖,和共享鎖的區(qū)別在于,其他線程既不能讀也不能修改。
7、樂觀鎖
樂觀鎖其實(shí)不會上鎖。顧名思義,很樂觀,它默認(rèn)別的線程不會修改數(shù)據(jù),所以不會上鎖。只是在更新前去判斷別的線程在此期間有沒有修改數(shù)據(jù),如果修改了,會交給業(yè)務(wù)層去處理。
- 目前幾乎很多大型網(wǎng)站及應(yīng)用都是分布式部署的,分布式場景中的數(shù)據(jù)一致性問題一直是一個比較重要的話題。分布式的CAP理論告訴我們“任何一個分布式系統(tǒng)都無法同時滿足一致性(Consistency)、可用性(Availability)和分區(qū)容錯性(Partition tolerance),最多只能同時滿足兩項(xiàng)。”所以,很多系統(tǒng)在設(shè)計(jì)之初就要對這三者做出取舍。在互聯(lián)網(wǎng)領(lǐng)域的絕大多數(shù)的場景中,都需要犧牲強(qiáng)一致性來換取系統(tǒng)的高可用性,系統(tǒng)往往只需要保證“最終一致性”,只要這個最終時間是在用戶可以接受的范圍內(nèi)即可。
- 在很多場景中,我們?yōu)榱吮WC數(shù)據(jù)的最終一致性,需要很多的技術(shù)方案來支持,比如分布式事務(wù)、分布式鎖等。有的時候,我們需要保證一個方法在同一時間內(nèi)只能被同一個線程執(zhí)行。在單機(jī)環(huán)境中,Java中其實(shí)提供了很多并發(fā)處理相關(guān)的API,但是這些API在分布式場景中就無能為力了。也就是說單純的Java Api并不能提供分布式鎖的能力。所以針對分布式鎖的實(shí)現(xiàn)目前有多種方案:
1、基于數(shù)據(jù)庫實(shí)現(xiàn)分布式鎖
2、基于緩存(redis,memcached)實(shí)現(xiàn)分布式鎖
3、基于Zookeeper實(shí)現(xiàn)分布式鎖
4、在分析這幾種實(shí)現(xiàn)方案之前我們先來想一下,我們需要的分布式鎖應(yīng)該是怎么樣的?(這里以方法鎖為例,資源鎖同理)
可以保證在分布式部署的應(yīng)用集群中,同一個方法在同一時間只能被一臺機(jī)器上的一個線程執(zhí)行。
- 這把鎖要是一把可重入鎖(避免死鎖)
- 這把鎖最好是一把阻塞鎖(根據(jù)業(yè)務(wù)需求考慮要不要這條)
- 有高可用的獲取鎖和釋放鎖功能
- 獲取鎖和釋放鎖的性能要好
悲觀鎖-數(shù)據(jù)庫鎖
借助數(shù)據(jù)中自帶的鎖來實(shí)現(xiàn)分布式的鎖
public boolean lock(){ ? ? connection.setAutoCommit(false) ? ? while(true){ ? ? ? ? try{ ? ? ? ? ? ? result = select * from methodLock where method_name=xxx for update; ? ? ? ? ? ? if(result==null){ ? ? ? ? ? ? ? ? return true; ? ? ? ? ? ? } ? ? ? ? }catch(Exception e){ ? ? ? ? ? } ? ? ? ? sleep(1000); ? ? } ? ? return false; }
在查詢語句后面增加for update,數(shù)據(jù)庫會在查詢過程中給數(shù)據(jù)庫表增加排他鎖。當(dāng)某條記錄被加上排他鎖之后,其他線程無法再在該行記錄上增加排他鎖。
我們可以認(rèn)為獲得排它鎖的線程即可獲得分布式鎖,當(dāng)獲取到鎖之后,可以執(zhí)行方法的業(yè)務(wù)邏輯,執(zhí)行完方法之后,再通過以下方法解鎖:
public void unlock(){ ? ? connection.commit(); }
通過connection.commit()操作來釋放鎖。
這種方法可以有效的解決上面提到的無法釋放鎖和阻塞鎖的問題。
阻塞鎖,for update語句會在執(zhí)行成功后立即返回,在執(zhí)行失敗時一直處于阻塞狀態(tài),直到成功。
鎖定之后服務(wù)宕機(jī),無法釋放,使用這種方式,服務(wù)宕機(jī)之后數(shù)據(jù)庫會自己把鎖釋放掉。
但是還是無法直接解決數(shù)據(jù)庫單點(diǎn)和可重入問題。
悲觀鎖-緩存鎖
相比較于基于數(shù)據(jù)庫實(shí)現(xiàn)分布式鎖的方案來說,基于緩存來實(shí)現(xiàn)在性能方面會表現(xiàn)的更好一點(diǎn)。而且很多緩存是可以集群部署的,可以解決單點(diǎn)問題。
redis2.6之后,SET命令支持超時和key存在檢查,這是一個原子操作
緩存鎖優(yōu)勢是性能出色,劣勢就是由于數(shù)據(jù)在內(nèi)存中,一旦緩存服務(wù)宕機(jī),鎖數(shù)據(jù)就丟失了。像redis自帶復(fù)制功能,可以對數(shù)據(jù)可靠性有一定的保證,但是由于復(fù)制也是異步完成的,因此依然可能出現(xiàn)master節(jié)點(diǎn)寫入鎖數(shù)據(jù)而未同步到slave節(jié)點(diǎn)的時候宕機(jī),鎖數(shù)據(jù)丟失問題。
分布式鎖—zookeeper
基于zookeeper臨時有序節(jié)點(diǎn)可以實(shí)現(xiàn)的分布式鎖。大致思想即為:每個客戶端對某個方法加鎖時,在zookeeper上的與該方法對應(yīng)的指定節(jié)點(diǎn)的目錄下,生成一個唯一的瞬時有序節(jié)點(diǎn)。 判斷是否獲取鎖的方式很簡單,只需要判斷有序節(jié)點(diǎn)中序號最小的一個。 當(dāng)釋放鎖的時候,只需將這個瞬時節(jié)點(diǎn)刪除即可。同時,其可以避免服務(wù)宕機(jī)導(dǎo)致的鎖無法釋放,而產(chǎn)生的死鎖問題。
來看下Zookeeper能不能解決前面提到的問題。
- 鎖無法釋放:使用Zookeeper可以有效的解決鎖無法釋放的問題,因?yàn)樵趧?chuàng)建鎖的時候,客戶端會在ZK中創(chuàng)建一個臨時節(jié)點(diǎn),一旦客戶端獲取到鎖之后突然掛掉(Session連接斷開),那么這個臨時節(jié)點(diǎn)就會自動刪除掉。其他客戶端就可以再次獲得鎖。
- 非阻塞鎖:使用Zookeeper可以實(shí)現(xiàn)阻塞的鎖,客戶端可以通過在ZK中創(chuàng)建順序節(jié)點(diǎn),并且在節(jié)點(diǎn)上綁定監(jiān)聽器,一旦節(jié)點(diǎn)有變化,Zookeeper會通知客戶端,客戶端可以檢查自己創(chuàng)建的節(jié)點(diǎn)是不是當(dāng)前所有節(jié)點(diǎn)中序號最小的,如果是,那么自己就獲取到鎖,便可以執(zhí)行業(yè)務(wù)邏輯了。
- 不可重入:使用Zookeeper也可以有效的解決不可重入的問題,客戶端在創(chuàng)建節(jié)點(diǎn)的時候,把當(dāng)前客戶端的主機(jī)信息和線程信息直接寫入到節(jié)點(diǎn)中,下次想要獲取鎖的時候和當(dāng)前最小的節(jié)點(diǎn)中的數(shù)據(jù)比對一下就可以了。如果和自己的信息一樣,那么自己直接獲取到鎖,如果不一樣就再創(chuàng)建一個臨時的順序節(jié)點(diǎn),參與排隊(duì)。
- 單點(diǎn)問題:使用Zookeeper可以有效的解決單點(diǎn)問題,ZK是集群部署的,只要集群中有半數(shù)以上的機(jī)器存活,就可以對外提供服務(wù)。
以上為個人經(jīng)驗(yàn),希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
El表達(dá)式使用問題javax.el.ELException:Failed to parse the expression
今天小編就為大家分享一篇關(guān)于Jsp El表達(dá)式使用問題javax.el.ELException:Failed to parse the expression的解決方式,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧2018-12-12利用數(shù)組實(shí)現(xiàn)棧(Java實(shí)現(xiàn))
這篇文章主要為大家詳細(xì)介紹了利用數(shù)組實(shí)現(xiàn)棧,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-09-09基于SpringBoot實(shí)現(xiàn)代碼在線運(yùn)行工具
這篇文章主要介紹了如何利用SpringBoot實(shí)現(xiàn)簡單的代碼在線運(yùn)行工具(類似于菜鳥工具),文中的示例代碼講解詳細(xì),需要的可以參考一下2022-06-06spring mvc 實(shí)現(xiàn)獲取后端傳遞的值操作示例
這篇文章主要介紹了spring mvc 實(shí)現(xiàn)獲取后端傳遞的值操作,結(jié)合實(shí)例形式詳細(xì)分析了spring mvc使用JSTL 方法獲取后端傳遞的值相關(guān)操作技巧2019-11-11springMVC+jersey實(shí)現(xiàn)跨服務(wù)器文件上傳
這篇文章主要介紹了springMVC+jersey實(shí)現(xiàn)跨服務(wù)器文件上傳,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-08-08