大家好,我是悟空。
先说下哈,这篇文章画原理图用了很多时间,求个三连!
Eureka 注册中心系列文章已经写到第七篇了,这里汇总下:
> 本文已收录到我的 github: https://github.com/Jackson0714/PassJava-Learning
上一讲我们讲到了 Eureka 注册中心的 Server 端有三级缓存来保存注册信息,可以利用缓存的快速读取来提高系统性能。我们再来细看下:
一级缓存:只读缓存 readOnlyCacheMap
,数据结构 ConcurrentHashMap。相当于数据库。
二级缓存:读写缓存 readOnlyCacheMap
,Guava Cache。相当于 Redis 主从架构中主节点,既可以进行读也可以进行写。
三级缓存:本地注册表 registry
,数据结构 ConcurentHashMap。相当于 Redis 主从架构的从节点,只负责读。
看图更清晰,如下图所示:
另外 ConcurrenthashMap 也是一种 map 结构,也就是以键值对的方式进行存储,如下图所示:
本篇悟空哥会带着大家来看下 Eureka 的缓存架构是怎么样,通过学习这篇,我们也可以借鉴 Eureka 的缓存设计思想,将其运用到项目当中。
我们再来看下 Eureka 源码,其实不难看懂,下面会做解释。
只读
缓存里面找。读写
缓存里面找。带来了三个问题:
> (1)三级缓存数据怎么来的? > > (2)缓存数据如何更新的? > > (3)缓存如何过期?
我们先来看下本地缓存 registry,它是一种定义为 ConcurrentHashMap 的数据结构,之前也详细讲解过。
当客户端发起注册请求的时候,就会把注册信息放到 registry 中。如下代码所示:
registry.putIfAbsent(app)
putIfAbsent 表示如果存在重复的 key,就不会放入值,如果传入的 key 对应的 value 已经存在,就返回存在的 value,不进行替换。
经过 putIfAbsent 操作就把客户端的注册信息放到 registry 中了。
我们再来看下其中的一种缓存结构:读写缓存。
读写缓存,顾名思义,就是既可以进行读,也可以进行写的缓存。读主要是给只读缓存来读取的。写主要是将缓存更新到自己的 Map 中。
下面分别从写缓存的原理、写缓存的源码、过期时机的原理、过期时机的源码几个方面来分别解答。
我开始以为当我们读缓存读不到的时候,就会去数据库查了。找了半天,没找到读数据库的地方。
然后我就用 IDEA 工具查找 readOnlyCacheMap 被使用的地方,终于让我找到了。
读写缓存用的是 Guava Cache工具类,这篇不会深究。简单来说就是当访问读写缓存时,如果这个 key 在缓存中不存在,则从本地去查,查到后再放回缓存。
然后又实现抽象方法 load(key),这个方法的作用就是当读写缓存中没有,则从本地 registry 缓存中拿。
读写缓存过期的时候其实分两种:定时过期和实时过期。由于上面的源码已经定义了定时过期的时间间隔,所以我们先来看定时过期。
当构建这个读写缓存时,就会定义间隔多久过期整个读写缓存。如下代码所示,180 s 会定时过期读写缓存。
expireAfterWrite(180s)
当有新的服务实例进行注册或者下线、发生故障时,就会把这个对应的服务实例的缓存给过期掉。
如下图所示,最上面的时注册中心,下面三个是服务实例。服务实例发生注册、下线、发生故障,注册中心都是可以感知到的,然后就会主动过期读写缓存对应的服务实例。
从源码层面我们再来看下读写缓存过期的源码。调用了 invalidateCache 方法,进行过期。
文件路径:com/netflix/eureka/registry/AbstractInstanceRegistry.java
只读缓存 readOnlyCacheMap,有一个定时更新的机制,每隔 30 秒就会更新一次只读缓存中的某些 key。
它其实是遍历自己的所有注册信息,然后和读写缓存进行比对,如果注册信息不一致,则替换为读写缓存的数据。
源码如下,有一个定时调度任务,每隔 30 秒调度一次。
另外当客户端获取注册信息时,也会先读只读缓存,如果只读缓存中没有,则会从读写缓存中找,找到后就放到只读缓存中。如果读写缓存中没有,则从本地注册表 registry 中加载到读写缓存中,然后将注册表信息返回。
> 这里大家是否有个疑问:既然这个缓存叫做只读缓存,怎么还能被更新,不应该是不变的吗?
其实这里的不变是相对于客户端来说的,客户端获取注册表信息时,最开始访问的就是只读缓存,类似数据库或 Redis 的主从架构,主负责读写,从负责读。然后系统内部会把主节点的信息同步给从节点。大家明白了吗?
下面我们来看下 Eureka Server 对于缓存有哪些配置呢?
> eureka.server.useReadOnlyResponseCache
当客户端获取注册信息时,是否先从只读缓存获取。如果为 false,则直接从读写缓存获取。默认为 true。
> eureka.server.responseCacheUpdateIntervalMs
默认每隔 30 秒将读写缓存更新的缓存同步到只读缓存。
三级缓存看似可以带来性能的提升。但是也会引入其他问题,比如缓存不一致问题。
只读缓存每隔 30s 才会刷新一次,和读写缓存会造成数据的不一致,客户端在 30s 内获取的注册表信息是滞后的。
当使用 Eureka 集群时,这种缓存不一致的问题会更明显,不同的节点之间也会出现只读缓存的数据不一致,所以 Eureka 只能保证高可用,并不能保证强一致性,也就是保证了 AP,不保证 CP,另外我们可以选用强一致性的注册中心,比如 Zookeeper、Nacos,这是后续要讲的内容了。
> 如何缓解不一致的问题呢?
(1)在服务端,我们可以设置更新只读缓存的时间间隔,默认是 30 秒,缩短一点,比如 15 秒,频率太高,可能对 Eureka 造成性能问题。
(2)服务端,我们也可以考虑关闭从只读缓存读注册表信息,Eureka Client 直接从读写缓存读取。
本篇学习了 Eureka 注册中心 Server 端的三层缓存架构,分为 registry、readOnlyCacheMap、readWriteCacheMap,用来保存服务注册信息。
参考资料: www.passjava.cn 《微服务架构深度解析》 Eureka 源码
> 作者简介:悟空,8年一线互联网开发和架构经验,用故事讲解分布式、架构设计、Java 核心技术。《JVM性能优化实战》专栏作者,开源了《Spring Cloud 实战 PassJava》项目,公众号:悟空聊架构
。本文已收录至 www.passjava.cn
|