概述
注册中心已经跑起来了,空列表一个实例都没有,接下来就是把 user-service、order-service 注册进去。这件事本身不难------引一个依赖、写两行配置就完事------但「谁需要注册」这个前提很容易想歪,很多人以为只有服务提供者才要注册,结果消费者拉不到服务列表,卡一整天。
纲要
- 注册对象 :提供者
user-service与消费者order-service都要注册,理由在下面单独说 - 三步改造
- 引入
spring-cloud-starter-netflix-eureka-client依赖 application.yml配spring.application.name与eureka.client.service-url.defaultZone- 启动类是否要加
@EnableDiscoveryClient(新版本可省,源码情况见正文)
- 引入
- 核心认知 :
spring.application.name是服务在注册中心里的唯一标识,后续lb://userservice、@FeignClient("userservice")全靠它匹配 - 验证方式 :访问
http://localhost:10086,看Instances currently registered with Eureka列表与实例状态 - 多实例:同一个服务用不同端口启动两次,控制台出现两个实例,这是负载均衡的前提
- 实战坑 :
defaultZone漏写/eureka、服务名带大写/下划线、消费者忘引 client、localhost与127.0.0.1混用
先想清楚:到底谁要注册进来
先纠正一个高频误解。很多人的直觉是「提供者提供服务,所以要注册;消费者只是来调用的,不用注册」。这个直觉在 Eureka 里是错的。
Eureka 的 client 依赖把服务注册 和服务发现 封装在了一起,引入它同时获得两种能力。order-service 需要知道 userservice 有哪些实例、IP 端口是多少,这份信息只能从注册中心拉------它自己不是注册中心的一员,凭什么拉?所以它也得注册进去,成为一个「既是消费者、也可能是提供者」的客户端。
| 服务 | 角色 | 是否注册 | 注册后拿到什么 |
|---|---|---|---|
user-service |
服务提供者 | 要 | 自己的信息进入注册表,被消费者发现 |
order-service |
服务消费者 | 要 | 能拉取 userservice 的实例列表做负载均衡 |
eureka-server |
注册中心 | 自身也配 client | 启动时把自己的地址配进 defaultZone,集群场景下互相注册 |
所以改造动作是两个模块各做一遍,写法几乎一样,只是 name 不同。
#mermaid-svg-onqz9yUqT1WSRU2a{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-onqz9yUqT1WSRU2a .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-onqz9yUqT1WSRU2a .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-onqz9yUqT1WSRU2a .error-icon{fill:#552222;}#mermaid-svg-onqz9yUqT1WSRU2a .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-onqz9yUqT1WSRU2a .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-onqz9yUqT1WSRU2a .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-onqz9yUqT1WSRU2a .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-onqz9yUqT1WSRU2a .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-onqz9yUqT1WSRU2a .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-onqz9yUqT1WSRU2a .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-onqz9yUqT1WSRU2a .marker{fill:#333333;stroke:#333333;}#mermaid-svg-onqz9yUqT1WSRU2a .marker.cross{stroke:#333333;}#mermaid-svg-onqz9yUqT1WSRU2a svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-onqz9yUqT1WSRU2a p{margin:0;}#mermaid-svg-onqz9yUqT1WSRU2a .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-onqz9yUqT1WSRU2a .cluster-label text{fill:#333;}#mermaid-svg-onqz9yUqT1WSRU2a .cluster-label span{color:#333;}#mermaid-svg-onqz9yUqT1WSRU2a .cluster-label span p{background-color:transparent;}#mermaid-svg-onqz9yUqT1WSRU2a .label text,#mermaid-svg-onqz9yUqT1WSRU2a span{fill:#333;color:#333;}#mermaid-svg-onqz9yUqT1WSRU2a .node rect,#mermaid-svg-onqz9yUqT1WSRU2a .node circle,#mermaid-svg-onqz9yUqT1WSRU2a .node ellipse,#mermaid-svg-onqz9yUqT1WSRU2a .node polygon,#mermaid-svg-onqz9yUqT1WSRU2a .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-onqz9yUqT1WSRU2a .rough-node .label text,#mermaid-svg-onqz9yUqT1WSRU2a .node .label text,#mermaid-svg-onqz9yUqT1WSRU2a .image-shape .label,#mermaid-svg-onqz9yUqT1WSRU2a .icon-shape .label{text-anchor:middle;}#mermaid-svg-onqz9yUqT1WSRU2a .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-onqz9yUqT1WSRU2a .rough-node .label,#mermaid-svg-onqz9yUqT1WSRU2a .node .label,#mermaid-svg-onqz9yUqT1WSRU2a .image-shape .label,#mermaid-svg-onqz9yUqT1WSRU2a .icon-shape .label{text-align:center;}#mermaid-svg-onqz9yUqT1WSRU2a .node.clickable{cursor:pointer;}#mermaid-svg-onqz9yUqT1WSRU2a .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-onqz9yUqT1WSRU2a .arrowheadPath{fill:#333333;}#mermaid-svg-onqz9yUqT1WSRU2a .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-onqz9yUqT1WSRU2a .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-onqz9yUqT1WSRU2a .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-onqz9yUqT1WSRU2a .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-onqz9yUqT1WSRU2a .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-onqz9yUqT1WSRU2a .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-onqz9yUqT1WSRU2a .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-onqz9yUqT1WSRU2a .cluster text{fill:#333;}#mermaid-svg-onqz9yUqT1WSRU2a .cluster span{color:#333;}#mermaid-svg-onqz9yUqT1WSRU2a div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-onqz9yUqT1WSRU2a .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-onqz9yUqT1WSRU2a rect.text{fill:none;stroke-width:0;}#mermaid-svg-onqz9yUqT1WSRU2a .icon-shape,#mermaid-svg-onqz9yUqT1WSRU2a .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-onqz9yUqT1WSRU2a .icon-shape p,#mermaid-svg-onqz9yUqT1WSRU2a .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-onqz9yUqT1WSRU2a .icon-shape .label rect,#mermaid-svg-onqz9yUqT1WSRU2a .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-onqz9yUqT1WSRU2a .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-onqz9yUqT1WSRU2a .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-onqz9yUqT1WSRU2a :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 写入注册表
服务发现
eureka-server :10086
注册中心
user-service :8081
注册:服务名 + IP + 端口
order-service :8080
注册后可拉取 userservice 列表
userservice -> 8081, 8082
改造模块一:user-service
改造前的真实源码长这样,pom.xml 里只有 web、mysql、mybatis 三个依赖,application.yml 只有一个 server.port 和数据源,启动类只有 @SpringBootApplication 和 @MapperScan------跟 Eureka 一点关系都没有。下面三步就是把它接进注册中心。
先看工程结构,确认改哪个文件。
tree
cloud-demo
├── pom.xml # 父工程,统一管理 spring-cloud 版本
├── user-service
│ ├── pom.xml # ① 加 eureka-client 依赖
│ └── src/main
│ ├── java/cn/itcast/user/UserApplication.java # ③ 启动类
│ └── resources/application.yml # ② 加服务名与注册中心地址
└── order-service
├── pom.xml
└── src/main
├── java/cn/itcast/order/OrderApplication.java
└── resources/application.yml
第一步:引入客户端依赖
在 user-service/pom.xml 的 <dependencies> 里加 spring-cloud-starter-netflix-eureka-client。
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>cloud-demo</artifactId>
<groupId>cn.itcast.demo</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>user-service</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!--eureka 客户端:同时提供"服务注册"与"服务发现"两项能力-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
<build>
<finalName>app</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
这里没写 <version> ,因为父工程 cloud-demo 用 dependencyManagement 导入了 spring-cloud-dependencies(本例版本 Hoxton.SR10,对应 SpringBoot 2.3.9.RELEASE),版本由它统一兜底。注意别手贱把服务端依赖 eureka-server 也引进来------那是注册中心该引的,引错了会平白启动一个 Server 出来。
第二步:配置服务名与注册中心地址
在 user-service/src/main/resources/application.yml 里加两段。原文件已经有 server 和 spring.datasource,spring.application.name 要挂在现有的 spring 节点下,不能新起一个 spring: 跟原来的打架。
yaml
server:
port: 8081
spring:
application:
name: userservice # 服务名,注册到 Eureka 的唯一标识
datasource:
url: jdbc:mysql://localhost:3306/cloud_user?useSSL=false
username: root
password: 123
driver-class-name: com.mysql.jdbc.Driver
eureka:
client:
service-url:
defaultZone: http://localhost:10086/eureka/ # 注册中心地址,注意 /eureka 后缀
mybatis:
type-aliases-package: cn.itcast.user.pojo
configuration:
map-underscore-to-camel-case: true
logging:
level:
cn.itcast: debug
pattern:
dateformat: MM-dd HH:mm:ss:SSS
这段配置跟之前配 eureka-server 的几乎一模一样,因为注册中心自己也要「注册到自己」------配服务名和地址就是注册这个动作的全部条件,跟客户端还是服务端身份无关。
YAML 有个很坑的细节:同一个 key 只能出现一次 。如果原来的文件里已经有 spring:,你把拷贝来的 spring: application: name: ... 整段粘上去,就会变成两个 spring: 根节点,SpringBoot 启动直接报解析异常。正确做法是把 application 这段合并到已有的 spring 下面,不要另起炉灶。工程里从 eureka-server 粘贴配置时最容易踩这个。
第三步:启动类要不要加 @EnableDiscoveryClient
先说结论:本例 SpringCloud Hoxton.SR10 下,不加也能注册成功 。看真实源码,改造前 UserApplication 就长这样。
java
package cn.itcast.user;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@MapperScan("cn.itcast.user.mapper")
@SpringBootApplication
public class UserApplication {
public static void main(String[] args) {
SpringApplication.run(UserApplication.class, args);
}
}
原因在于新版 SpringCloud 通过 spring.factories 的自动装配机制,只要类路径下存在 eureka-client,EurekaClientAutoConfiguration 就会自动生效,@EnableDiscoveryClient 属于可省。加上它也不会出错,语义上更直白------告诉读代码的人「这是个注册中心客户端」。所以两种写法都对:
java
package cn.itcast.user;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@MapperScan("cn.itcast.user.mapper")
@SpringBootApplication
@EnableDiscoveryClient // 新版本可省,加上更明确
public class UserApplication {
public static void main(String[] args) {
SpringApplication.run(UserApplication.class, args);
}
}
老版本(如 Dalston、Edgware 早期)必须加这个注解才会开启注册,如果你是跟着老教程学的,别把「不用加」当成通用结论。
spring.application.name 到底管什么
这一步值得单独拎出来讲,因为它是后面所有跨服务调用的地基。
spring.application.name 不是随手起的显示名,它是服务在注册中心里的唯一标识,直接决定三件事:
- 控制台
Application列里显示什么(Eureka 会把它转成大写,userservice显示为USERSERVICE) - 网关里
lb://userservice的userservice部分怎么写 - Feign 里
@FeignClient("userservice")的括号里填什么
名字对不对,决定后面能不能调通。举几个会翻车的例子:
| 写法 | 问题 | 后果 |
|---|---|---|
userService(驼峰) |
Eureka 统一转大写存成 USERSERVICE |
lb://userService 匹配不上,报 Unable to find instance for userService |
user_service(下划线) |
与 user-service 不是同一个名字 |
提供者注册的是 USER_SERVICE,消费者找 userservice,找不到 |
两个服务用同一个 name |
同名即同服务 | 注册表里混成一批实例,负载均衡把请求打到错误的服务上 |
经验做法:服务名全小写、用中划线或直接连写 ,并且保证提供方和消费方引用的字符串一字不差。改名前先全局搜一遍 lb:// 和 @FeignClient 里的字符串。
改造模块二:order-service
消费者一模一样做三步,只是 name 换成 orderservice,端口沿用源码里的 8080。
依赖段与 user-service 完全相同,照搬即可。
xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
order-service/src/main/resources/application.yml:
yaml
server:
port: 8080
spring:
application:
name: orderservice # 消费者自己的服务名
datasource:
url: jdbc:mysql://localhost:3306/cloud_order?useSSL=false
username: root
password: 123
driver-class-name: com.mysql.jdbc.Driver
eureka:
client:
service-url:
defaultZone: http://localhost:10086/eureka/
mybatis:
type-aliases-package: cn.itcast.order.pojo
configuration:
map-underscore-to-camel-case: true
logging:
level:
cn.itcast: debug
pattern:
dateformat: MM-dd HH:mm:ss:SSS
order-service 的启动类 OrderApplication 与 user-service 同理,加不加 @EnableDiscoveryClient 都能跑通。
这里又有人会问:order-service 明明是被调用的提供者角色都没有,为什么还要写 name?因为它要按名字去注册表里查别人,查询接口本身不需要自己的名字,但 Eureka 的注册流程无差别对待所有 client------你注册进来,就必须有个名字占位。
验证注册结果
顺序上先起 eureka-server,再起 user-service、order-service。启动顺序不影响最终注册,只影响首次拉取时能不能立刻拿到别人------本机开发先起注册中心最省事。
访问控制台 http://localhost:10086,页面下方 Instances currently registered with Eureka 一节会长这样。
text
Instances currently registered with Eureka
Application AMIs Availability Zones Status
USERSERVICE n/a (1) UP (192.168.1.10:8081)
ORDERSERVICE n/a (1) UP (192.168.1.10:8080)
General Info
total-avail-memory : 256mb
...
几个状态要认识:
| 现象 | 含义 | 排查方向 |
|---|---|---|
列表出现 USERSERVICE + UP (ip:port) |
注册成功 | 无 |
服务名显示为 UNKNOWN |
没配 spring.application.name |
补上服务名,重启 |
状态是 DOWN |
实例认为自己是宕机状态 | 看服务自身健康检查、端口是否真的在监听 |
| 应用名都看不到 | client 压根没注册上去 | 检查依赖是否引入、defaultZone 地址与后缀是否正确 |
| 服务名在、实例数为 0 | 注册过了但实例被剔除 | 心跳断了,看服务进程是否已退出 |
重启客户端时控制台可能有十几秒延迟才刷新,因为实例列表默认 30 秒同步一次,别立刻下结论说没注册上。
多实例注册
一个服务只出现一个实例,负载均衡就无从谈起。本机没条件部署两台机器,用 IDEA 复制一个启动配置、换个端口即可模拟。
做法:右键 UserApplication 的启动配置 → Copy Configuration → 起个名字,在 Environment 的 VM options 里加 -Dserver.port=8082(-D 是 JVM 设置系统属性的方式,server.port 正是 yml 里那个键,优先级更高,会覆盖 application.yml 里的 8081),启动第二个实例。
text
VM options: -Dserver.port=8082
两个实例用同一份代码、同一套依赖配置,只有运行端口不同。刷新控制台:
text
Application AMIs Availability Zones Status
USERSERVICE n/a (2) UP (192.168.1.10:8082)
UP (192.168.1.10:8081)
ORDERSERVICE n/a (1) UP (192.168.1.10:8080)
Availability Zones 那一列的 (2) 就是实例数量,下面挂两个 UP 地址。
#mermaid-svg-oISzMBla4zl99W07{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-oISzMBla4zl99W07 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-oISzMBla4zl99W07 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-oISzMBla4zl99W07 .error-icon{fill:#552222;}#mermaid-svg-oISzMBla4zl99W07 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-oISzMBla4zl99W07 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-oISzMBla4zl99W07 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-oISzMBla4zl99W07 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-oISzMBla4zl99W07 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-oISzMBla4zl99W07 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-oISzMBla4zl99W07 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-oISzMBla4zl99W07 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-oISzMBla4zl99W07 .marker.cross{stroke:#333333;}#mermaid-svg-oISzMBla4zl99W07 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-oISzMBla4zl99W07 p{margin:0;}#mermaid-svg-oISzMBla4zl99W07 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-oISzMBla4zl99W07 .cluster-label text{fill:#333;}#mermaid-svg-oISzMBla4zl99W07 .cluster-label span{color:#333;}#mermaid-svg-oISzMBla4zl99W07 .cluster-label span p{background-color:transparent;}#mermaid-svg-oISzMBla4zl99W07 .label text,#mermaid-svg-oISzMBla4zl99W07 span{fill:#333;color:#333;}#mermaid-svg-oISzMBla4zl99W07 .node rect,#mermaid-svg-oISzMBla4zl99W07 .node circle,#mermaid-svg-oISzMBla4zl99W07 .node ellipse,#mermaid-svg-oISzMBla4zl99W07 .node polygon,#mermaid-svg-oISzMBla4zl99W07 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-oISzMBla4zl99W07 .rough-node .label text,#mermaid-svg-oISzMBla4zl99W07 .node .label text,#mermaid-svg-oISzMBla4zl99W07 .image-shape .label,#mermaid-svg-oISzMBla4zl99W07 .icon-shape .label{text-anchor:middle;}#mermaid-svg-oISzMBla4zl99W07 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-oISzMBla4zl99W07 .rough-node .label,#mermaid-svg-oISzMBla4zl99W07 .node .label,#mermaid-svg-oISzMBla4zl99W07 .image-shape .label,#mermaid-svg-oISzMBla4zl99W07 .icon-shape .label{text-align:center;}#mermaid-svg-oISzMBla4zl99W07 .node.clickable{cursor:pointer;}#mermaid-svg-oISzMBla4zl99W07 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-oISzMBla4zl99W07 .arrowheadPath{fill:#333333;}#mermaid-svg-oISzMBla4zl99W07 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-oISzMBla4zl99W07 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-oISzMBla4zl99W07 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-oISzMBla4zl99W07 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-oISzMBla4zl99W07 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-oISzMBla4zl99W07 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-oISzMBla4zl99W07 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-oISzMBla4zl99W07 .cluster text{fill:#333;}#mermaid-svg-oISzMBla4zl99W07 .cluster span{color:#333;}#mermaid-svg-oISzMBla4zl99W07 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-oISzMBla4zl99W07 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-oISzMBla4zl99W07 rect.text{fill:none;stroke-width:0;}#mermaid-svg-oISzMBla4zl99W07 .icon-shape,#mermaid-svg-oISzMBla4zl99W07 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-oISzMBla4zl99W07 .icon-shape p,#mermaid-svg-oISzMBla4zl99W07 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-oISzMBla4zl99W07 .icon-shape .label rect,#mermaid-svg-oISzMBla4zl99W07 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-oISzMBla4zl99W07 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-oISzMBla4zl99W07 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-oISzMBla4zl99W07 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 拉取列表后负载均衡
eureka-server :10086
USERSERVICE
两个实例
实例 A 8081
实例 B 8082
order-service :8080
这是后面 Ribbon / LoadBalancer 做负载均衡的前提------注册表里只有一个实例,均衡算法再聪明也没得选。
实战坑
defaultZone 漏写 /eureka 后缀。 写成 http://localhost:10086 是最常见的错,客户端会去请求根路径,连不上注册接口,日志里反复刷 Cannot execute request on any known server,而且服务不会报错退出,就那样干耗着。正确值要走 /eureka。末尾多一个 / 无所谓,.../eureka 和 .../eureka/ 都能识别,但中间那段 /eureka 不能少。
服务名带大写或下划线。 Eureka 会把应用名转成大写存储并查询,userService 在控制台变成 USERSERVICE,而你在 lb://userService 里写的是原样字符串,两下一对比就对不上。统一全小写、用中划线,能少掉一类玄学问题。
消费者忘引 eureka-client。 order-service 只加了 web 和 mybatis,RestTemplate 里写 http://userservice/user/1 就报 UnknownHostException: userservice------因为它既没注册自己、也没从注册中心拉列表,userservice 这个主机名它根本不认识。消费者和提供者都要引 client。
localhost 与 127.0.0.1 混用。 注册时用 localhost,访问控制台却用 127.0.0.1,多数情况没问题;但如果父工程 cloud-demo 被人把 hosts 改过,或者某些环境下 localhost 解析到 IPv6 的 ::1 而 Eureka 只监听了 IPv4,就会出现「注册地址是一个、调用地址是另一个」的错位。团队里统一用 127.0.0.1 最稳。
服务启动顺序。 先起 user-service 再起 eureka-server,服务不会崩,客户端会隔一段时间重试注册,只是控制台迟迟看不到。反过来先起注册中心,注册立即可见,排查更爽快。
配置节点写重。 从别处粘贴 eureka: 或 spring: 整段时,同名的根节点叠加会让 YAML 解析失败或后者覆盖前者,表现为「明明配了却不生效」。加配置前先看一遍文件里有没有同名 key。
API 速览
| 配置项 | 作用 | 示例值 |
|---|---|---|
spring.application.name |
服务名,注册中心的唯一标识 | userservice |
eureka.client.service-url.defaultZone |
注册中心地址,必须带 /eureka |
http://localhost:10086/eureka/ |
eureka.client.register-with-eureka |
是否把自己注册上去,默认 true |
false(纯拉取场景可关) |
eureka.client.fetch-registry |
是否拉取注册表,默认 true |
false(纯提供者场景可关) |
eureka.instance.instance-id |
实例在控制台显示的名字,可自定义 | ${spring.cloud.client.ip-address}:${server.port} |
| 依赖 | 引入方 | 说明 |
|---|---|---|
spring-cloud-starter-netflix-eureka-server |
注册中心模块 | 提供 Server 能力,启动类加 @EnableEurekaServer |
spring-cloud-starter-netflix-eureka-client |
提供者与消费者 | 注册 + 发现一体,启动类 @EnableDiscoveryClient 可省 |
官方文档
- Spring Cloud Netflix --- Service Discovery: Eureka Clients
- Spring Cloud Commons --- @EnableDiscoveryClient
总结
服务注册的动作就是两步:引客户端依赖 + 配服务名与注册中心地址 。凡是看到「引入 eureka-client 并配上 defaultZone」,就是在做注册这件事,跟它自报家门是提供者还是消费者身份无关。
几个值得记住的点:
spring.application.name是后续lb://、@FeignClient匹配的字符串基础,写错了后患无穷,先定名再动代码。- 消费者也要注册,否则它没有拉取注册表的身份。
@EnableDiscoveryClient在新版本可省,但老教程必加,别把两种结论混着用。defaultZone的/eureka后缀、两个UP实例、控制台里的USERSERVICE大写显示,这三个是验证注册是否成功最直观的抓手。
注册完成,下一步就是让 order-service 从注册表里按服务名拉取实例并发起远程调用了。