为了开发效率高效和业务逻辑清晰,越来越多的项目采用分布式系统。分布式最重要的就是注册中心了。Eureka是SpringCloud原生提供的注册中心,来look一波吧。
引入依赖:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> <version>Greenwich.SR1</version> </dependency>
给启动类加上注解@EnableEurekaServer
@EnableEurekaServer @SpringBootApplication public class EurekaApplication { public static void main(String[] args) { SpringApplication.run(EurekaApplication.class, args); } }
配置一下yml文件:
#端口号 server: port: 8331 #Eureka实例名,集群中根据这里相互识别 eureka: instance: hostname: eureka #客户端 client: #是否开启注册服务,作为注册中心,就不注册了 register-with-eureka: false #是否拉取服务列表,这里我只提供服务给别的服务。 fetch-registry: false #注册中心地址 service-url: defaultZone: http://localhost:8331/eureka/
启动项目EurekaApplication ,浏览器访问 http://localhost :8331/,Euerka 服务器搭建成功了。
现在还没有东西注册进来。
引入依赖:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> <version>Greenwich.SR1</version> </dependency>
这回启动类注解变了,@EnableDiscoveryClient
@EnableDiscoveryClient @SpringBootApplication public class ProviderApplication { public static void main(String[] args) { SpringApplication.run(ProviderApplication.class, args); } }
接下来是配置文件:
#端口号 server: port: 8332 #Eureka实例名,集群中根据这里相互识别 spring: application: name: first-service eureka: #客户端 client: #注册中心地址 service-url: defaultZone: http://localhost:8331/eureka/
然后启动项目,过一会儿,刷新页面:
注册中心有了刚才那个服务了。这个叫做first-service注册到注册中心,它既可以叫做生产者,也可以被叫做消费者,因为它可以为别的服务提供接口,也可以调用其他服务提供的接口。总之,无论是生产者还是消费者,它都被叫做Client,要用@EnableDiscoveryClient注解。我不小心点进去这个注解里面,发现还有个参数, boolean autoRegister() default true
。这是是否项目已启动,该服务自动注册到注册中心。默认为自动。
除了@EnableDiscoveryClient这个注解以外,还可以使用另外一个注解@EnableEurekaClient。效果相同,如果是Eureka做注册中心的话,建议使用@EnableEurekaClient,如果是其他注册中心的话(例如阿里的nacos),建议使用@EnableDiscoveryClient。
要想运行起来一个个微服务,形成分布式系统,作为注册中心和其中服务,应该实现一下需求:
一个服务client注册到注册中心eureka,该client的信息会被存在一个Map中,实现了第一步。同时,client会拉取一份名单,名单里面有其他注册服务的信息,并且为了保证实时性,每30s会再从注册中心那边拉取一份名单信息,实现了第二步。为了确保注册中心实时知道哪些服务还存活着,需要每个client,每隔一段时间(默认30s)向注册中心发送一个心跳,告诉注册中心,我还在,注册中心那份名单拿上还会记录着这个client还可以用,实现了第三步。
先看一眼注册中心,也就是服务端Service,有个启动引导类EurekaBootStrap,其中有个方法:
@Override public void contextInitialized(ServletContextEvent event) { try { initEurekaEnvironment(); initEurekaServerContext(); ServletContext sc = event.getServletContext(); sc.setAttribute(EurekaServerContext.class.getName(), serverContext); } catch (Throwable e) { logger.error("Cannot bootstrap eureka server :", e); throw new RuntimeException("Cannot bootstrap eureka server :", e); } }
这方法是初始化Eureka方法的,现在我特别想知道注册中心是用什么数据结构存下客户端client信息的,所以我得去找注册中心为客户端client提供的注册接口,于是乎,点进initEurekaServerContext()这个方法看看,有个PeerAwareInstanceRegistry这个接口,再点进去看看,发现了
void register(InstanceInfo info, boolean isReplication);
看下它的实现类
@Override public void register(final InstanceInfo info, final boolean isReplication) { 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); }
replicateToPeers() 这个方法用于注册中心是集群的情况,主要是注册完之后,同步该服务给其他eureka节点。
public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) { try { read.lock(); Map<String, Lease<InstanceInfo>> gMap = registry.get(registrant.getAppName()); REGISTER.increment(isReplication); if (gMap == null) { final ConcurrentHashMap<String, Lease<InstanceInfo>> gNewMap = new ConcurrentHashMap<String, Lease<InstanceInfo>>(); gMap = registry.putIfAbsent(registrant.getAppName(), gNewMap); if (gMap == null) { gMap = gNewMap; } } Lease<InstanceInfo> existingLease = gMap.get(registrant.getId()); ... 仿佛有好多代码... } finally { read.unlock(); } }
目测 registry 应该就是储存着所有的服务,点一下看其结构。
private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry = new ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>();
最外层是线程安全的ConcurrentHashMap,key值是registrant.getAppName(),也就是实例中的应用名称 first-service。 里面又是一个ConcurrentHashMap(代码里面是Map接口,但其实肯定是ConcurrentHashMap,你可以看gNewMap 对象怎么new的)。里面这个key是registrant.getId()实例id,value 是Lease<InstanceInfo>,这里面存着服务实例和过期时间什么的。ok,具体注册,今天找到地方,先不看了。
关于client端,需要定时拉取服务名单,定时发送注册中心一个心跳。所以用了两个定时器。
在DiscoveryClient 类中,有个initScheduledTasks() 这个方法,是初始化那两个定时器的,简略代码如下:
private void initScheduledTasks() { if (clientConfig.shouldFetchRegistry()) { // registry cache refresh timer int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds(); int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound(); scheduler.schedule( new TimedSupervisorTask( "cacheRefresh", scheduler, cacheRefreshExecutor, registryFetchIntervalSeconds, TimeUnit.SECONDS, expBackOffBound, new CacheRefreshThread() ), registryFetchIntervalSeconds, TimeUnit.SECONDS); } if (clientConfig.shouldRegisterWithEureka()) { int renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs(); int expBackOffBound = clientConfig.getHeartbeatExecutorExponentialBackOffBound(); logger.info("Starting heartbeat executor: " + "renew interval is: {}", renewalIntervalInSecs); // Heartbeat timer scheduler.schedule( new TimedSupervisorTask( "heartbeat", scheduler, heartbeatExecutor, renewalIntervalInSecs, TimeUnit.SECONDS, expBackOffBound, new HeartbeatThread() ), renewalIntervalInSecs, TimeUnit.SECONDS); } else { logger.info("Not registering with Eureka server per configuration"); }
SpringBoot让集成Eureka非常的简单,本篇提供了快速入门的示例。今后还要考虑到注册中心集群的问题。当然,现在还有更好用的注册中心,阿里的nacos,不仅有注册中心的功能,同时还继承了配置中心的功能。了解Eureka工作原理,有助于帮助我们更好的理解分布式系统中的注册中心,为了将来学习了解其他注册中心提供理论基础。