eureka(三)-注冊(cè)中心之多級(jí)緩存機(jī)制

Eureka源碼分析(2.1.4.Release)

首先源碼切忌一行一行debug,需先了解eureka主要功能后咙咽,再分析其功能如何實(shí)現(xiàn)洪燥。


image.png

大家一定有疑問(wèn),eureka(一)-功能介紹與客戶端之服務(wù)獲取分析了客戶端是通過(guò)發(fā)起restful請(qǐng)求給注冊(cè)中心來(lái)獲取服務(wù)列表的脐湾,那么注冊(cè)中心即eureka服務(wù)端的服務(wù)列表數(shù)據(jù)是如何存儲(chǔ)的臭笆?又是如何返回給客戶端的?

eureka服務(wù)端的相關(guān)bean初始化

從EurekaServerAutoConfiguration出發(fā)(為什么會(huì)加載該配置類(lèi)的bean秤掌?詳情請(qǐng)看springboot自動(dòng)裝載

image.png

重點(diǎn)分析PeerAwareInstanceRegistry和EurekaServerContext愁铺。
PeerAwareInstanceRegistry的實(shí)現(xiàn)類(lèi)為InstanceRegistry:

@Bean
    public PeerAwareInstanceRegistry peerAwareInstanceRegistry(
            ServerCodecs serverCodecs) {
        this.eurekaClient.getApplications(); // force initialization
        return new InstanceRegistry(this.eurekaServerConfig, this.eurekaClientConfig,
                serverCodecs, this.eurekaClient,
                this.instanceRegistryProperties.getExpectedNumberOfClientsSendingRenews(),
                this.instanceRegistryProperties.getDefaultOpenForTrafficCount());
    }

EurekaServerContext初始化,引入了上面提到的PeerAwareInstanceRegistry實(shí)例

@Bean
    public EurekaServerContext eurekaServerContext(ServerCodecs serverCodecs,
            PeerAwareInstanceRegistry registry, PeerEurekaNodes peerEurekaNodes) {
        return new DefaultEurekaServerContext(this.eurekaServerConfig, serverCodecs,
                registry, peerEurekaNodes, this.applicationInfoManager);
    }

再來(lái)看看EurekaServerContext上下文的初始化:
com.netflix.eureka.DefaultEurekaServerContext#initialize:

@PostConstruct
    @Override
    public void initialize() {
        logger.info("Initializing ...");
        //eureka服務(wù)端節(jié)點(diǎn)更新任務(wù)開(kāi)啟
        peerEurekaNodes.start();
        try {
            //注冊(cè)中心初始化
            registry.init(peerEurekaNodes);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        logger.info("Initialized");
    }

重點(diǎn)看看registry.init(peerEurekaNodes)的代碼邏輯
com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#init:

    @Override
    public void init(PeerEurekaNodes peerEurekaNodes) throws Exception {
        this.numberOfReplicationsLastMin.start();
        this.peerEurekaNodes = peerEurekaNodes;
        //重點(diǎn)看這里闻鉴,初始化response緩存
        initializedResponseCache();
        //開(kāi)啟續(xù)約閾值更新任務(wù)
        scheduleRenewalThresholdUpdateTask();
        initRemoteRegionRegistry();

        try {
            Monitors.registerObject(this);
        } catch (Throwable e) {
            logger.warn("Cannot register the JMX monitor for the InstanceRegistry :", e);
        }
    }

初始化流程圖如下:


image.png

response緩存結(jié)構(gòu)

終于到了本章的重點(diǎn)茵乱,接上文提到的initializedResponseCache()方法:初始化response緩存結(jié)構(gòu)。

@Override
    public synchronized void initializedResponseCache() {
        if (responseCache == null) {
            responseCache = new ResponseCacheImpl(serverConfig, serverCodecs, this);
        }
    }
ResponseCacheImpl(EurekaServerConfig serverConfig, ServerCodecs serverCodecs, AbstractInstanceRegistry registry) {
        this.serverConfig = serverConfig;
        this.serverCodecs = serverCodecs;
        //是否使用只讀模式的response緩存孟岛,默認(rèn)為true
        this.shouldUseReadOnlyResponseCache = serverConfig.shouldUseReadOnlyResponseCache();
        //上文初始化的InstanceRegistry
        this.registry = registry;
        //responseCacheUpdateIntervalMs=30*1000,默認(rèn)為30s
        long responseCacheUpdateIntervalMs = serverConfig.getResponseCacheUpdateIntervalMs();
        //讀寫(xiě)緩存map瓶竭,該map的結(jié)構(gòu)為google的guava cache,暫不了解其原理渠羞。從方法中可大概猜測(cè)其作用
        this.readWriteCacheMap =
                CacheBuilder.newBuilder().initialCapacity(serverConfig.getInitialCapacityOfResponseCache())
                        .expireAfterWrite(serverConfig.getResponseCacheAutoExpirationInSeconds(), TimeUnit.SECONDS)//寫(xiě)入后斤贰,默認(rèn)180s后過(guò)期
                        .removalListener(new RemovalListener<Key, Value>() {
                            @Override
                            public void onRemoval(RemovalNotification<Key, Value> notification) {
                                Key removedKey = notification.getKey();
                                if (removedKey.hasRegions()) {
                                    Key cloneWithNoRegions = removedKey.cloneWithoutRegions();
                                    regionSpecificKeys.remove(cloneWithNoRegions, removedKey);
                                }
                            }
                        })
                        //加載key的value值
                        .build(new CacheLoader<Key, Value>() {
                            @Override
                            public Value load(Key key) throws Exception {
                                if (key.hasRegions()) {
                                    Key cloneWithNoRegions = key.cloneWithoutRegions();
                                    regionSpecificKeys.put(cloneWithNoRegions, key);
                                }
                                //value值生成
                                Value value = generatePayload(key);
                                return value;
                            }
                        });
        //默認(rèn)為true
        if (shouldUseReadOnlyResponseCache) {
            //responseCacheUpdateIntervalMs=30,默認(rèn)每隔30s執(zhí)行一次getCacheUpdateTask()
            timer.schedule(getCacheUpdateTask(),
                    new Date(((System.currentTimeMillis() / responseCacheUpdateIntervalMs) * responseCacheUpdateIntervalMs)
                            + responseCacheUpdateIntervalMs),
                    responseCacheUpdateIntervalMs);
        }

……忽略下半部分代碼……

先來(lái)分析一下readWriteCacheMap的作用

  1. 寫(xiě)入180s后次询,元素過(guò)期荧恍。
  2. 通過(guò)generatePayload(key)生成value值。
    下面再來(lái)看看generatePayload(key)又是如何生成value的。如傳入key為“ALL_APPS”
    private Value generatePayload(Key key) {
        Stopwatch tracer = null;
        try {
            String payload;
            switch (key.getEntityType()) {
                case Application:
                    boolean isRemoteRegionRequested = key.hasRegions();
                    if (ALL_APPS.equals(key.getName())) {
                        if (isRemoteRegionRequested) {
                            tracer = serializeAllAppsWithRemoteRegionTimer.start();
                            payload = getPayLoad(key, registry.getApplicationsFromMultipleRegions(key.getRegions()));
                        } else {
                            tracer = serializeAllAppsTimer.start();
                            //debug模式下送巡,可知跑到這里獲取value值摹菠。
                            payload = getPayLoad(key, registry.getApplications());
                        }
                    } 
   ……忽略下部分代碼……
}

可知readWriteCacheMap的key是通過(guò)registry本地注冊(cè)表獲取到的(registry也是一個(gè)本地緩存)。即這里可以分析到骗爆,eureka有兩層緩存次氨,上層為讀寫(xiě)緩存map,底層為registry注冊(cè)表緩存摘投。

跳出到ResponseCacheImpl初始化中的getCacheUpdateTask方法煮寡,從字面意思是更新緩存,那么它具體的實(shí)現(xiàn)邏輯是什么呢谷朝?

private TimerTask getCacheUpdateTask() {
        return new TimerTask() {
            @Override
            public void run() {
                logger.debug("Updating the client cache from response cache");
                for (Key key : readOnlyCacheMap.keySet()) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Updating the client cache from response cache for key : {} {} {} {}",
                                key.getEntityType(), key.getName(), key.getVersion(), key.getType());
                    }
                    try {
                        CurrentRequestVersion.set(key.getVersion());
                        Value cacheValue = readWriteCacheMap.get(key);
                        Value currentCacheValue = readOnlyCacheMap.get(key);
                        if (cacheValue != currentCacheValue) {
                            readOnlyCacheMap.put(key, cacheValue);
                        }
                    } catch (Throwable th) {
                        logger.error("Error while updating the client cache from response cache for key {}", key.toStringCompact(), th);
                    }
                }
            }
        };
    }

遍歷只讀map(readOnlyCacheMap)中的key洲押,將readWriteMap對(duì)應(yīng)的value值賦值到只讀map里面,即readOnlyCacheMap定期從readWriteMap中更新value值。

response緩存初始化流程如下:


image.png

總結(jié):從這里可以分析到eureka注冊(cè)中心的response緩存一共有3層緩存圆凰,第一層為只讀緩存杈帐,第二層為讀寫(xiě)緩存,第三層為registry本地注冊(cè)表緩存专钉。只讀緩存每30s拉取讀寫(xiě)緩存的值挑童,讀寫(xiě)緩存寫(xiě)入180s后過(guò)期,如果要獲取的key沒(méi)有value值時(shí)跃须,則通過(guò)registry注冊(cè)表緩存獲取數(shù)據(jù)站叼。

response緩存結(jié)構(gòu)是如何實(shí)現(xiàn)讀功能的?

response緩存主要作用于客戶端與eureka注冊(cè)中心交互的時(shí)候菇民。
從客戶端向注冊(cè)中心獲取服務(wù)列表的功能中尽楔,可以分析出response緩存是如何實(shí)現(xiàn)讀功能的。
獲取服務(wù)列表時(shí)第练,服務(wù)端的運(yùn)行流程如下:


image.png
  1. 默認(rèn)讀取只讀map阔馋,如果只讀map沒(méi)有,則讀取讀寫(xiě)map娇掏,如果讀寫(xiě)map也沒(méi)有呕寝,就讀取registry本地注冊(cè)表緩存。
  2. registry本地注冊(cè)表存儲(chǔ)的是最新的服務(wù)列表數(shù)據(jù)婴梧。(registry的具體存儲(chǔ)邏輯暫不深究)

問(wèn)題:為什么這樣設(shè)計(jì)下梢?

這讓我想起主從數(shù)據(jù)庫(kù)的讀寫(xiě)分離,數(shù)據(jù)庫(kù)的讀寫(xiě)分離是為了分?jǐn)傊鲾?shù)據(jù)庫(kù)服務(wù)器的讀寫(xiě)壓力塞蹭。而eureka所設(shè)計(jì)的緩存級(jí)別無(wú)疑也是為了讀寫(xiě)分離孽江,因?yàn)樵趯?xiě)的時(shí)候,如ConcurrentHashmap會(huì)持有桶節(jié)點(diǎn)對(duì)象的鎖浮还,阻塞同一個(gè)桶的讀寫(xiě)線程竟坛。這樣設(shè)計(jì)的話,線程在寫(xiě)的時(shí)候钧舌,并不會(huì)影響讀操作担汤,避免了爭(zhēng)搶資源所帶來(lái)的壓力。

問(wèn)題:該緩存結(jié)構(gòu)如何保證最終一致性洼冻?
  1. 從只讀map中獲取key對(duì)應(yīng)的值崭歧,如果只讀map沒(méi)有value值的時(shí)候,會(huì)從讀寫(xiě)緩存里面獲取撞牢,而讀寫(xiě)緩存180s后過(guò)期率碾,所以,它又會(huì)從本地注冊(cè)表中獲取到最新的實(shí)例信息屋彪。
  2. 只讀map中會(huì)每30s遍歷所宰,將讀寫(xiě)map里面的key賦值到只讀map中。
問(wèn)題:如果有新的實(shí)例注冊(cè)畜挥,極端情況下難道要等讀寫(xiě)緩存的key仔粥,180s后過(guò)期,才能獲取到最新的服務(wù)列表數(shù)據(jù)嗎蟹但?即在實(shí)例有變化的時(shí)候躯泰,服務(wù)端又是如何實(shí)現(xiàn)的?

服務(wù)端接受客戶端注冊(cè)

帶著疑問(wèn)华糖,我們?cè)賮?lái)分析一下麦向,服務(wù)端是如何實(shí)現(xiàn)客戶端的注冊(cè)操作的骆膝。
具體流程如下:(可以根據(jù)流程所提及到的方法進(jìn)行分析馅袁,這里不把代碼貼出來(lái)了)

結(jié)論:在接受客戶端注冊(cè)的時(shí)候,服務(wù)端會(huì)將讀寫(xiě)緩存的key清掉念颈,30s后只讀緩存從讀寫(xiě)緩存拉取數(shù)據(jù)的時(shí)候兼搏,該服務(wù)列表獲取到的是最新的數(shù)據(jù)卵慰。如果客戶端下線,同樣地向族,讀寫(xiě)緩存也會(huì)被清除掉呵燕。所以極端情況,最長(zhǎng)30s后件相,客戶端才能獲取到最新的服務(wù)列表再扭。

優(yōu)點(diǎn):

  1. 盡可能保證內(nèi)存注冊(cè)表數(shù)據(jù)不會(huì)出現(xiàn)頻繁的讀寫(xiě)沖突
  2. 保證對(duì)eureka服務(wù)端的請(qǐng)求讀取的都是內(nèi)存,性能高夜矗。

在以后的開(kāi)發(fā)工作中泛范,面對(duì)頻繁的讀寫(xiě)資源爭(zhēng)搶的情況,也可以考慮采用多級(jí)緩存這種方案來(lái)設(shè)計(jì)系統(tǒng)紊撕。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末罢荡,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌区赵,老刑警劉巖惭缰,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異笼才,居然都是意外死亡漱受,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門(mén)骡送,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)昂羡,“玉大人,你說(shuō)我怎么就攤上這事摔踱∨跋龋” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵派敷,是天一觀的道長(zhǎng)蛹批。 經(jīng)常有香客問(wèn)我,道長(zhǎng)膀息,這世上最難降的妖魔是什么般眉? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮潜支,結(jié)果婚禮上甸赃,老公的妹妹穿的比我還像新娘。我一直安慰自己冗酿,他們只是感情好埠对,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著裁替,像睡著了一般项玛。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上弱判,一...
    開(kāi)封第一講書(shū)人閱讀 51,292評(píng)論 1 301
  • 那天襟沮,我揣著相機(jī)與錄音,去河邊找鬼昌腰。 笑死开伏,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的遭商。 我是一名探鬼主播固灵,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼劫流!你這毒婦竟也來(lái)了巫玻?” 一聲冷哼從身側(cè)響起丛忆,我...
    開(kāi)封第一講書(shū)人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎仍秤,沒(méi)想到半個(gè)月后熄诡,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡徒扶,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年粮彤,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了根穷。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片姜骡。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖屿良,靈堂內(nèi)的尸體忽然破棺而出圈澈,到底是詐尸還是另有隱情,我是刑警寧澤尘惧,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布康栈,位于F島的核電站,受9級(jí)特大地震影響喷橙,放射性物質(zhì)發(fā)生泄漏啥么。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一贰逾、第九天 我趴在偏房一處隱蔽的房頂上張望悬荣。 院中可真熱鬧,春花似錦疙剑、人聲如沸氯迂。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)嚼蚀。三九已至,卻和暖如春管挟,著一層夾襖步出監(jiān)牢的瞬間轿曙,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工僻孝, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留导帝,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓皮璧,卻偏偏與公主長(zhǎng)得像舟扎,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子悴务,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容