本文来说下Eureka缓存机制

文章目录

  • Eureka Server数据存储
  • Eureka Server缓存机制
  • 其它缓存设计
  • Eureka Server缓存机制源码分析
    • ResponseCacheImpl
    • CacheUpdateTask
    • invalidate缓存过期
    • GET获取缓存
    • generatePayload
  • 本文小结

Eureka Server数据存储

Eureka Client缓存机制很简单,设置了一个每30秒执行一次的定时任务,定时去服务端获取注册信息。获取之后,存入本地内存。

我们知道 Eureka Server 在运行期间就是一个普通的 Java 项目,并没有使用数据库之类的存储软件,那么在运行期间是如何存储数据的呢?

Eureka Server 的数据存储分了两层:数据存储层和缓存层。数据存储层记录注册到 Eureka Server 上的服务信息,缓存层是经过包装后的数据,可以直接在 Eureka Client 调用时返回。我们先来看看数据存储层的数据结构。

Eureka Server 的数据存储层是双层的 ConcurrentHashMap,我们知道 ConcurrentHashMap 是线程安全高效的 Map 集合。

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

第一层的 ConcurrentHashMap 的 key=spring.application.name 也就是客户端实例注册的应用名;value 为嵌套的 ConcurrentHashMap。

第二层嵌套的 ConcurrentHashMap 的 key=instanceId 也就是服务的唯一实例 ID,value 为 Lease 对象,Lease 对象存储着这个实例的所有注册信息,包括 ip 、端口、属性等。

根据这个存储结构我们可以发现,Eureka Server 第一层都是存储着所有的服务名,以及服务名对应的实例信息,也就是说第一层都是按照服务应用名这个维度来切分存储:

应用名1:应用1实例 Map
应该名2:应用2实例 Map
...

第二层是根据实例的唯一 ID 来存储的,那么按照这个结构最终的存储数据格式为:

                :  应用1实例A:实例A的注册信息
应用名1:应用1实例:  应用1实例B:实例B的注册信息 :  应用1实例C:实例C的注册信息:  ....
-----------------              :  应用2实例F:实例F的注册信息
应该名2:应用2实例:  应用2实例G:实例G的注册信息 :  ... ...

数据存储层数据结构如下图所示:


当如服务的状态发生变更时,会同步 Eureka Server 中的 registry 数据信息,比如服务注册、剔除服务时。


Eureka Server缓存机制

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 的响应时间,通过数据存储层和缓存层的数据切割,根据使用场景来提供不同的数据支持。


其它缓存设计

除过 Eureka Server 端存在缓存外,Eureka Client 也同样存在着缓存机制,Eureka Client 启动时会全量拉取服务列表,启动后每隔 30 秒从 Eureka Server 量获取服务列表信息,并保持在本地缓存中。

Eureka Client 增量拉取失败,或者增量拉取之后对比 hashcode 发现不一致,就会执行全量拉取,这样避免了网络某时段分片带来的问题,同样会更新到本地缓存。

同时对于服务调用,如果涉及到 ribbon 负载均衡,那么 ribbon 对于这个实例列表也有自己的缓存,这个缓存定时(默认30秒)从 Eureka Client 的缓存更新。

这么多的缓存机制可能就会造成一些问题,一个服务启动后可能最长需要 90s 才能被其它服务感知到:

  1. 首先,Eureka Server 维护每 30s 更新的响应缓存
  2. Eureka Client 对已经获取到的注册信息也做了 30s 缓存
  3. 负载均衡组件 Ribbon 也有 30s 缓存

这三个缓存加起来,就有可能导致服务注册最长延迟 90s ,这个需要我们在特殊业务场景中注意其产生的影响。


Eureka Server缓存机制源码分析

Eureka Server的缓存机制依赖于谷歌的gauva cache , 在Eureka中通过com.netflix.eureka.registry.ResponseCacheImpl , 这个操作类来实现缓存的机制。


ResponseCacheImpl

 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(1000).expireAfterWrite(serverConfig.getResponseCacheAutoExpirationInSeconds(), TimeUnit.SECONDS).removalListener(new RemovalListener<Key, Value>() {@Overridepublic void onRemoval(RemovalNotification<Key, Value> notification) {Key removedKey = notification.getKey();if (removedKey.hasRegions()) {Key cloneWithNoRegions = removedKey.cloneWithoutRegions();regionSpecificKeys.remove(cloneWithNoRegions, removedKey);}}})// 缓存加载器,当缓存不存在时,会自动执行load方法,进行缓存加载。同时返回缓存数据.build(new CacheLoader<Key, Value>() {@Overridepublic Value load(Key key) throws Exception {if (key.hasRegions()) {Key cloneWithNoRegions = key.cloneWithoutRegions();regionSpecificKeys.put(cloneWithNoRegions, key);}Value value = generatePayload(key);return value;}});// 是否使用只读缓存,如果使用,此处则启动一个定时器,用来复制readWriteCacheMap 的数据至readOnlyCacheMapif (shouldUseReadOnlyResponseCache) {timer.schedule(getCacheUpdateTask(),new Date(((System.currentTimeMillis() / responseCacheUpdateIntervalMs) * responseCacheUpdateIntervalMs)+ responseCacheUpdateIntervalMs),responseCacheUpdateIntervalMs);}try {Monitors.registerObject(this);} catch (Throwable e) {logger.warn("Cannot register the JMX monitor for the InstanceRegistry", e);}
}

通过上面可以很简单的看出, Eureka Server的缓存是通过一个只读,一个读写缓存来实现的。

readWriteCacheMap : 此处存放的是最终的缓存, 当服务下线,过期,注册,状态变更,都会来清除这个缓存里面的数据。 然后通过CacheLoader进行缓存加载,在进行readWriteCacheMap.get(key)的时候,首先看这个缓存里面有没有该数据,如果没有则通过CacheLoader的load方法去加载,加载成功之后将数据放入缓存,同时返回数据

readOnlyCacheMap : 这是一个JVM的CurrentHashMap只读缓存,这个主要是为了供客户端获取注册信息时使用,其缓存更新,依赖于定时器的更新,通过和readWriteCacheMap 的值做对比,如果数据不一致,则以readWriteCacheMap 的数据为准。

responseCacheUpdateIntervalMs : readOnlyCacheMap 缓存更新的定时器时间间隔,默认为30秒

responseCacheAutoExpirationInSeconds : readWriteCacheMap 缓存过期时间,默认为 180 秒 。


CacheUpdateTask

readOnlyCacheMap 定时器的任务执行类。

private TimerTask getCacheUpdateTask() {return new TimerTask() {@Overridepublic void run() {logger.debug("Updating the client cache from response cache");// 循环readOnlyCacheMap里面的KEYfor (Key key : readOnlyCacheMap.keySet()) {if (logger.isDebugEnabled()) {Object[] args = {key.getEntityType(), key.getName(), key.getVersion(), key.getType()};logger.debug("Updating the client cache from response cache for key : {} {} {} {}", args);}try {// 版本号CurrentRequestVersion.set(key.getVersion());// 从readWriteCacheMap获取数据Value cacheValue = readWriteCacheMap.get(key);// 当前的只读数据Value currentCacheValue = readOnlyCacheMap.get(key);// 判断数据是否一致if (cacheValue != currentCacheValue) {// 如果不一致,覆盖只读缓存里面的数据,以readWriteCacheMap为准readOnlyCacheMap.put(key, cacheValue);}} catch (Throwable th) {logger.error("Error while updating the client cache from response cache", th);}}}};
}

invalidate缓存过期

invalidate缓存过期

public void invalidate(Key... keys) {// 循环传入的key一次调用API进行清除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);}}}
}

这个方法,是在服务下线, 过期,注册,状态变更的时候会调用的,从上面可以看到,这里的缓存清除只是会去清除readWriteCacheMap这个缓存, readOnlyCacheMap 只读 缓存并没有更新,也就说当客户端的信息发生变化之后, 只读缓存不是第一时间感知到的。 只读缓存的更新只能依赖那个30秒的定时任务来更新。


GET获取缓存

GET获取缓存

 @VisibleForTestingString get(final Key key, boolean useReadOnlyCache) {// 主要看这个getValue Value payload = getValue(key, useReadOnlyCache);if (payload == null || payload.getPayload().equals(EMPTY_PAYLOAD)) {return null;} else {return payload.getPayload();}}Value getValue(final Key key, boolean useReadOnlyCache) {Value payload = null;try {// 是否使用只读缓存if (useReadOnlyCache) {// 从只读缓存里面获取数据final Value currentPayload = readOnlyCacheMap.get(key);if (currentPayload != null) {// 不为空的话,直接返回数据payload = currentPayload;} else {// 只读缓存里面没有,就到读写缓存里面去获取。 payload = readWriteCacheMap.get(key);// 同时将数据,放入只读缓存。readOnlyCacheMap.put(key, payload);}} else {// 不适用只读缓存payload = readWriteCacheMap.get(key);}} catch (Throwable t) {logger.error("Cannot get value for key :" + key, t);}return payload;
}

useReadOnlyCache : shouldUseReadOnlyResponseCache ,可以配置是否使用只读缓存,默认是true。

readWriteCacheMap.get(key) : 这个使用的是gauva 的缓存机制,如果当前的缓存里面这个key没有,那么会直接调用CacheLoader.load()方法,从最上面的代码可以看到, load方法,主要是执行了generatePayload()方法。


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();//调用registry.getApplications() 获取应用信息。同时调用getPayLoad进行编码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();// 调用registry.getApplicationDeltas() 获取增量注册信息payload = getPayLoad(key, registry.getApplicationDeltas());}} else {// 根据key直接获取注册信息tracer = serializeOneApptimer.start();payload = getPayLoad(key, registry.getApplication(key.getName()));}break;// 根据VIP获取case VIP:case SVIP:tracer = serializeViptimer.start();payload = getPayLoad(key, getApplicationsForVip(key, registry));break;default:logger.error("Unidentified entity type: " + key.getEntityType() + " found in the cache key.");payload = "";break;}return new Value(payload);} finally {if (tracer != null) {tracer.stop();}}}// 编码类private String getPayLoad(Key key, Applications apps) {EncoderWrapper encoderWrapper = serverCodecs.getEncoder(key.getType(), key.getEurekaAccept());String result;try {result = encoderWrapper.encode(apps);} catch (Exception e) {logger.error("Failed to encode the payload for all apps", e);return "";}if(logger.isDebugEnabled()) {logger.debug("New application cache entry {} with apps hashcode {}", key.toStringCompact(), apps.getAppsHashCode());}return result;
}

entityType : 分为三种,Application, VIP, SVIP , 客户端获取注册信息的话,传入的主要是Application类型的,另外两种类型此处不做考虑 。


本文小结

本文详细介绍了Eureka的缓存机制以及核心实现源码。

Eureka缓存机制相关推荐

  1. eureka server配置_程序员笔记|详解Eureka 缓存机制

    引言 Eureka是Netflix开源的.用于实现服务注册和发现的服务.Spring Cloud Eureka基于Eureka进行二次封装,增加了更人性化的UI,使用更为方便.但是由于Eureka本身 ...

  2. 图文详述Eureka的缓存机制/三级缓存

    前言 1.为什么说Eureka是CAP理论中的AP? 从CAP理论看,Eureka是一个AP系统,其优先保证可用性(A)和分区容错性§,不保证强一致性©,但能做到最终一致性. 因为只要集群中任意一个实 ...

  3. Django缓存机制

    Django缓存机制三个粒度:1 全站缓存 settings.py 全局配置文件用中间件:MIDDLEWARE = [# 'django.middleware.cache.UpdateCacheMid ...

  4. MyBatis复习笔记6:MyBatis缓存机制

    MyBatis缓存机制 MyBatis 包含一个非常强大的查询缓存特性,它可以非常方便地配置和定制.缓存可以极大的提升查询效率. MyBatis系统中默认定义了两级缓存. 一级缓存和二级缓存. 默认情 ...

  5. java设置缓存机制

    2019独角兽企业重金招聘Python工程师标准>>> java设置缓存机制 所谓缓存,就是将程序或系统经常要调用的对象存在内存中,一遍其使用时可以快速调用,不必再去创建新的重复的实 ...

  6. Mybait缓存机制

    MyBatis同大多数ORM框架一样,提供了一级缓存和二级缓存的支持. 一级缓存:其作用域为session范围内,当session执行flush或close方法后,一级缓存会被清空. 二级缓存:二级缓 ...

  7. LeetCode实战:LRU缓存机制

    背景 为什么你要加入一个技术团队? 如何加入 LSGO 软件技术团队? 我是如何组织"算法刻意练习活动"的? 为什么要求团队的学生们写技术Blog 题目英文 Design and ...

  8. 微服务架构下静态数据通用缓存机制

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 来源 |  my.oschina.net/u/3971241/bl ...

  9. 微服务架构下的静态数据通用缓存机制!

    什么是静态数据 为什么需要缓存 通用缓存机制 总结 后记 在分布式系统中,特别是最近很火的微服务架构下,有没有或者能不能总结出一个业务静态数据的通用缓存处理机制或方案,这篇文章将结合一些实际的研发经验 ...

最新文章

  1. java 程序运行时注入方法_Spring入门(九):运行时值注入
  2. 用字体在网页中画Icon图标
  3. Redis 性能问题分析
  4. HTML笔记一,部分常用的元素与属性
  5. 《大数据》第2期“专题”——数据开放与政府治理创新
  6. 如何从900万张图片中对600类照片进行分类,附代码
  7. 腾讯,创新工场,淘宝等公司最新面试三十题(第171-200题)
  8. 萌新的Python练习菜鸟100例(十一)生兔子练习
  9. linux vi-vim编辑器快捷键
  10. (原創) Verilog入門書推薦2:數位系統實習 Quartus II (SOC) (Verilog)
  11. 苹果发布2019年上半年透明度报告,收到数万条政府请求
  12. 六石管理学:流程是为工作服务的
  13. SQL中使用WITH AS提高性能-使用公用表表达式(CTE)简化嵌套SQL(转载)
  14. libcurl的封装,支持同步异步请求,支持多线程下载,支持https(z)
  15. “我们检测到您之前将硬盘移动到新的DS3617xs。如果您要现在还原数据和设置,请单击“还原” 解决办法
  16. Mysql基础命令语句(1)
  17. 抽象代数之S3的自同构群和S3的内自同构群
  18. 这个轮子让SpringBoot实现api加密So Easy!
  19. 市场营销学9——产品策略
  20. Python序列 数据类型 创建方式 Tuple元组 Str字符串 List列表 dict字典 Set集合 range,zip,map,enumerate

热门文章

  1. 设计模式之禅之设计模式-组合模式
  2. ListView和SlidingDrawer
  3. 服务器托管常见问题纠纷与解决方法
  4. 分布式压测系列之Jmeter4.0
  5. 从java的NIO版hello world看java源码,我们能看到什么?
  6. 编写第二个页面:新闻阅读列表页面
  7. linux有关信号的FAQ
  8. HashMap的使用方法及注意事项
  9. AIX 常用命令汇总(二)
  10. where、having、group by、order by、limit的区别和使用顺序