
為了開發效率高效和業務邏輯清晰,越來越多的項目采用分布式系統。分布式最重要的就是注冊中心了。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
關於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工作原理,有助於幫助我們更好的理解分布式系統中的注冊中心,為了將來學習了解其他注冊中心提供理論基礎。
