12 張圖 | 硬剛了一波,三層緩存架構(gòu)
一、前言
上一講我們講到了 Eureka 注冊(cè)中心的 Server 端有三級(jí)緩存來(lái)保存注冊(cè)信息,可以利用緩存的快速讀取來(lái)提高系統(tǒng)性能。我們?cè)賮?lái)細(xì)看下:
一級(jí)緩存:只讀緩存 readOnlyCacheMap,數(shù)據(jù)結(jié)構(gòu) ConcurrentHashMap。相當(dāng)于數(shù)據(jù)庫(kù)。
二級(jí)緩存:讀寫(xiě)緩存 readOnlyCacheMap,Guava Cache。相當(dāng)于 Redis 主從架構(gòu)中主節(jié)點(diǎn),既可以進(jìn)行讀也可以進(jìn)行寫(xiě)。
三級(jí)緩存:本地注冊(cè)表 registry,數(shù)據(jù)結(jié)構(gòu) ConcurentHashMap。相當(dāng)于 Redis 主從架構(gòu)的從節(jié)點(diǎn),只負(fù)責(zé)讀。
看圖更清晰,如下圖所示:
三種緩存
另外 ConcurrenthashMap 也是一種 map 結(jié)構(gòu),也就是以鍵值對(duì)的方式進(jìn)行存儲(chǔ),如下圖所示:
Map 結(jié)構(gòu)
本篇悟空哥會(huì)帶著大家來(lái)看下 Eureka 的緩存架構(gòu)是怎么樣,通過(guò)學(xué)習(xí)這篇,我們也可以借鑒 Eureka 的緩存設(shè)計(jì)思想,將其運(yùn)用到項(xiàng)目當(dāng)中。
二、引發(fā)的幾個(gè)思考
我們?cè)賮?lái)看下 Eureka 源碼,其實(shí)不難看懂,下面會(huì)做解釋。
- 默認(rèn)會(huì)先從只讀緩存里面找。
- 沒(méi)有的話(huà),再?gòu)淖x寫(xiě)緩存里面找。
- 找到了的話(huà)就更新只讀緩存,并返回找到的緩存。
- 還找不到的話(huà),就從本地緩存 registry 中加載進(jìn)來(lái)。
帶來(lái)了三個(gè)問(wèn)題:
(1)三級(jí)緩存數(shù)據(jù)怎么來(lái)的?
(2)緩存數(shù)據(jù)如何更新的?
(3)緩存如何過(guò)期?
三、本地緩存
我們先來(lái)看下本地緩存 registry,它是一種定義為 ConcurrentHashMap 的數(shù)據(jù)結(jié)構(gòu),之前也詳細(xì)講解過(guò)。
當(dāng)客戶(hù)端發(fā)起注冊(cè)請(qǐng)求的時(shí)候,就會(huì)把注冊(cè)信息放到 registry 中。如下代碼所示:
- registry.putIfAbsent(app)
putIfAbsent 表示如果存在重復(fù)的 key,就不會(huì)放入值,如果傳入的 key 對(duì)應(yīng)的 value 已經(jīng)存在,就返回存在的 value,不進(jìn)行替換。
經(jīng)過(guò) putIfAbsent 操作就把客戶(hù)端的注冊(cè)信息放到 registry 中了。
我們?cè)賮?lái)看下其中的一種緩存結(jié)構(gòu):讀寫(xiě)緩存。
四、讀寫(xiě)緩存
讀寫(xiě)緩存,顧名思義,就是既可以進(jìn)行讀,也可以進(jìn)行寫(xiě)的緩存。讀主要是給只讀緩存來(lái)讀取的。寫(xiě)主要是將緩存更新到自己的 Map 中。
下面分別從寫(xiě)緩存的原理、寫(xiě)緩存的源碼、過(guò)期時(shí)機(jī)的原理、過(guò)期時(shí)機(jī)的源碼幾個(gè)方面來(lái)分別解答。
4.1 寫(xiě)緩存的原理和源碼
我開(kāi)始以為當(dāng)我們讀緩存讀不到的時(shí)候,就會(huì)去數(shù)據(jù)庫(kù)查了。找了半天,沒(méi)找到讀數(shù)據(jù)庫(kù)的地方。
然后我就用 IDEA 工具查找 readOnlyCacheMap 被使用的地方,終于讓我找到了。
讀寫(xiě)緩存用的是 Guava Cache工具類(lèi),這篇不會(huì)深究。簡(jiǎn)單來(lái)說(shuō)就是當(dāng)訪(fǎng)問(wèn)讀寫(xiě)緩存時(shí),如果這個(gè) key 在緩存中不存在,則從本地去查,查到后再放回緩存。
然后又實(shí)現(xiàn)抽象方法 load(key),這個(gè)方法的作用就是當(dāng)讀寫(xiě)緩存中沒(méi)有,則從本地 registry 緩存中拿。
讀寫(xiě)緩存過(guò)期的時(shí)候其實(shí)分兩種:定時(shí)過(guò)期和實(shí)時(shí)過(guò)期。由于上面的源碼已經(jīng)定義了定時(shí)過(guò)期的時(shí)間間隔,所以我們先來(lái)看定時(shí)過(guò)期。
4.2 定時(shí)過(guò)期
當(dāng)構(gòu)建這個(gè)讀寫(xiě)緩存時(shí),就會(huì)定義間隔多久過(guò)期整個(gè)讀寫(xiě)緩存。如下代碼所示,180 s 會(huì)定時(shí)過(guò)期讀寫(xiě)緩存。
- expireAfterWrite(180s)
4.3 實(shí)時(shí)過(guò)期
當(dāng)有新的服務(wù)實(shí)例進(jìn)行注冊(cè)或者下線(xiàn)、發(fā)生故障時(shí),就會(huì)把這個(gè)對(duì)應(yīng)的服務(wù)實(shí)例的緩存給過(guò)期掉。
如下圖所示,最上面的是注冊(cè)中心,下面三個(gè)是服務(wù)實(shí)例。服務(wù)實(shí)例發(fā)生注冊(cè)、下線(xiàn)、發(fā)生故障,注冊(cè)中心都是可以感知到的,然后就會(huì)主動(dòng)過(guò)期讀寫(xiě)緩存對(duì)應(yīng)的服務(wù)實(shí)例。
4.4 實(shí)時(shí)過(guò)期源碼
從源碼層面我們?cè)賮?lái)看下讀寫(xiě)緩存過(guò)期的源碼。調(diào)用了 invalidateCache 方法,進(jìn)行過(guò)期。
文件路徑:com/netflix/eureka/registry/AbstractInstanceRegistry.java
五、只讀緩存
5.1 定時(shí)更新
只讀緩存 readOnlyCacheMap,有一個(gè)定時(shí)更新的機(jī)制,每隔 30 秒就會(huì)更新一次只讀緩存中的某些 key。
它其實(shí)是遍歷自己的所有注冊(cè)信息,然后和讀寫(xiě)緩存進(jìn)行比對(duì),如果注冊(cè)信息不一致,則替換為讀寫(xiě)緩存的數(shù)據(jù)。
源碼如下,有一個(gè)定時(shí)調(diào)度任務(wù),每隔 30 秒調(diào)度一次。
5.2 更新
另外當(dāng)客戶(hù)端獲取注冊(cè)信息時(shí),也會(huì)先讀只讀緩存,如果只讀緩存中沒(méi)有,則會(huì)從讀寫(xiě)緩存中找,找到后就放到只讀緩存中。如果讀寫(xiě)緩存中沒(méi)有,則從本地注冊(cè)表 registry 中加載到讀寫(xiě)緩存中,然后將注冊(cè)表信息返回。
這里大家是否有個(gè)疑問(wèn):既然這個(gè)緩存叫做只讀緩存,怎么還能被更新,不應(yīng)該是不變的嗎?
其實(shí)這里的不變是相對(duì)于客戶(hù)端來(lái)說(shuō)的,客戶(hù)端獲取注冊(cè)表信息時(shí),最開(kāi)始訪(fǎng)問(wèn)的就是只讀緩存,類(lèi)似數(shù)據(jù)庫(kù)或 Redis 的主從架構(gòu),主負(fù)責(zé)讀寫(xiě),從負(fù)責(zé)讀。然后系統(tǒng)內(nèi)部會(huì)把主節(jié)點(diǎn)的信息同步給從節(jié)點(diǎn)。大家明白了嗎?
六、緩存相關(guān)配置
下面我們來(lái)看下 Eureka Server 對(duì)于緩存有哪些配置呢?
6.1 是否開(kāi)啟只讀緩存
eureka.server.useReadOnlyResponseCache
當(dāng)客戶(hù)端獲取注冊(cè)信息時(shí),是否先從只讀緩存獲取。如果為 false,則直接從讀寫(xiě)緩存獲取。默認(rèn)為 true。
6.2 定時(shí)更新只讀緩存的間隔時(shí)間
eureka.server.responseCacheUpdateIntervalMs
默認(rèn)每隔 30 秒將讀寫(xiě)緩存更新的緩存同步到只讀緩存。
七、緩存帶來(lái)的問(wèn)題
三級(jí)緩存看似可以帶來(lái)性能的提升。但是也會(huì)引入其他問(wèn)題,比如緩存不一致問(wèn)題。
只讀緩存每隔 30s 才會(huì)刷新一次,和讀寫(xiě)緩存會(huì)造成數(shù)據(jù)的不一致,客戶(hù)端在 30s 內(nèi)獲取的注冊(cè)表信息是滯后的。
當(dāng)使用 Eureka 集群時(shí),這種緩存不一致的問(wèn)題會(huì)更明顯,不同的節(jié)點(diǎn)之間也會(huì)出現(xiàn)只讀緩存的數(shù)據(jù)不一致,所以 Eureka 只能保證高可用,并不能保證強(qiáng)一致性,也就是保證了 AP,不保證 CP,另外我們可以選用強(qiáng)一致性的注冊(cè)中心,比如 Zookeeper、Nacos,這是后續(xù)要講的內(nèi)容了。
如何緩解不一致的問(wèn)題呢?
(1)在服務(wù)端,我們可以設(shè)置更新只讀緩存的時(shí)間間隔,默認(rèn)是 30 秒,縮短一點(diǎn),比如 15 秒,頻率太高,可能對(duì) Eureka 造成性能問(wèn)題。
(2)服務(wù)端,我們也可以考慮關(guān)閉從只讀緩存讀注冊(cè)表信息,Eureka Client 直接從讀寫(xiě)緩存讀取。
八、總結(jié)
Eureka Server 注冊(cè)表三級(jí)緩存架構(gòu)
本篇學(xué)習(xí)了 Eureka 注冊(cè)中心 Server 端的三層緩存架構(gòu),分為 registry、readOnlyCacheMap、readWriteCacheMap,用來(lái)保存服務(wù)注冊(cè)信息。
- 默認(rèn)情況下,每隔 30 秒從讀寫(xiě)緩存將注冊(cè)信息更新到只讀緩存。
- 默認(rèn)情況下,客戶(hù)端讀取注冊(cè)表時(shí),先從只讀緩存讀,如果沒(méi)有,則從讀寫(xiě)緩存中讀取,如果還是沒(méi)有,則從本地注冊(cè)表 registry 讀取。
- 默認(rèn)情況下,每隔 180 秒定時(shí)過(guò)期讀寫(xiě)緩存。
- 服務(wù)實(shí)例注冊(cè)、下線(xiàn)、故障時(shí),會(huì)實(shí)時(shí)過(guò)期讀寫(xiě)緩存。
- 引入了多級(jí)緩存,也會(huì)帶來(lái)緩存不一致的問(wèn)題。
參考資料:
www.passjava.cn
《微服務(wù)架構(gòu)深度解析》
Eureka 源碼