Eureka服务端如何接收客户端注册

原理浅析

既然服务端要接收客户端注册,那服务端一定是有一个数据结构来保存客户端注册的列表。这个数据结构在Eureka中就是一个Map!

java 复制代码
private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry  = new ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>();

这个Map的key是AppName,默认指的是在yml文件中配置的spring.application.name属性。

这个Map的value是一个列表,一个什么列表呢?在分布式的环境下,一个服务通常会以集群的形式部署,所以一个AppName通常也对应若干个服务实例,所以这里的value就是这若干个服务列表。这个列表以Map<String, Lease<InstanceInfo>>的形式存储。

其中这个列表中的key指的是InstanceId,通常InstanceId默认指的是主机名称。

这个列表的value指的是租约Lease,也就是这个服务实例的详细信息(注册时间、续约时间间隔、最新续约时间、服务实例详细信息等)

也就是说,服务端维护了这样一个列表:

有了这个一个注册表,客户端想要获取服务实例信息,就可以通过Appname获取同一个服务所有的集群信息,再通过InstanceId就可以获取指定服务实例信息。

从源码深入

从上一小节,我们了解到Eureka通过ApplicationResource暴露了接收客户端注册到服务端的接口。具体接口如下:

客户端调用HTTP接口就会走到com.netflix.eureka.resources.ApplicationResource#addInstance这个方法,其中,我们只需要关注PeerAwareInstanceRegistry的注册方法,代码如下

java 复制代码
@POST
@Consumes({"application/json", "application/xml"})
public Response addInstance(InstanceInfo info, @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) {
        logger.debug("Registering instance {} (replication={})", info.getId(), isReplication);
  // 以上省略参数校验,和其它代码
  registry.register(info, "true".equals(isReplication));
  return Response.status(204).build();  // 204 to be backwards compatible
}

具体的register逻辑如下(com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#register

java 复制代码
public void register(final InstanceInfo info, final boolean isReplication) {
    // 默认租期最大值:90秒,如果客户端没指定该参数,则默认90秒。超过90秒没收到客户端心跳就删除服务实例
    int leaseDuration = Lease.DEFAULT_DURATION_IN_SECS;
    // 获取客户端的值
    if (info.getLeaseInfo() != null && info.getLeaseInfo().getDurationInSecs() > 0) {
        leaseDuration = info.getLeaseInfo().getDurationInSecs();
    }
    // 重点关注 调用父类方法真正注册
    super.register(info, leaseDuration, isReplication);
    replicateToPeers(Action.Register, info.getAppName(), info.getId(), info, null, isReplication);
}

在PeerAwareInstanceRegistryImpl的register方法里,调用了父类的register完成注册。它的父类是AbstractInstanceRegistry,我们来看下AbstractInstanceRegistryregister方法。register的逻辑我都注释在代码里了,如下:

java 复制代码
public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
    // 加上读锁,当服务正在注册时,不允许其他线程获取服务。具体可以查看AbstractInstanceRegistry写锁的位置
    read.lock();
    try {
       // registrant.getAppName()就是应用名称,一般通过Spring.application.name属性指定。假如一个微服务名为SPRING-SERVICEA,它可以有多个实例。
       // 所以这里通过服务名称可能会有多个获取服务实例。因此返回值为Map
        Map<String, Lease<InstanceInfo>> gMap = registry.get(registrant.getAppName());
        // Eureka内部统计注册次数。非核心逻辑
        REGISTER.increment(isReplication);
        // 第一次进来一定为null。那么初始化gMap并放入registry中
        if (gMap == null) {
            final ConcurrentHashMap<String, Lease<InstanceInfo>> gNewMap = new ConcurrentHashMap<String, Lease<InstanceInfo>>();
            gMap = registry.putIfAbsent(registrant.getAppName(), gNewMap);
            // 上面明明都调用putIfAbsent赋值了,怎么还要判断gMap是不是null呢?因为这里可能有多个线程同时进入,第一个线程put成功后,第二个线程自然会失败
            // 不过这里的逻辑就是给一个初始值。
            if (gMap == null) {
                gMap = gNewMap;
            }
        }
        // 获取主机ID对应的注册实例信息,实例ID是唯一标识,这里是服务名+端口号,比如 SPRING-SERVICEA:12000
        // 第一次进来这里肯定是null
        Lease<InstanceInfo> existingLease = gMap.get(registrant.getId());
        // 如果注册信息已经存在,有可能是网络原因导致的重新注册,这时候只需要更新原来服务实例的一些属性即可
        if (existingLease != null && (existingLease.getHolder() != null)) {
            Long existingLastDirtyTimestamp = existingLease.getHolder().getLastDirtyTimestamp();
            Long registrationLastDirtyTimestamp = registrant.getLastDirtyTimestamp();
            if (existingLastDirtyTimestamp > registrationLastDirtyTimestamp) {
                registrant = existingLease.getHolder();
            }
        } 
        // 第一次进来走else分支 
        else {
            // The lease does not exist and hence it is a new registration
            synchronized (lock) {
                if (this.expectedNumberOfClientsSendingRenews > 0) {
                    // 设置阈值,超过0.85的比例
                    this.expectedNumberOfClientsSendingRenews = this.expectedNumberOfClientsSendingRenews + 1;
                    updateRenewsPerMinThreshold();
                }
            }
            logger.debug("No previous lease information found; it is new registration");
        }
        // 新建一个服务租赁实例
        Lease<InstanceInfo> lease = new Lease<>(registrant, leaseDuration);
        if (existingLease != null) {
            lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp());
        }
        // put实例,相当于注册实例成功!
        gMap.put(registrant.getId(), lease);
        // 最近1000条最新注册的主机信息,解析在下面
        recentRegisteredQueue.add(new Pair<Long, String>(
                System.currentTimeMillis(),
                registrant.getAppName() + "(" + registrant.getId() + ")"));
        // 省略一些状态覆盖的代码......状态覆盖后面再说
    } finally {
        // 解锁,让其他线程可以读
        read.unlock();
    }
}

上面有一段代码是recentRegisteredQueue.add,这个是Eureka自定义的一个队列,队列大小是1000,对应了控制台上的一个标签页,用来展示最近1000条的注册信息,如下。同时Eureka也还有recentCanceledQueue------用来记录最近1000条服务下线的实例

相关推荐
元Y亨H2 小时前
微服务架构核心组件、职责与交互全解析
spring cloud
李慕婉学姐3 小时前
【开题答辩过程】以《基于JAVA的校园即时配送系统的设计与实现》为例,不知道这个选题怎么做的,不知道这个选题怎么开题答辩的可以进来看看
java·开发语言·数据库
奋进的芋圆5 小时前
Java 延时任务实现方案详解(适用于 Spring Boot 3)
java·spring boot·redis·rabbitmq
sxlishaobin5 小时前
设计模式之桥接模式
java·设计模式·桥接模式
model20055 小时前
alibaba linux3 系统盘网站迁移数据盘
java·服务器·前端
荒诞硬汉5 小时前
JavaBean相关补充
java·开发语言
提笔忘字的帝国5 小时前
【教程】macOS 如何完全卸载 Java 开发环境
java·开发语言·macos
2501_941882486 小时前
从灰度发布到流量切分的互联网工程语法控制与多语言实现实践思路随笔分享
java·开发语言
華勳全栈6 小时前
两天开发完成智能体平台
java·spring·go
alonewolf_996 小时前
Spring MVC重点功能底层源码深度解析
java·spring·mvc