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条服务下线的实例

相关推荐
天河归来13 分钟前
springboot框架redis开启管道批量写入数据
java·spring boot·redis
合作小小程序员小小店19 分钟前
web网页,在线%食谱推荐系统%分析系统demo,基于vscode,uniapp,vue,java,jdk,springboot,mysql数据库
vue.js·spring boot·vscode·spring·uni-app
张先shen22 分钟前
Elasticsearch RESTful API入门:全文搜索实战
java·大数据·elasticsearch·搜索引擎·全文检索·restful
codervibe23 分钟前
如何用 Spring Security 构建无状态权限控制系统(含角色菜单控制)
java·后端
codervibe26 分钟前
项目中如何用策略模式实现多角色登录解耦?(附实战代码)
java·后端
TCChzp28 分钟前
synchronized全链路解析:从字节码到JVM内核的锁实现与升级策略
java·jvm
大葱白菜29 分钟前
🧩 Java 枚举详解:从基础到实战,掌握类型安全与优雅设计
java·程序员
笑衬人心。31 分钟前
在 Mac 上安装 Java 和 IntelliJ IDEA(完整笔记)
java·macos·intellij-idea
SimonKing38 分钟前
颠覆传统IO:零拷贝技术如何重塑Java高性能编程?
java·后端·程序员
sniper_fandc1 小时前
SpringBoot系列—MyBatis(xml使用)
java·spring boot·mybatis