Eureka讀時(shí)加寫鎖,寫時(shí)加讀鎖,到底是故意為之還是一個(gè)bug?

在對(duì)于讀寫鎖的認(rèn)識(shí)當(dāng)中,我們都認(rèn)為讀時(shí)加讀鎖,寫時(shí)加寫鎖來保證讀寫和寫寫互斥,從而達(dá)到讀寫安全的目的。但是就在我翻Eureka源碼的時(shí)候,發(fā)現(xiàn)Eureka在使用讀寫鎖時(shí)竟然是在讀時(shí)加寫鎖,寫時(shí)加讀鎖,這波操作屬實(shí)震驚到了我,Eureka到底是故意為之還是一個(gè)bug?于是我就花了點(diǎn)時(shí)間研究了一下Eureka的這波操作。

Eureka服務(wù)注冊實(shí)現(xiàn)類

眾所周知,Eureka作為一個(gè)服務(wù)注冊中心,肯定會(huì)涉及到服務(wù)實(shí)例的注冊和發(fā)現(xiàn),從而肯定會(huì)有服務(wù)實(shí)例寫操作和讀操作,這是每個(gè)注冊中心最基本也是最核心的功能。

image.png

AbstractInstanceRegistry

如上圖,AbstractInstanceRegistry是注冊中心的服務(wù)注冊核心實(shí)現(xiàn)類,這里面保存了服務(wù)實(shí)例的數(shù)據(jù),封裝了對(duì)于服務(wù)實(shí)例注冊、下線、讀取等核心方法。

這里講解一下這個(gè)類比較重要的成員變量

服務(wù)注冊表

private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry= new ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>();

注冊表就是存儲(chǔ)的服務(wù)實(shí)例的信息。Eureka是使用ConcurrentHashMap來進(jìn)行保存的。鍵值是服務(wù)的名稱,值為服務(wù)的每個(gè)具體的實(shí)例id和實(shí)例數(shù)據(jù)的映射,所以也是一個(gè)Map數(shù)據(jù)結(jié)構(gòu)。InstanceInfo就是每個(gè)服務(wù)實(shí)例的數(shù)據(jù)的封裝對(duì)象。
服務(wù)的上線、下線、讀取其實(shí)就是從注冊表中讀寫數(shù)據(jù)。
最近變動(dòng)的實(shí)例隊(duì)列

private ConcurrentLinkedQueue<RecentlyChangedItem> recentlyChangedQueue = new ConcurrentLinkedQueue<>();

recentlyChangedQueue保存了最近變動(dòng)的服務(wù)實(shí)例的信息。如果有服務(wù)實(shí)例的變動(dòng)發(fā)生,就會(huì)將這個(gè)服務(wù)實(shí)例封裝到RecentlyChangedItem中,存到recentlyChangedQueue中。

什么叫服務(wù)實(shí)例發(fā)生了變動(dòng)。舉個(gè)例子,比如說,有個(gè)服務(wù)實(shí)例來注冊了,這個(gè)新添加的實(shí)例就是變動(dòng)的實(shí)例。

所以服務(wù)注冊這個(gè)操作就會(huì)有兩步操作,首先會(huì)往注冊表中添加這個(gè)實(shí)例的信息,其次會(huì)給這個(gè)實(shí)例標(biāo)記為新添加的,然后封裝到RecentlyChangedItem中,存到recentlyChangedQueue中。

image.png

新增

同樣的,服務(wù)實(shí)例狀態(tài)的修改、刪除(服務(wù)實(shí)例下線)不僅會(huì)操作注冊表,同樣也會(huì)進(jìn)行標(biāo)記,封裝成一個(gè)RecentlyChangedItem并添加到recentlyChangedQueue中。

image.png

修改

image.png

下線

所以從這分析也可以看出,注冊表的寫操作同時(shí)也會(huì)往recentlyChangedQueue中寫一條數(shù)據(jù),這句話很重要。

后面本文提到的注冊表的寫操作都包含對(duì)recentlyChangedQueue的寫操作。

讀寫鎖

下線

所以從這分析也可以看出,注冊表的寫操作同時(shí)也會(huì)往recentlyChangedQueue中寫一條數(shù)據(jù),這句話很重要。

后面本文提到的注冊表的寫操作都包含對(duì)recentlyChangedQueue的寫操作。

讀寫鎖

private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private final Lock read = readWriteLock.readLock();
private final Lock write = readWriteLock.writeLock();

讀寫鎖就不用說了,JDK提供的實(shí)現(xiàn)。

讀寫鎖的加鎖場景

上面說完了AbstractInstanceRegistry比較重要的成員變量,其中就有一個(gè)讀寫鎖,也是本文的主題,所以接下來看看哪些操作加讀鎖,哪些操作加寫鎖。

加讀鎖的場景

1、服務(wù)注冊
image.png

register

服務(wù)注冊就是在注冊表中添加一個(gè)服務(wù)實(shí)例的信息,加讀鎖。

2、服務(wù)下線
image.png

cancel和internalCancel

服務(wù)下線就是在注冊表刪除這個(gè)服務(wù)實(shí)例的信息,服務(wù)下線的方法最后是調(diào)用internalCancel實(shí)現(xiàn)的,而internalCancel是加的讀鎖,所以服務(wù)實(shí)例下線的時(shí)候加了讀鎖。

3、服務(wù)驅(qū)逐

什么叫服務(wù)驅(qū)逐,很簡單,就是服務(wù)端會(huì)定時(shí)檢查每個(gè)服務(wù)實(shí)例是否有向服務(wù)端發(fā)送心跳,如果服務(wù)端超過一定時(shí)間沒有接收到服務(wù)實(shí)例的心跳信息,那么就會(huì)認(rèn)為這個(gè)服務(wù)實(shí)例不可用,就會(huì)自動(dòng)將這個(gè)服務(wù)實(shí)例從注冊表刪除,這就是叫服務(wù)驅(qū)逐。

服務(wù)驅(qū)逐是通過evict方法實(shí)現(xiàn)的,這個(gè)方法最終也是調(diào)用服務(wù)下線internalCancel方法來實(shí)現(xiàn)驅(qū)逐的。

image.png

所以服務(wù)驅(qū)逐,其實(shí)也是加讀鎖的,因?yàn)樽詈笫钦{(diào)用internalCancel方法來實(shí)現(xiàn)的,而internalCancel方法就是加的讀鎖。

4、更新服務(wù)狀態(tài)
image.png

服務(wù)實(shí)例的狀態(tài)變動(dòng)了,進(jìn)行更新操作,也是加的讀鎖

5、刪除服務(wù)狀態(tài)
image.png

將服務(wù)的狀態(tài)刪了,也是加的讀鎖。

這里都是對(duì)于注冊表的寫操作,所以進(jìn)行這些操作的同時(shí)也會(huì)往recentlyChangedQueue中寫一條數(shù)據(jù),只不過方法太長,代碼太多,這里就沒有截出來。

加寫鎖的場景

獲取增量的服務(wù)實(shí)例的信息。

image.png

getApplicationDeltasFromMultipleRegions

所謂的增量信息,就是返回最近有變動(dòng)的服務(wù)實(shí)例,而recentlyChangedQueue剛剛好保存了最近的服務(wù)實(shí)例的信息,所以這個(gè)方法的實(shí)現(xiàn)就是遍歷recentlyChangedQueue,取出最近有變動(dòng)的實(shí)例,返回。所以保存最近變動(dòng)的實(shí)例,其實(shí)是為了增量拉取做準(zhǔn)備的。

加鎖總結(jié)

這里我總結(jié)一下讀鎖和寫鎖的加鎖場景:

  • 加讀鎖: 服務(wù)注冊、服務(wù)下線、服務(wù)驅(qū)逐、服務(wù)狀態(tài)的更新和刪除
  • 加寫鎖:獲取增量的服務(wù)實(shí)例的信息

讀寫鎖的加鎖疑問

上一節(jié)講了Eureka中加讀鎖和寫鎖的場景,有細(xì)心的小伙伴可能會(huì)有疑問,加讀鎖的場景主要涉及到服務(wù)注冊表的增刪操作,也就是寫操作;而加寫鎖的場景是一個(gè)讀的操作。

這不是很奇怪么,不按套路出牌啊,別人都是寫時(shí)加寫鎖,讀時(shí)加讀鎖,Eureka剛好反過來,屬實(shí)是真的會(huì)玩。

image.png

寫的時(shí)候加的讀鎖,那么就說明可以同時(shí)寫,那會(huì)不會(huì)有線程安全問題呢?

答案是不會(huì)有安全問題。

我們以一個(gè)服務(wù)注冊為例。一個(gè)服務(wù)注冊,涉及到注冊表的寫操作和recentlyChangedQueue的寫操作。

注冊表本身就是一個(gè)ConcurrentHashMap,線程安全的map,注冊表的值的Map數(shù)據(jù)結(jié)構(gòu),其實(shí)也是一個(gè)ConcurrentHashMap,如圖。

image.png

通過源碼可以發(fā)現(xiàn),其實(shí)也是放入的值也是一個(gè)ConcurrentHashMap,所以注冊表本身就是線程安全的,所以對(duì)于注冊表的寫操作,本身就是安全的。

再來看一下對(duì)于recentlyChangedQueue,它本身就是一個(gè)ConcurrentLinkedQueue,并發(fā)安全的隊(duì)列,也是線程安全的。

所以單獨(dú)對(duì)注冊表和recentlyChangedQueue的操作,其實(shí)是線程安全的。

到這里更加迷糊了,本身就是線程安全的,為什么要加鎖呢,而且對(duì)于寫操作,還加的是讀鎖,這就導(dǎo)致可以有很多線程同時(shí)去寫,對(duì)于寫來說,相當(dāng)加鎖加了個(gè)寂寞。

帶著疑惑,接著往下看。

Eureka服務(wù)實(shí)例的拉取方式和hash對(duì)比機(jī)制

拉取方式

Eureka作為一個(gè)注冊中心,客戶端肯定需要知道服務(wù)端道理存了哪些服務(wù)實(shí)例吧,所以就涉及到了服務(wù)的發(fā)現(xiàn),從而涉及到了客戶端跟服務(wù)端數(shù)據(jù)的交互方式,pull還是push。如果有不清楚pull和push的機(jī)制,可以看一下RocketMQ的push消費(fèi)方式實(shí)現(xiàn)的太聰明了這篇文章,里面有交代什么是pull還是push。

那么Eureka到底是pull還是push模式呢?這里我就不再賣關(guān)子了,其實(shí)是一種pull模式,也就是說客戶端會(huì)定期從服務(wù)端拉取服務(wù)實(shí)例的數(shù)據(jù)。并且Eureka提供了兩種拉取方式,全量和增量。

1、全量

全量其實(shí)很好理解,就是拉取注冊表所有的數(shù)據(jù)。

全量一般發(fā)生在客戶端啟動(dòng)之后第一次獲取注冊表的信息的時(shí)候,就會(huì)全量拉取注冊表。還有一種場景也會(huì)全量拉取,后面會(huì)說。

2、增量

增量,前面在說加寫鎖的時(shí)候提到了,就是獲取最近發(fā)生變化的實(shí)例的信息,也就是recentlyChangedQueue里面的數(shù)據(jù)。

增量相比于全量拉取的好處就是可以減少資源的浪費(fèi),假如全量拉取的時(shí)候數(shù)據(jù)壓根就沒有變動(dòng),那么白白浪費(fèi)網(wǎng)絡(luò)資源;但是如果是增量的話,數(shù)據(jù)沒有變動(dòng),那么就沒有增量信息,就不會(huì)有資源的浪費(fèi)。

在客戶端第一次啟動(dòng)的全量拉取之后,定時(shí)任務(wù)每次拉取的就是增量數(shù)據(jù)。

增量拉取的hash對(duì)比機(jī)制

如果是增量拉取,客戶端在拉取到增量數(shù)據(jù)之后會(huì)多干兩件事:

  • 會(huì)將增量信息跟本地緩存的服務(wù)實(shí)例進(jìn)行合并
  • 判斷合并后的服務(wù)的數(shù)據(jù)跟服務(wù)端的數(shù)據(jù)是不是一樣

那么如何去判定客戶端的數(shù)據(jù)跟服務(wù)端的數(shù)據(jù)是不是一樣呢?

Eureka是通過一種hash對(duì)比的機(jī)制來實(shí)現(xiàn)的。

當(dāng)服務(wù)端生成增量信息的時(shí)候,同時(shí)會(huì)生成一個(gè)代表這一刻全部服務(wù)實(shí)例的hash值,設(shè)置到返回值中,代碼如下

image.png

所以增量信息返回的數(shù)據(jù)有兩部分,一部分是變動(dòng)的實(shí)例的信息,還有就是這一刻服務(wù)端所有的實(shí)例信息生成的hash值。

當(dāng)客戶端拉取到增量信息并跟本地原有的老的服務(wù)實(shí)例合并完增量信息之后,客戶端會(huì)用相同的方式計(jì)算出合并后服務(wù)實(shí)例的hash值,然后會(huì)跟服務(wù)端返回的hash值進(jìn)行對(duì)比,如果一樣,說明本次增量拉取之后,客戶端緩存的服務(wù)實(shí)例跟服務(wù)端一樣,如果不一樣,說明兩邊的服務(wù)實(shí)例的數(shù)據(jù)不一樣。

這就是hash對(duì)比機(jī)制,通過這個(gè)機(jī)制來判斷增量拉取的時(shí)候兩邊的服務(wù)實(shí)例數(shù)據(jù)是不是一樣。

image.png

hash對(duì)比

但是,如果發(fā)現(xiàn)了不一樣,那么此時(shí)客戶端就會(huì)重新從服務(wù)端全量拉取一次服務(wù)數(shù)據(jù),然后將該次全量拉取的數(shù)據(jù)設(shè)置到本地的緩存中,所以前面說的還有一種全量拉取的場景就在這里,源碼如下

image.png

重新全量拉取

讀寫鎖的使用揭秘

前面說了增量拉取和hash對(duì)比機(jī)制,此時(shí)我們再回過頭仔細(xì)分析一下增量信息封裝的兩步操作:

  • 第一步遍歷recentlyChangedQueue,封裝增量的實(shí)例信息
  • 第二步生成所有服務(wù)實(shí)例數(shù)據(jù)對(duì)應(yīng)的hash值,設(shè)置到增量信息返回值中

為什么要加鎖

假設(shè)不加鎖,那么對(duì)于注冊表和recentlyChangedQueue讀寫都可以同時(shí)進(jìn)行,那么會(huì)出現(xiàn)這么一種情況

當(dāng)獲取增量信息的時(shí)候,在第一步遍歷recentlyChangedQueue時(shí)有2個(gè)變動(dòng)的實(shí)例,注冊表總共有5個(gè)實(shí)例

當(dāng)recentlyChangedQueue遍歷完之后,還沒有進(jìn)行第二步計(jì)算hash值時(shí),此時(shí)有服務(wù)實(shí)例來注冊了,由于不加鎖,那么可以同時(shí)操作注冊表和recentlyChangedQueue,于是注冊成功之后注冊表數(shù)據(jù)就變成了6個(gè)實(shí)例,recentlyChangedQueue也會(huì)添加一條數(shù)據(jù)

但是因?yàn)閞ecentlyChangedQueue已經(jīng)遍歷完了,此時(shí)不會(huì)在遍歷了,那么剛注冊的這個(gè)實(shí)例在此次獲取增量數(shù)據(jù)時(shí)就獲取不到了,但是由于計(jì)算hash值是通過這一時(shí)刻所有的實(shí)例數(shù)據(jù)來計(jì)算,那么就會(huì)把這個(gè)新的實(shí)例計(jì)算進(jìn)去了。

這不完?duì)僮恿嗣?,增量信息沒有,但是全部實(shí)例數(shù)據(jù)的hash值有,那么就會(huì)導(dǎo)致客戶端在合并增量信息之后計(jì)算的hash值跟返回的hash值不一樣,就會(huì)導(dǎo)致再次全量拉取,白白浪費(fèi)了本次增量拉取操作。

所以一定要加鎖,保證在獲取增量數(shù)據(jù)時(shí),不能對(duì)注冊表進(jìn)行改動(dòng)。

為什么加讀寫鎖而不是synchronized鎖

這個(gè)其實(shí)跟Eureka沒多大關(guān)系,主要是讀寫鎖和synchronized鎖特性決定的。synchronized會(huì)使得所有的操作都是串行化,雖然也能解決問題,但是也會(huì)導(dǎo)致并發(fā)性能降低。

為什么寫時(shí)加讀鎖,讀時(shí)加寫鎖

現(xiàn)在我們轉(zhuǎn)過來,按照正常的操作,服務(wù)注冊等寫操作加寫鎖,獲取增量的時(shí)候加讀鎖,那么可以不可呢?

其實(shí)也是可以的,因?yàn)檫@樣注冊表寫操作和獲取的增量信息讀操作還是互斥的,那么獲取的增量信息還是對(duì)的。

那么為什么Eureka要反過來?

寫(鎖)寫(鎖)是互斥的。如果注冊表寫操作加了寫鎖,那么所有的服務(wù)注冊、下線、狀態(tài)更新都會(huì)串行執(zhí)行,并發(fā)性能就會(huì)降低,所以對(duì)于注冊表寫操作加了讀鎖,可以提高寫的性能。

但是,如果獲取的增量讀的操作加了寫鎖,那豈不是讀操作都串行化了,那么讀的性能不是會(huì)變低么?而且注冊中心其實(shí)是一個(gè)讀多寫少的場景,為了提升寫的性能,浪費(fèi)讀的性能不是得不償失么?

哈哈,其實(shí)對(duì)于這個(gè)讀操作性能低的問題,Eureka也進(jìn)行了優(yōu)化,那就是通過緩存來優(yōu)化了這個(gè)讀的性能問題,讀的時(shí)候先讀緩存,緩存沒有才會(huì)真正調(diào)用獲取增量的方法來讀取增量的信息,所以最后真正走到獲取增量信息的方法,請(qǐng)求量很低。

image.png

ResponseCacheImpl

ResponseCacheImpl內(nèi)部封裝了緩存的操作,因?yàn)椴皇潜疚牡闹攸c(diǎn),這里就不討論了。

總結(jié)

所以,通過上面的一步一步分析,終于知道了Eureka讀寫鎖的加鎖場景、為什么要加讀寫鎖以及為什么寫時(shí)加讀鎖,讀時(shí)加寫鎖。這里我再總結(jié)一下:

為什么加讀寫鎖

是為了保證獲取增量信息的讀操作和注冊表的寫操作互斥,避免由于并發(fā)問題導(dǎo)致獲取到的增量信息和實(shí)際注冊表的數(shù)據(jù)對(duì)不上,從而引發(fā)客戶端的多余的一次全量拉取的操作。

為什么寫時(shí)加讀鎖,讀時(shí)加寫鎖

其實(shí)是為了提升寫的性能,而讀由于有緩存的原因,真正走到獲取增量信息的請(qǐng)求很少,所以讀的時(shí)候就算加寫鎖,對(duì)于讀的性能也沒有多大的影響。

從Eureka對(duì)于讀寫鎖的使用也可以看出,一個(gè)技術(shù)什么時(shí)候用,如何使用都是根據(jù)具體的場景來判斷的,不能要一概而論。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 整體架構(gòu)圖: Eureka結(jié)構(gòu)圖: Eureka原理:Eureka是Netflix開源的一款提供服務(wù)注冊和發(fā)現(xiàn)的產(chǎn)...
    xuxw閱讀 702評(píng)論 0 0
  • Eureka是什么? eureka是Netflix的子模塊之一,也是一個(gè)核心的模塊,eureka里有2個(gè)組件,一個(gè)...
    段永平閱讀 1,643評(píng)論 0 0
  • Eureka Client為了簡化開發(fā)人員的開發(fā)工作,將很多與Eureka Server交互的工作隱藏起來,自主完...
    JBryan閱讀 246評(píng)論 0 0
  • 一、客戶端工作流程 1.1、初始化階段 1、讀取與server交互的信息,封裝成EurekaClientConfi...
    潛水艇_閱讀 164評(píng)論 0 0
  • Eureka Server作為一個(gè)開箱即用的服務(wù)中心,主要有以下功能: 服務(wù)注冊; 接收服務(wù)心跳(續(xù)租); 服務(wù)剔...
    靈08_1024閱讀 581評(píng)論 0 0

友情鏈接更多精彩內(nèi)容