Spring Cloud Eureka源码分析之三级缓存的设计原理及源码分析

Eureka Server 为了提供响应效率,提供了两层的缓存结构,将 Eureka Client 所需要的注册信息,直接存储在缓存结构中,实现原理如下图所示。

第一层缓存:readOnlyCacheMap,本质上是 ConcurrentHashMap,依赖定时从 readWriteCacheMap 同步数据,默认时间为 30 秒。
readOnlyCacheMap : 是一个 CurrentHashMap 只读缓存,这个主要是为了供客户端获取注册信息时使用,其缓存更新,依赖于定时器的更新,通过和 readWriteCacheMap 的值做对比,如果数据不一致,则以 readWriteCacheMap 的数据为准。
第二层缓存:readWriteCacheMap,本质上是 Guava 缓存。
readWriteCacheMap:readWriteCacheMap 的数据主要同步于存储层。当获取缓存时判断缓存中是否没有数据,如果不存在此数据,则通过 CacheLoader 的 load 方法去加载,加载成功之后将数据放入缓存,同时返回数据。
readWriteCacheMap 缓存过期时间,默认为 180 秒,当服务下线、过期、注册、状态变更,都会来清除此缓存中的数据。
Eureka Client 获取全量或者增量的数据时,会先从一级缓存中获取;如果一级缓存中不存在,再从二级缓存中获取;如果二级缓存也不存在,这时候先将存储层的数据同步到缓存中,再从缓存中获取。
通过 Eureka Server 的二层缓存机制,可以非常有效地提升 Eureka Server 的响应时间,通过数据存储层和缓存层的数据切割,根据使用场景来提供不同的数据支持。
多级缓存的意义
这里为什么要设计多级缓存呢?原因很简单,就是当存在大规模的服务注册和更新时,如果只是修改一个ConcurrentHashMap数据,那么势必因为锁的存在导致竞争,影响性能。
而Eureka又是AP模型,只需要满足最终可用就行。所以它在这里用到多级缓存来实现读写分离。注册方法写的时候直接写内存注册表,写完表之后主动失效读写缓存。
获取注册信息接口先从只读缓存取,只读缓存没有再去读写缓存取,读写缓存没有再去内存注册表里取(不只是取,此处较复杂)。并且,读写缓存会更新回写只读缓存
- responseCacheUpdateIntervalMs : readOnlyCacheMap 缓存更新的定时器时间间隔,默认为30秒
- responseCacheAutoExpirationInSeconds : readWriteCacheMap 缓存过期时间,默认为 180 秒 。
缓存初始化
readWriteCacheMap使用的是LoadingCache对象,它是guava中提供的用来实现内存缓存的一个api。创建方式如下
LoadingCache<Long, String> cache = CacheBuilder.newBuilder()
//缓存池大小,在缓存项接近该大小时, Guava开始回收旧的缓存项
.maximumSize(10000)
//设置时间对象没有被读/写访问则对象从内存中删除(在另外的线程里面不定期维护)
.expireAfterAccess(10, TimeUnit.MINUTES)
//移除监听器,缓存项被移除时会触发
.removalListener(new RemovalListener <Long, String>() {
@Override
public void onRemoval(RemovalNotification<Long, String> rn) {
//执行逻辑操作
}
})
.recordStats()//开启Guava Cache的统计功能
.build(new CacheLoader<String, Object>() {
@Override
public Object load(String key) {
//从 SQL或者NoSql 获取对象
}
});//CacheLoader类 实现自动加载
其中,CacheLoader是用来实现缓存自动加载的功能,当触发readWriteCacheMap.get(key)方法时,就会回调CacheLoader.load方法,根据key去服务注册信息中去查找实例数据进行缓存
ResponseCacheImpl(EurekaServerConfig serverConfig, ServerCodecs serverCodecs, AbstractInstanceRegistry registry) {
this.serverConfig = serverConfig;
this.serverCodecs = serverCodecs;
this.shouldUseReadOnlyResponseCache = serverConfig.shouldUseReadOnlyResponseCache();
this.registry = registry;
long responseCacheUpdateIntervalMs = serverConfig.getResponseCacheUpdateIntervalMs();
this.readWriteCacheMap =
CacheBuilder.newBuilder().initialCapacity(serverConfig.getInitialCapacityOfResponseCache())
.expireAfterWrite(serverConfig.getResponseCacheAutoExpirationInSeconds(), TimeUnit.SECONDS)
.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);
}
}
})
.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 = generatePayload(key); //注意这里
return value;
}
});
而缓存的加载,是基于generatePayload方法完成的,代码如下。
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();
payload = getPayLoad(key, registry.getApplications());
}
} else if (ALL_APPS_DELTA.equals(key.getName())) {
if (isRemoteRegionRequested) {
tracer = serializeDeltaAppsWithRemoteRegionTimer.start();
versionDeltaWithRegions.incrementAndGet();
versionDeltaWithRegionsLegacy.incrementAndGet();
payload = getPayLoad(key,
registry.getApplicationDeltasFromMultipleRegions(key.getRegions()));
} else {
tracer = serializeDeltaAppsTimer.start();
versionDelta.incrementAndGet();
versionDeltaLegacy.incrementAndGet();
payload = getPayLoad(key, registry.getApplicationDeltas());
}
} else {
tracer = serializeOneApptimer.start();
payload = getPayLoad(key, registry.getApplication(key.getName()));
}
break;
case VIP:
case SVIP:
tracer = serializeViptimer.start();
payload = getPayLoad(key, getApplicationsForVip(key, registry));
break;
default:
logger.error("Unidentified entity type: {} found in the cache key.", key.getEntityType());
payload = "";
break;
}
return new Value(payload);
} finally {
if (tracer != null) {
tracer.stop();
}
}
}
此方法接受一个 Key 类型的参数,返回一个 Value 类型。 其中 Key 中重要的字段有:
KeyType,表示payload文本格式,有 JSON 和 XML 两种值。EntityType,表示缓存的类型,有Application,VIP,SVIP三种值。entityName,表示缓存的名称,可能是单个应用名,也可能是ALL_APPS或ALL_APPS_DELTA。
Value 则有一个 String 类型的payload和一个 byte 数组,表示gzip压缩后的字节。
缓存同步
在ResponseCacheImpl这个类的构造实现中,初始化了一个定时任务,这个定时任务每个
ResponseCacheImpl(EurekaServerConfig serverConfig, ServerCodecs serverCodecs, AbstractInstanceRegistry registry) {
//省略...
if (shouldUseReadOnlyResponseCache) {
timer.schedule(getCacheUpdateTask(),
new Date(((System.currentTimeMillis() / responseCacheUpdateIntervalMs) * responseCacheUpdateIntervalMs)
+ responseCacheUpdateIntervalMs),
responseCacheUpdateIntervalMs);
}
}
默认每30s从readWriteCacheMap更新有差异的数据同步到readOnlyCacheMap中
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);
} finally {
CurrentRequestVersion.remove();
}
}
}
};
}
缓存失效
在AbstractInstanceRegistry.register这个方法中,当完成服务信息保存后,会调用invalidateCache失效缓存
public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
//....
invalidateCache(registrant.getAppName(), registrant.getVIPAddress(), registrant.getSecureVipAddress());
//....
}
最终调用ResponseCacheImpl.invalidate方法,完成缓存的失效机制
public void invalidate(Key... keys) {
for (Key key : keys) {
logger.debug("Invalidating the response cache key : {} {} {} {}, {}",
key.getEntityType(), key.getName(), key.getVersion(), key.getType(), key.getEurekaAccept());
readWriteCacheMap.invalidate(key);
Collection<Key> keysWithRegions = regionSpecificKeys.get(key);
if (null != keysWithRegions && !keysWithRegions.isEmpty()) {
for (Key keysWithRegion : keysWithRegions) {
logger.debug("Invalidating the response cache key : {} {} {} {} {}",
key.getEntityType(), key.getName(), key.getVersion(), key.getType(), key.getEurekaAccept());
readWriteCacheMap.invalidate(keysWithRegion);
}
}
}
}
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自
Mic带你学架构!
如果本篇文章对您有帮助,还请帮忙点个关注和赞,您的坚持是我不断创作的动力。欢迎关注「跟着Mic学架构」公众号公众号获取更多技术干货!

Spring Cloud Eureka源码分析之三级缓存的设计原理及源码分析的更多相关文章
- Spring Cloud 学习 之 Spring Cloud Eureka(源码分析)
Spring Cloud 学习 之 Spring Cloud Eureka(源码分析) Spring Boot版本:2.1.4.RELEASE Spring Cloud版本:Greenwich.SR1 ...
- Spring Cloud Eureka源码分析之服务注册的流程与数据存储设计!
Spring Cloud是一个生态,它提供了一套标准,这套标准可以通过不同的组件来实现,其中就包含服务注册/发现.熔断.负载均衡等,在spring-cloud-common这个包中,org.sprin ...
- Spring Cloud Eureka(七):DiscoveryClient 源码分析
1.本节概要 上一节文章主要介绍了Eureka Client 的服务注册的流程,没有对服务治理进行介绍,本文目的就是从源码角度来学习服务实例的治理机制,主要包括以下内容: 服务注册(register) ...
- Spring Cloud Eureka 自我保护机制实战分析
前些天栈长在Java技术栈微信公众号分享过 Spring Cloud Eureka 的系列文章: Spring Cloud Eureka 自我保护机制 Spring Cloud Eureka 常用配置 ...
- 第三章 服务治理:Spring Cloud Eureka
Spring Cloud Eureka是Spring Cloud Netflix 微服务套件中的一部分,它基于Netflix Eureka做了二次封装,主要负责完成微服务架构中的服务治理功能.Spri ...
- Spring Cloud Eureka(一): 开篇说明及目录汇总
开篇简述 基于Spring Boot 和 Spring Cloud 的微服务应用,本人在工作中已经使用两年有余了,伴随着个人学习计划的实施,希望借助博文的方式,将工作中使用到的技术点体系化的总结出来, ...
- 1 Spring Cloud Eureka服务治理
注:此随笔为读书笔记.<Spring Cloud微服务实战> 什么是微服务? 微服务是将一个原本独立的系统拆分成若干个小型服务(一般按照功能模块拆分),这些小型服务都在各自独立的进程中运行 ...
- 1 Spring Cloud Eureka服务治理(上)
注:此随笔为读书笔记.<Spring Cloud微服务实战>,想学习Spring Cloud的同伴们可以去看看此书,里面对源码有详细的解读. 什么是微服务? 微服务是将一个原本独立的系统拆 ...
- Spring Cloud Eureka 服务注册中心(二)
序言 Eureka 是 Netflix 开发的,一个基于 REST 服务的,服务注册与发现的组件 它主要包括两个组件:Eureka Server 和 Eureka Client Eureka Clie ...
随机推荐
- [luogu4484]最长上升子序列
标算是状压dp+打表,前者时间复杂度为$o(n^{2}2^{n})$,并通过打表做到$o(1)$ 参考loj2265中关于杨表的相关知识,不难发现答案即$\frac{\sum_{a\vdash n}a ...
- [noi109]排队
题目要求其实相当于要让大于和小于m的数的个数都不超过n/2,因此当要对一个数处理时,要么把它改成m,要么不作修改,根据这个贪心就可以完成了. 1 #include<bits/stdc++.h&g ...
- [nowcoder5667G]Greater and Greater
令$f[i][j]$表示前i个数的后j位能否匹配b的前j位,有转移$f[i][j]=f[i-1][j-1] \ \&\ [b_{j}\le a_{i}]$ 将$g[i][j]=[b_{j}\ ...
- Hardware assisted virtualization and data execution protection must be enabled in the BIOS. See https://docs.docker.com/docker-for-windows/troubleshoot/#virtualization
解决办法:先关闭 Hyper-V ,然后重新开启 Hyper-V 即可! 来自:https://zhuanlan.zhihu.com/p/51939654
- asList和subList的缺陷
概述 在开发中我们经常使用asList去把一个数组转换为List.也存在通过subList.subMap.来对List.Map.Set来进行类似使用subString方法来对String对象进行分割处 ...
- watch异步操作
异步操作: 1.ajax, 2.定时器 3.点击事件 4.数据库操作 特点:代码不等待,后续代码会继续执行. watch:{ //watch作用监测已经存在的数据 newVal 新值,oldVal 旧 ...
- .Net Core中使用ElasticSearch(二)
.Net的ElasticSearch 有两个版本,Elasticsearch.Net(低级) 和 NEST(高级),推荐使用 NEST,低级版本的更灵活,水太深 把握不住.有个需要注意,使用的版本号必 ...
- IDEA 2021.2.3 安装与破解教程
首先说明,大部分情况下,花10块钱都可以在淘宝找人直接帮你完美解决这个资源问题 所以千万不要相信一些所谓的百度结果,一般都是花费了时间却最后无法解决破解的问题 我相信任何一个想要学习软件开发的人一定要 ...
- 区分wsgi、uWSGI、uwsgi、php-fpm、CGI、FastCGI
在学习Python web开发时候,可能会遇到诸如uwsgi,wsgi等名词,下面通过梳理总结探究它们之间的关系. CGI CGI,(Common Gateway Interface)通用网关接口,是 ...
- [R]在dplyr基础上编写函数-(1)eval
tidyverse系列的R包虽然解放了大家的双手,但同时也束缚了我们重新编写函数的能力.在这一套语法中,要实现作为函数参数的字符串和变量之间的相互转换困难重重,但只要掌握了其中原理后,也就能够游刃有余 ...