@EnableEurekaServer作用
@SpringBootApplication
//为什么加上这个注解,就可以当做注册中心了?
//可以先解释一下: 1、初始化了Eureka应用上下文环境 2、初始化了Filter过滤器
@EnableEurekaServer
public class AppEureka3000 {
public static void main(String[] args) {
SpringApplication.run(AppEureka3000.class);
}
}
//点开@EnableEurekaServer 就是下面的类
//重要的是@Import({EurekaServerMarkerConfiguration.class})
//因此,EnableEurekaServer这个注解类的作用就是往spring容器中注入一个Bean(EurekaServerMarkerConfiguration)
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({EurekaServerMarkerConfiguration.class})
public @interface EnableEurekaServer {
}
//点开这个@Import({EurekaServerMarkerConfiguration.class}) 中的类
//@Configuration 这个是表示 是个全注解类 以后会更新spring源码,会着重说一下这个注解
@Configuration
public class EurekaServerMarkerConfiguration {
public EurekaServerMarkerConfiguration() {
}
@Bean
public EurekaServerMarkerConfiguration.Marker eurekaServerMarkerBean() {
return new EurekaServerMarkerConfiguration.Marker();
}
class Marker {
Marker() {
}
}
}
//还记得springboot中的那个配置文件吗?spring.factories
//springcloud是基于springboot开发的,因此,springcloud也得去找这个配置文件
//这边又得用到springboot的自动配置的原理了,这个是Eureka-server,我们得看看那个配置文件了
//这个springboot自动配置的原理,我会在《springboot源码解析》中详细解释
这个spring.factories 文件中:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.netflix.eureka.server.EurekaServerAutoConfiguration
@Configuration
//@Import它的作用就不用多说了,上面是有解释的 这个就是用来初始化 Eureka环境上下文的
@Import({EurekaServerInitializerConfiguration.class})
//@ConditionalOnBean({Marker.class,就不会加载下面的类})
//这个东西的功能就是:如果spring容器中没有Marker.class,就不会加载EurekaServerAutoConfiguration
@ConditionalOnBean({Marker.class})
@EnableConfigurationProperties({EurekaDashboardProperties.class, InstanceRegistryProperties.class})
@PropertySource({"classpath:/eureka/server.properties"})
public class EurekaServerAutoConfiguration extends WebMvcConfigurerAdapter {
//看我这个类的继承,是否感觉很熟悉,不错,在springMVC源码中也是有继承一个MVC****什么的类
//那,就有疑问了,springcloud完全剥离了页面,怎么还会有MVC方面的东西呢?
//小孩没娘,说来话长了,这个是个注册服务,那么,我们的服务是不是要注册到这个Eureka上面呢,
//那么,就得类似Controller那样的,put get啥的,请求Eureka,把微服务注册到这个Eureka上面
//Eureka用的不是springMVC了,它用的是Jersey
//这个Jersey框架就注册一个过滤器就行,只要微服务请求过来了,就拦截注册到Eureka上面就行
//因此,这个类,就有两个很重要的作用: 1、初始化Eureka上下文环境 2、初始化 Jersey(过滤器)
//这个类就是用来初始化Jersey(过滤器)
@Bean
public FilterRegistrationBean jerseyFilterRegistration(Application eurekaJerseyApp) {
FilterRegistrationBean bean = new FilterRegistrationBean();
bean.setFilter(new ServletContainer(eurekaJerseyApp));
bean.setOrder(2147483647);
bean.setUrlPatterns(Collections.singletonList("/eureka/*"));
return bean;
}
}
如果是springMVC项目的话,我们把项目启动后,就要写Controller层了,@post @get什么的
springcloud是在springboot基础上对Eureka进行了继承,初始化的时候也是初始化了一个MVC容器,只不过这个不是springMVC而是Jersey,其实是一个道理,我们也要像springMVC那样写Controller了,只不过在这里不叫Controller而是叫做Resource,spring整合了Eureka,所以Eureka核心代码不是在上面的那个包。
服务注册
看上面的图片,Eureka很重要的一个Controller类,ApplicationResource。(说Controller说习惯了)
private final PeerAwareInstanceRegistry registry;
//这个方法就是服务注册
@POST
@Consumes({"application/json", "application/xml"})
public Response addInstance(InstanceInfo info, @HeaderParam("x-netflix-discovery- replication") String isReplication) {
//Netflix, Amazon,认为是来源MyOwn
//DataCenterInfo 这个里面有个泛型,就是上面的三个。说明,微服务来源于什么地方
//1、云服务中心 2、亚马逊、自己 90%的都是来源于自己的,我们就认为是来源MyOwn
DataCenterInfo dataCenterInfo = info.getDataCenterInfo();
//我只粘贴了重要的代码,想看完整代码,请自己去打开对应的类去看
//这边,我们要注册微服务了
this.registry.register(info, "true".equals(isReplication));
}
我们看到最后会调用一个接口的register() 这边用到一个设计模式,责任链,我先把类的关系图摆上
我是要着重说一个这张图的结构,这张图只有最下面的那个类 InstanceRegistry.java 是spring开发的,其它都是Eureka的类。
Eureka是个很牛逼的开发团队开发的,spring当了一次很卑微的舔狗,继承了人家的类。
其实,我们在打开上面的类的时候,有心的话,就会发现,只有最下面的那个类,包名是org.springframework
其他的都是这种类的路径。
最顶层的一个类:InstanceRegistery 这个是个接口。这个里面写了好多方法, 比如说服务注册
AbstractInstanceRegistry 这个类就实现了接口InstanceRegistery ,完成了服务注册的业务代码。
PeerAwareInstanceRegistryImpl 这个类就是完成了集群信息同步的功能。
责任分的很明确,不同的类干不一样的事情。
责任链就是这样的,每个类完成了不同的业务,耦合度其实是很低的。
这个springcloud开发的类,InstanceRegistery 这个就是里面有监听器,检测是否服务注册之类的,
如果,哪天,我们要加入新的功能,我们只需要继承springcloud的这个类 InstanceRegistery 就行,
不用改它原有的代码,很是方便,是不是!!!我们要学会这种方式,
聚是一坨屎,散是满天星。 也是很友好的描述了这种写法。降低代码的耦合度。
关于监听器,我会在《springboot源码解析》中,以案例的方式来详细阐述一下。
接着上面的类ApplicationResource,接着说我们的服务注册。
this.registry.register(info, "true".equals(isReplication));
//上面是服务注册的代码,直接调用到了springcloud开的类InstanceRegistry中
//InstanceRegistry.java
//看这个类,实现了一个接口 ApplicationContextAware
//我可以先说一下,实现这个接口,就是为了拿到spring上下文对象。
//实现这个接口的作用就是,等我们spring环境初始化完成后,我们就可以通过setApplicationContext()
//把spring环境上下文对象拿到
//在springboot整合Eureka中,Eureka里面根本没有架构监听器,监听器是由spring架构的
//既然要用到监听器,我们肯定得先拿到spring上下文对象。因此,实现了接口ApplicationContextAware
public class InstanceRegistry extends PeerAwareInstanceRegistryImpl implements
ApplicationContextAware {
private ApplicationContext ctxt;
public void setApplicationContext(ApplicationContext context) throws BeansException {
this.ctxt = context;
}
public void register(final InstanceInfo info, final boolean isReplication) {
//下面的这个方法handleRegistration()就是spring监听器的架构了
//this.publishEvent(new EurekaInstanceRegisteredEvent(this, info, leaseDuration, isReplication));
this.handleRegistration(info, this.resolveInstanceLeaseDuration(info), isReplication);
//上面的 this.handleRegistration() 仅仅是做了一个发布监听的作用
//下面才是去实现注册的地方
super.register(info, isReplication);
}
//super.register(info, isReplication);点开就是下面的方法
public void register(InstanceInfo info, boolean isReplication) {
//这个是心跳续约时间,默认90S 这个可以面试的时候,说说,springcloud默认就是90S
//当然,这个可以在application.yml配置文件中可以配置的
int leaseDuration = 90;
if (info.getLeaseInfo() != null && info.getLeaseInfo().getDurationInSecs() > 0) {
leaseDuration = info.getLeaseInfo().getDurationInSecs();
}
//这个是服务注册的核心方法 就是上面责任链说明的,有个类专门的搞服务注册的
super.register(info, leaseDuration, isReplication);
//这个方法是做 集群同步的, 什么时候才能同步? 肯定是服务注册完之后,才能同步到其它服务器上面
this.replicateToPeers(PeerAwareInstanceRegistryImpl.Action.Register,
info.getAppName(), info.getId(), info, (InstanceStatus)null, isReplication);
}
//类AbstractInstanceRegistry.java
//super.register(info, leaseDuration, isReplication); 这个方法
private final Lock read;
private final Lock write;
protected final Object lock;
//这个数据结构,我需要说明一下,
private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry = new ConcurrentHashMap();
//leaseDuration 这个是过期时间
register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
this.read.lock();
//这边有个东西还是要说一下的,我们要是以断点的方式走进的话,这个gMap不是空的
//这个问题是不是很神奇,为什么呢?Eureka也是刚刚启动的,微服务注册也是第一次进来,它怎会已经注册进去了?
//解释一下: 并发注册
//客户端在注册的时候,会有个超时判断,如果长时间注册不成功,客户端会把该线程舍掉
//客户端会开辟一个新的线程去注册,等你断点走到这里的时候,肯定已经注册成功了,但是,不是你注册的
//我们打断点来搞这个,肯定是已经超时了
Map<String, Lease<InstanceInfo>> gMap =
(Map)this.registry.get(registrant.getAppName());
if(gMap == null){
//这边我就不粘贴代码了,既然为空了,肯定是要put到数据结构 得到gMap
}
//由于上面,我们是把要注册的微服务已经放入到gMap数据结构中了
//然后,我们就用我们传进来的InstanceInfo中的id从gMap中获取已经存在的Lease
//existingLease 这个是已经存在数据结构中的微服务实例
Lease<InstanceInfo> existingLease = (Lease)((Map)gMap).get(registrant.getId());
//为什么会有这个?这边是有个冲突判断的的,就是上面我们说的这种情况,万一出现了,怎么办?
//一般情况下, 这个冲突是不会存在的,但是还是怕万一
//代码逻辑写的很是严谨,我们要向他们学习这种严谨的思维。
if(existingLease != null && existingLease.getHolder() != null){
//这个参数:已经存在的微服务实例最后的操作时间
Long existingLastDirtyTimestamp =
((InstanceInfo)existingLease.getHolder()).getLastDirtyTimestamp();
//注册该微服务的时间戳
Long registrationLastDirtyTimestamp =
registrant.getLastDirtyTimestamp();
//这个判断 就是上面类似断点进来的那种情况,我们要保证数据结构中的微服务信息是最新的
if (existingLastDirtyTimestamp > registrationLastDirtyTimestamp) {
registrant = (InstanceInfo)existingLease.getHolder();
}
}else{
// 这个else就是 服务注册 更新自我保护机制阈值的 地方
//上面试有冲突的情况,这个else肯定就是没有冲突的时候
//没有冲突的时候又干了什么呢? 自我保护阈值更新
//有必要说一下,这个地方的代码了,为什么+2 啥的,一开始我是懵逼的
//首先,我要说明一下,这边为什么会在else中?
//if是解决冲突所在,不懂是啥冲突的,请再看一遍上面写的注释,既然是解决冲突的,
//证明,这个微服务肯定是已经注册成功了,不能再走下面的流程了。
//expectedNumberOfRenewsPerMin 期待每分钟心跳的数量
//为何每来一次心跳续约要 +2?那是因为,心跳续约默认是的30S 既然这是每分钟了,肯定是要 +2
//正常情况下,每分钟,肯定得来2个心跳续约,所以+2
synchronized(this.lock) {
if (this.expectedNumberOfRenewsPerMin > 0) {
this.expectedNumberOfRenewsPerMin += 2;
//每分钟,服务心跳预估值
//这边这个值,会在自我保护机制用到《Eureka源码解析第三篇》
this.numberOfRenewsPerMinThreshold =
(int)(
(double)
this.expectedNumberOfRenewsPerMin *
this.serverConfig.getRenewalPercentThreshold());
)
}
}
}
//看到这里是不是感觉很是诧异?在这个方法的入口,我们已经把传进来的微服务实例
//放入到了数据结构中了,为什么这边又搞了一个Lease对象
//说白了,就是要搞一个最新的对象,就是前面处理冲突的地方,拿到最新的微服务实例
// 我觉得还是有必要讲解一下这个Lease对象的(下面代码块)
Lease<InstanceInfo> lease = new Lease(registrant, leaseDuration);
if (existingLease != null) {
lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp());
}
((Map)gMap).put(registrant.getId(), lease);
//到这里,服务注册已经完成了
}
//讲解一下这个租债器对象
//它是一个工具类,并不是专门为微服务写的
//这个类中还是有很多其它方法的,下面都会写出来的
public class Lease<T> {
public static final int DEFAULT_DURATION_IN_SECS = 90;
//这个就是泛型,不仅仅可以是微服务对象,也可以是其它对象,只不过在服务注册就是一直我们
//可能就是错误的理解为,它就是为了微服务对象诞生的,不是的,也可以是String,可以是很多东西
private T holder;
//服务剔除时间,当服务被剔除的时候,得把时间更新一下
private long evictionTimestamp;
//服务注册时间
private long registrationTimestamp;
//最后一次正常工作的时间
//注册 心跳续约 服务下架 都算操作
private long serviceUpTimestamp;
//最后操作时间
private volatile long lastUpdateTimestamp;
private long duration;
public void renew() {
this.lastUpdateTimestamp = System.currentTimeMillis() + this.duration;
}
public void cancel() {
if (this.evictionTimestamp <= 0L) {
this.evictionTimestamp = System.currentTimeMillis();
}
}
}
心跳续约
这边先给出一个时间:
心跳续约时间:30S
最后操作时间:21:44:30
续约前过期时间:21:45:00
续约后的时间:21:45:00
续约后最终过期时间:21:45:30
各个微服务会定时的发送请求给新注册中心,以确保服务还在正常工作。 心跳续约
所谓的心跳续约:就是把最后的操作时间 = 当前时间 + 服务过期时间
从此篇文章开头,第二张图片。
这个里面有个类 InstanceResource 这个是操作单个服务的类
private final PeerAwareInstanceRegistry registry;
@PUT
public Response renewLease(
@HeaderParam("x-netflix-discovery-replication") String isReplication,
@QueryParam("overriddenstatus") String overriddenStatus,
@QueryParam("status") String status,
@QueryParam("lastDirtyTimestamp") String lastDirtyTimestamp){
boolean isFromReplicaNode = "true".equals(isReplication);
//这个是调用心跳续约的方法
//其实这个又会跟踪到spring写的那个类中,又会发布监听
boolean isSuccess = this.registry.renew(this.app.getName(), this.id, isFromReplicaNode);
}
boolean renew(final String appName, final String serverId, boolean isReplication) {
//这个又是到了spring自己写的那个类,发布一个监听器
this.publishEvent(**********************);
//这个是心跳续约的方法
return super.renew(appName, serverId, isReplication);
}
PeerAwareInstanceRegistryImpl.java
boolean renew(String appName, String id, boolean isReplication) {
if (super.renew(appName, id, isReplication)) {
//如果心跳续约成功,那么就该同步集群了
this.replicateToPeers(*************************);
}
}
//其实心跳续约就是把最后的操作时间 = 当前时间 + 服务过期时间
//调到最后又到 Lease对象(租债器)
public void renew() {
//更新最后操作时间了 = 当前时间 + 心跳续约时间 (这个是bug)
//这个我觉得是Eureka的一个bug,从代码理解角度老说,确实是个bug
//lastUpdateTimestamp 这个是最后操作时间
//System.currentTimeMillis() + this.duration 这个是什么?是过期时间
//如果让我写,我会这样写,
//应该把当前时间赋值给最后操作时间
//this.lastUpdateTimestamp = System.currentTimeMillis();
this.lastUpdateTimestamp = System.currentTimeMillis() + this.duration;
}
//这个也是Lease中的方法,判断是否过期
//由于上面心跳续约有问题了,这边代码肯定是不对的
//System.currentTimeMillis() > this.lastUpdateTimestamp + this.duration + additionalLeaseMs
//这段代码的意思,当前时间 > 最后操作时间+心跳续约时间+集群同步损耗
//Eureka在源代码逻辑中,心跳续约中 已经加上了心跳续约时间,这边又加了一遍,相当于多加了一遍
public boolean isExpired(long additionalLeaseMs) {
return
this.evictionTimestamp > 0L ||
System.currentTimeMillis() > this.lastUpdateTimestamp + this.duration + additionalLeaseMs;
}