Spring Cloud 微服务
什么是微服务?
微服务是一种将应用划分为多个独立服务的架构风格,服务通过轻量级通信协议互相协作,每个服务负责单一功能,可独立开发、部署和运行。
微服务架构的优缺点
- 优点:
- 独立部署,技术选型灵活;
- 容错性强,支持快速迭代;
- 高扩展性,更易于维护。
- 缺点:
- 系统复杂度高;
- 数据一致性和分布式事务管理难;
- 运维成本和性能开销增加。
单体架构向微服务架构的演进
- 单体架构缺点: 扩展性差、维护困难、更新风险高,无法满足复杂业务需求。
- 演进过程: 拆分单体模块为独立服务,定义接口,采用API网关统一入口,通过分布式通信和独立数据库实现服务化。
- 迁移策略: 分步迁移,优先处理高频模块,确保新旧系统平稳过渡。
1. 服务注册与发现
服务注册与发现是微服务架构的基础组件,用于管理和定位服务实例,帮助实现服务之间的动态通信和解耦。通过注册中心,服务提供者可以注册自身信息,服务消费者可以根据服务名动态发现目标服务,从而避免硬编码服务地址的问题。
Nacos 是由阿里巴巴开源的一个易用的动态服务发现、配置管理和服务治理平台。相较于其他注册中心(Eureka、Consul),具有明显的配置管理与动态扩展方面的优势。
它是一个专门用于服务注册和发现的程序,同时具备配置管理功能。在微服务架构中,它类似于一个“电话簿”,服务提供者将自己的信息注册到 Nacos,服务消费者通过 Nacos 动态发现目标服务,而无需硬编码服务的地址。
服务提供者注册
服务提供者启动后,会通过 Nacos 客户端向 Nacos 注册自己的服务实例信息,包括:
- 服务名称:如
user-service
。- 实例信息:包括实例的 IP 和端口。
- 元数据:如服务版本、环境信息。
注册的信息存储在 Nacos 的注册表中。
服务消费者发现
服务消费者调用其他服务时,会通过 Nacos 客户端向注册中心查询目标服务的信息,动态获取服务实例的地址列表,进行负载均衡调用。动态维护
- 心跳机制:服务实例定期向 Nacos 发送心跳,证明自己还在运行。
- 服务下线:如果服务实例长时间未发送心跳,Nacos 会将其标记为不可用并从注册表中移除。
1.1 服务注册
1.1.1 什么是服务注册
服务注册是指微服务实例启动时,将自己的地址(IP 和端口)、服务名及元数据注册到注册中心,供其他服务发现和调用。服务注册是服务通信的基础,确保微服务可以通过服务名动态定位。
1.1.2 服务注册的工作原理
1.服务启动时注册: 服务实例在启动时,通过注册客户端(如 nacos-client
或 eureka-client
)向注册中心发送注册请求,包含以下信息:
- 服务名称:唯一标识服务的逻辑名称(如
user-service
)。 - 实例信息:实例的 IP 地址、端口号、元数据(如版本号)。
- 健康信息:服务是否健康的标记。
2.注册中心保存信息: 注册中心将这些信息存储在注册表中,以供其他服务发现。
3.动态更新和维护:
- 心跳机制:服务实例定期向注册中心发送心跳包,表明服务正常运行。
- 服务下线:服务实例关闭或异常时,注册中心会移除该实例。
1.1.3 示例:使用 Nacos 实现服务注册
(1)安装 Nacos 注册中心
下载 Nacos:Nacos 官方下载链接。
启动 Nacos:
- 解压后,在bin目录下运行以下命令:
.\startup.cmd -m standalone
- 默认控制台地址:
http://localhost:8848/nacos
。
(2)Spring Boot 项目集成 Nacos
引入依赖: 在 pom.xml
文件中添加以下依赖:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>2021.0.5.0</version> <!-- 根据 Spring Cloud 版本选择 -->
</dependency>
配置服务注册信息: 在 application.properties
文件中,添加如下配置:
spring.application.name=user-service # 当前服务名
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848 # Nacos 注册中心地址
server.port=8080 # 服务启动的端口
启用服务注册: 在主类中添加 @EnableDiscoveryClient
注解,启用服务注册功能:
@SpringBootApplication
@EnableDiscoveryClient // 启用服务注册功能
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
}
启动服务并验证:
- 启动
user-service
项目。 - 打开 Nacos 控制台(
http://localhost:8848/nacos
),在 服务管理 -> 服务列表 中,可以看到user-service
已成功注册。
1.2 服务发现
1.2.1 什么是服务发现
服务发现是指服务消费者通过注册中心动态获取目标服务的实例信息(IP 地址、端口等),并发起调用。
通过服务发现,服务之间无需提前配置地址,解决了服务动态扩缩容和部署复杂性问题。
1.2.2 服务发现的工作原理
- 查询服务列表:服务消费者通过注册客户端向注册中心发送查询请求,指定服务名(如
product-service
)。 - 返回实例列表:注册中心返回目标服务的所有实例信息(包括 IP、端口和元数据)。
- 负载均衡选择实例:消费者根据负载均衡策略(如轮询、随机)选择一个实例并发起调用。
1.2.3 示例:服务调用
(1)创建服务提供者
新建服务 product-service
: 提供一个简单的商品查询接口:
@RestController
@RequestMapping("/product")
public class ProductController {
@GetMapping("/{id}")
public String getProduct(@PathVariable String id) {
return "Product ID: " + id;
}
}
配置注册信息: 在 application.properties
中:
spring.application.name=product-service # 服务名
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848 # 注册中心地址
server.port=8081 # 服务端口
(2)创建服务消费者
新建服务 user-service
: 消费者服务调用 product-service
。
引入 Feign 依赖: 在 pom.xml
文件中添加:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
定义 Feign 客户端: 使用 @FeignClient
调用远程服务:
@FeignClient("product-service") // 指定调用的目标服务
public interface ProductClient {
@GetMapping("/product/{id}")
String getProduct(@PathVariable("id") String id);
}
消费者 Controller: 调用 product-service
的接口:
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private ProductClient productClient;
@GetMapping("/product/{id}")
public String getProduct(@PathVariable String id) {
return productClient.getProduct(id);
}
}
配置注册信息: 在 application.properties
中:
spring.application.name=user-service # 服务名
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848 # 注册中心地址
server.port=8082 # 服务端口
启动服务并验证:
- 启动
product-service
和user-service
。 - 访问
http://localhost:8082/user/product/123
。 - 结果应返回:
Product ID: 123
1.3 服务健康检查
1.3.1 什么是服务健康检查
服务健康检查用于检测服务实例的运行状态,确保注册表中的服务是健康可用的。如果某服务实例不可用,注册中心会将其从注册表中移除,避免请求失败。
1.3.2 Nacos 健康检查机制
主动心跳检测:
- 服务实例定期向注册中心发送心跳包(默认每 5 秒),通知注册中心自己在线。
被动健康检查:
- 注册中心主动向服务实例发送探测请求(如 HTTP),检查其运行状态。
1.3.3 示例:模拟服务宕机
- 启动服务后,手动关闭
product-service
。 - 在 Nacos 控制台的 服务列表 中,可以看到
product-service
的实例状态变为不健康,并被移除。
2. 服务间通信
服务间通信是微服务架构中的核心环节,用于实现服务之间的协作。在微服务中,服务之间通常通过 HTTP 通信来实现数据交互。Spring Cloud 提供了多种方式来实现服务间通信,包括 Feign 和 RestTemplate,同时结合 Spring Cloud LoadBalancer 实现负载均衡。
2.1 Feign(声明式服务调用)
2.1.1 什么是 Feign
Feign 是 Spring Cloud 提供的声明式服务调用工具,它将 HTTP 请求抽象成 Java 接口,开发者只需定义接口方法和注解,就可以调用远程服务,无需手动构造 HTTP 请求。
2.1.2 Feign 的特点
- 声明式调用:通过接口方法调用远程服务,代码清晰易读。
- 集成负载均衡:与 Spring Cloud LoadBalancer 集成,可实现多实例负载均衡。
- 自动解析返回值:支持将远程服务返回的 JSON 自动映射为 Java 对象。
2.1.3 Feign 的工作原理
- 接口定义:开发者定义一个接口,使用
@FeignClient
注解标注目标服务名称。 - 方法映射:通过注解(如
@GetMapping
、@PostMapping
)指定 HTTP 请求的路径和参数。 - 代理实现:Spring Cloud 自动生成接口的代理类,将接口方法调用转换为 HTTP 请求。
- 负载均衡:Feign 与 Spring Cloud LoadBalancer 集成,通过服务名自动选择实例。
2.1.4 Feign 的使用
(1)引入依赖
在 pom.xml
中添加 Feign 依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
(2)配置 Feign
在 application.properties
中:
spring.application.name=user-service
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848 # Nacos 地址
server.port=8080
(3)定义 Feign 客户端
创建一个接口,用于调用远程服务:
@FeignClient("product-service") // 指定目标服务名称
public interface ProductClient {
@GetMapping("/product/{id}")
String getProduct(@PathVariable("id") String id);
}
(4)使用 Feign 客户端
在 Controller 中注入并使用 Feign 客户端:
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private ProductClient productClient;
@GetMapping("/product/{id}")
public String getProduct(@PathVariable String id) {
return productClient.getProduct(id); // 通过 Feign 调用远程服务
}
}
(5)启用 Feign
在主类上添加 @EnableFeignClients
注解:
@SpringBootApplication
@EnableFeignClients // 启用 Feign 功能
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
}
(6)测试服务调用
启动 product-service
和 user-service
后,访问:
http://localhost:8080/user/product/123
返回结果:
Product ID: 123
2.1.5 Feign 的超时与重试机制配置
在远程调用中,为了防止请求阻塞,可以设置超时时间和重试策略。
(1)超时配置: 在 application.properties
中:
feign.client.config.default.connectTimeout=5000 # 连接超时(毫秒)
feign.client.config.default.readTimeout=10000 # 读取超时(毫秒)
(2)重试配置: 通过 Feign 的配置启用重试:
feign.client.config.default.retryer=Retryer.Default
feign.client.config.default.maxAttempts=3 # 最大重试次数
feign.client.config.default.backoff=2000 # 重试间隔(毫秒)
2.1.6 Feign 的日志配置
Feign 提供了日志功能,可以记录请求和响应的详细信息,便于调试。
(1)启用 Feign 日志: 在 application.properties
中:
logging.level.feign=DEBUG # 将 Feign 日志级别设置为 DEBUG
(2)自定义日志级别: 通过配置类设置日志级别:
@Configuration
public class FeignConfig {
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL; // FULL 表示记录完整的请求和响应数据
}
}
2.2 RestTemplate(传统方式调用服务)
2.2.1 什么是 RestTemplate
RestTemplate 是 Spring 提供的同步 HTTP 客户端工具,用于构造和发送 HTTP 请求。虽然 RestTemplate 功能强大,但需要手动处理服务地址,代码复杂度较高。
2.2.2 RestTemplate 的基本使用
(1)引入依赖
Spring Boot 默认包含 RestTemplate 的依赖,无需额外引入。
(2)定义 RestTemplate Bean
在配置类中定义 RestTemplate Bean:
@Configuration
public class RestTemplateConfig {
@Bean
@LoadBalanced // 启用负载均衡
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
(3)使用 RestTemplate
在代码中调用远程服务:
@Autowired
private RestTemplate restTemplate;
@GetMapping("/user/product/{id}")
public String getProduct(@PathVariable String id) {
String url = "http://product-service/product/" + id; // 服务名替代硬编码地址
return restTemplate.getForObject(url, String.class);
}
2.3 Spring Cloud LoadBalancer
2.3.1 什么是 Spring Cloud LoadBalancer
Spring Cloud LoadBalancer 是 Spring Cloud 提供的客户端负载均衡器,支持根据服务名动态解析服务实例列表,并在请求时自动选择一个实例。
2.3.2 负载均衡的实现原理
- 服务名解析:
- 消费者通过服务名(如
product-service
)向 Spring Cloud LoadBalancer 发起请求。 - LoadBalancer 从服务注册中心(如 Eureka 或 Nacos)获取服务名对应的实例列表(IP 和端口)。
- 消费者通过服务名(如
- 负载均衡策略:
- 根据配置的负载均衡算法,从实例列表中选择一个可用的服务实例。
- 调用目标服务:
- 将请求转发到选定的服务实例,并返回响应结果。
2.3.3 配置与使用
在 application.properties
中无需额外配置,默认启用。
Spring Cloud LoadBalancer 默认使用 轮询策略(Round Robin)。
修改为随机策略
-
创建配置类: 自定义负载均衡策略为随机(Random):
@Configuration
public class LoadBalancerConfig {
@Bean
public ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(Environment environment,
LoadBalancerClientFactory clientFactory) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new RandomLoadBalancer(clientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
}
}
3. API 网关
API 网关是微服务架构中的入口组件,用于实现路由转发、统一认证、流量控制等功能。Spring Cloud Gateway 是目前推荐的高性能网关解决方案,基于非阻塞的 WebFlux,提供灵活的路由和过滤器机制。
3.1 什么是 API 网关
API 网关的核心功能:
- 路由转发:将客户端的请求根据规则转发到后端服务。
- 统一认证:实现集中式认证和权限校验,避免重复开发。
- 流量控制:通过限流和熔断保护后端服务。
- 日志监控:记录请求日志,便于分析和排查问题。
3.2 Spring Cloud Gateway 使用
3.2.1 引入依赖
在 pom.xml
文件中添加以下依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
spring-cloud-starter-gateway
:提供网关的核心功能。spring-boot-starter-data-redis
:为限流功能提供支持。
3.2.2 静态路由配置
在 application.yml
文件中配置简单的路由规则:
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
- id: user-service-route # 路由 ID
uri: http://localhost:8081 # 目标服务地址
predicates:
- Path=/user/** # 路由规则:匹配 /user/** 的请求
- id: product-service-route
uri: http://localhost:8082
predicates:
- Path=/product/**
server:
port: 8080 # 网关服务的端口
配置解析
id
:路由规则的唯一标识。uri
:目标服务的地址,可以是具体的http://localhost:8081
或服务名(结合服务注册中心使用)。predicates
:路由的条件,Path
表示按路径规则转发请求。
测试静态路由
启动 user-service
(8081)和 product-service
(8082),访问:
http://localhost:8080/user/123
转发到http://localhost:8081/user/123
http://localhost:8080/product/456
转发到http://localhost:8082/product/456
3.2.3 动态路由配置
Spring Cloud Gateway 可以结合服务注册中心(如 Nacos、Eureka)实现动态路由。动态路由会根据注册的服务名生成路由规则,避免手动配置。
在 application.yml
中启用服务发现:
spring:
cloud:
gateway:
discovery:
locator:
enabled: true # 启用服务发现
lower-case-service-id: true # 服务名自动转换为小写
动态路由规则
- 访问
http://localhost:8080/user-service/**
会自动转发到user-service
的实例。 - 访问
http://localhost:8080/product-service/**
会自动转发到product-service
的实例。
测试动态路由
- 启动注册中心(如 Nacos)和各微服务。
- 访问网关地址:
http://localhost:8080/user-service/user/123
http://localhost:8080/product-service/product/456
3.2.4 自定义过滤器
过滤器是 Gateway 的核心组件,可用于拦截和修改请求或响应,实现日志记录、鉴权等功能。
全局过滤器示例
定义全局过滤器
@Component
public class CustomGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 日志记录示例
String path = exchange.getRequest().getPath().toString();
System.out.println("Request Path: " + path);
// 添加自定义请求头
exchange.getRequest().mutate().header("X-Custom-Header", "CustomValue");
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0; // 优先级,值越小优先级越高
}
}
功能解析
- 日志记录:记录每个请求的路径。
- 添加请求头:为所有请求添加自定义请求头。
测试过滤器 启动网关服务,访问任意路由地址,例如 /user/123
,在控制台可以看到日志输出。
3.2.5 限流功能
限流功能通过 Redis 实现,使用 RequestRateLimiter
过滤器限制请求速率。
限流配置示例
在 application.yml
文件中:
spring:
cloud:
gateway:
routes:
- id: user-service-route
uri: http://localhost:8081
predicates:
- Path=/user/**
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10 # 每秒生成的令牌数
redis-rate-limiter.burstCapacity: 20 # 最大令牌数
key-resolver: "#{@remoteAddrKeyResolver}" # 限流的唯一标识(按 IP)
配置解析
redis-rate-limiter.replenishRate
:每秒生成的令牌数(允许的请求数)。redis-rate-limiter.burstCapacity
:令牌桶的最大容量(允许的瞬时请求量)。key-resolver
:限流依据,这里使用客户端 IP 作为唯一标识。
定义 KeyResolver
在配置类中定义限流的 KeyResolver:
@Configuration
public class RateLimiterConfig {
@Bean
public KeyResolver remoteAddrKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
}
}
测试限流功能
- 使用工具(如 Postman 或 JMeter)模拟并发请求。
- 超过限流阈值时,网关会返回 HTTP 429 状态码(Too Many Requests)。
4. 配置管理
在微服务架构中,随着服务数量的增加,不同服务需要管理大量的配置信息,例如数据库连接信息、服务端口、日志级别等。这些配置可能因环境(开发、测试、生产)不同而有所变化。配置管理的目标是集中化、动态化地管理这些配置文件,提升维护效率和系统灵活性。
推荐方案:Nacos 配置中心
-
集成方便、功能丰富,推荐在 Spring Cloud 项目中作为配置管理工具。
-
配合动态刷新和命名空间,能够满足多环境配置的需求。
备选方案:Spring Cloud Config
-
配置存储依赖 Git 或 SVN,更适合需要版本控制的场景。
-
配置刷新需要结合消息队列(如 RabbitMQ、Kafka)才能实现动态更新。
4.1 什么是配置中心
配置中心是一个专门用来集中管理微服务配置的服务,能够提供以下功能:
- 集中管理:统一管理所有微服务的配置文件。
- 环境隔离:支持不同环境(如开发、测试、生产)下的配置分离。
- 动态刷新:配置变更后,无需重启服务即可应用最新配置。
- 版本控制:对配置文件进行版本管理,便于回溯历史记录。
4.2 Nacos 配置中心(推荐)
Nacos 是阿里巴巴开源的微服务解决方案,既能充当服务注册中心,又可以作为配置中心使用,推荐在 Spring Cloud 微服务中使用。
4.2.1 Nacos 配置管理功能
- 集中管理配置:支持将多个微服务的配置文件统一存储到 Nacos 中。
- 多环境支持:按命名空间、分组、DataId 管理不同环境的配置。
- 动态刷新:通过 Spring Cloud 与 Nacos 集成,配置变更后能实时刷新。
4.2.2 Nacos 配置中心的基本使用
(1)安装和启动 Nacos
下载 Nacos:下载链接。
启动 Nacos:
sh startup.sh -m standalone
访问 Nacos 控制台:http://localhost:8848/nacos
。
(2)Spring Boot 集成 Nacos 配置
引入依赖 在 pom.xml
中添加 Nacos 的配置依赖:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
<version>2021.0.5.0</version>
</dependency>
配置 bootstrap.yml
配置与 Nacos 的连接信息:
spring:
application:
name: config-demo # 服务名称
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848 # Nacos 地址
file-extension: yaml # 配置文件格式(可选 yml 或 properties)
创建 Nacos 配置文件 在 Nacos 控制台添加配置:
- DataId:
config-demo.yaml
(对应服务名称加后缀) - Group:
DEFAULT_GROUP
(默认分组,可自定义) - 内容:
custom: message: Hello from Nacos! # 自定义配置
在代码中读取配置 使用 @Value
或 @ConfigurationProperties
注解读取配置:
@RestController
@RequestMapping("/config")
public class ConfigController {
@Value("${custom.message}")
private String message;
@GetMapping("/message")
public String getMessage() {
return message;
}
}
测试配置 启动服务后,访问 http://localhost:8080/config/message
,输出:
Hello from Nacos!
4.2.3 配置动态刷新
启用动态刷新 在 pom.xml
中引入 Spring Boot Actuator:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
配置 bootstrap.yml
开启动态刷新功能:
spring:
cloud:
nacos:
config:
refresh-enabled: true
management:
endpoints:
web:
exposure:
include: "*"
测试动态刷新
- 修改 Nacos 配置中心中的
custom.message
内容。 - 刷新页面访问
http://localhost:8080/config/message
,即可看到实时更新的内容。
4.3 Spring Cloud Config(备选)
Spring Cloud Config 是另一种配置管理方案,支持将配置文件存储在 Git、SVN 等版本管理工具中,并通过 Config Server 提供统一的访问接口。
4.3.1 Spring Cloud Config 的基本使用
架构图
- Config Server:配置中心服务,负责从 Git 拉取配置并提供给客户端。
- Config Client:微服务客户端,从 Config Server 拉取配置。
(1)创建 Config Server
引入依赖 在 pom.xml
中添加 Config Server 依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
配置 application.yml
配置 Config Server 的 Git 仓库地址:
spring:
application:
name: config-server
cloud:
config:
server:
git:
uri: https://github.com/your-repo/config-repo.git # Git 仓库地址
search-paths: config # 配置文件所在目录
server:
port: 8888
启动类 启用 Config Server 功能:
@SpringBootApplication
@EnableConfigServer // 启用 Config Server
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class, args);
}
}
(2)创建 Config Client
引入依赖 在 pom.xml
中添加 Config Client 依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
配置 bootstrap.yml
配置客户端与 Config Server 的连接:
spring:
application:
name: client-demo
cloud:
config:
uri: http://localhost:8888 # Config Server 地址
读取配置 在代码中读取配置内容:
@RestController
@RequestMapping("/config")
public class ConfigController {
@Value("${custom.message}")
private String message;
@GetMapping("/message")
public String getMessage() {
return message;
}
}
4.3.2 配置刷新与 Spring Cloud Bus 集成
引入依赖 在 pom.xml
中添加 Spring Cloud Bus 的依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
配置 application.yml
配置消息队列(如 RabbitMQ):
spring:
cloud:
bus:
enabled: true
rabbitmq:
host: localhost
username: guest
password: guest
刷新配置
- 修改 Git 仓库中的配置文件。
- 提交变更后,向
/actuator/bus-refresh
发送 POST 请求:curl -X POST http://localhost:8080/actuator/bus-refresh
- 客户端会实时刷新配置。
4.4 Nacos 与 Spring Cloud Config 对比
功能 | Nacos 配置中心 | Spring Cloud Config |
---|---|---|
配置存储 | 内置存储,支持多种格式(YAML、JSON 等) | 基于 Git 或其他版本管理工具 |
动态刷新 | 原生支持动态刷新 | 需结合 Spring Cloud Bus 实现 |
多环境支持 | 支持命名空间、分组管理环境 | 支持环境的配置文件分层(如 dev 、prod ) |
操作界面 | 提供友好的 Web 管理界面 | 无界面,仅支持 Git/SVN 管理 |
5. 服务容错与熔断
服务容错与熔断的目的是在微服务系统中保证部分功能可用,避免服务雪崩效应。当某些服务不可用时,系统通过熔断、降级、限流等方式保障服务的整体稳定性。
- 熔断、降级、重试:推荐使用 Resilience4j,简单轻量,功能满足大部分需求。
- 限流与监控:推荐使用 Sentinel,支持复杂规则,且提供完善的监控界面。
5.1 什么是服务容错与熔断
服务容错
服务容错是指在服务调用失败或超时时,提供备用处理逻辑,保证系统的部分功能仍然可用。
熔断
熔断是一种保护机制,当某服务的失败率达到一定阈值时,短路请求,不再调用该服务。熔断器一段时间后会进入“半开”状态,尝试恢复调用。
服务降级
服务降级是在目标服务不可用或超时时,为调用方提供备用处理逻辑,避免用户体验过度受影响。
限流
限流是通过限制接口的访问频率或并发量,防止服务因流量过大而崩溃。
5.2 Resilience4j
Resilience4j 是一款轻量级的服务容错库,支持熔断器、限流、重试等功能。
5.2.1 熔断器
熔断器用于检测服务调用的健康状况,并在服务连续失败时短路请求。Resilience4j 的熔断器支持三种状态:
- CLOSED(关闭):正常状态,允许所有请求通过。
- OPEN(打开):熔断状态,所有请求直接失败。
- HALF_OPEN(半开):测试状态,部分请求尝试调用目标服务。
配置熔断器
引入依赖 在 pom.xml
文件中添加 Resilience4j 的依赖:
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot2</artifactId>
</dependency>
在 application.yml
文件中配置熔断规则
resilience4j:
circuitbreaker:
instances:
demoCircuitBreaker: # 熔断器名称
sliding-window-size: 10 # 滑动窗口的大小
failure-rate-threshold: 50 # 失败率阈值(%)
wait-duration-in-open-state: 5000ms # 熔断后等待时间(毫秒)
permitted-number-of-calls-in-half-open-state: 3 # 半开状态时允许的调用数量
解释:
sliding-window-size
:统计最近多少次请求(窗口大小)。failure-rate-threshold
:当请求失败率超过 50% 时触发熔断。wait-duration-in-open-state
:熔断器打开后的等待时间,时间到后进入半开状态。permitted-number-of-calls-in-half-open-state
:半开状态允许的调用次数,用于测试服务恢复情况。
在代码中使用熔断器
@RestController
public class DemoController {
@Autowired
private RestTemplate restTemplate;
// 通过注解使用熔断器
@CircuitBreaker(name = "demoCircuitBreaker", fallbackMethod = "fallback")
@GetMapping("/api/demo")
public String callExternalService() {
// 模拟调用外部服务
return restTemplate.getForObject("http://external-service/api", String.class);
}
// 熔断后触发的降级逻辑
public String fallback(Throwable t) {
return "Service is unavailable. Please try again later.";
}
}
代码解释:
@CircuitBreaker(name = "demoCircuitBreaker")
:- 指定熔断器的名称为
demoCircuitBreaker
。 - 熔断器的配置从
application.yml
中读取。
- 指定熔断器的名称为
fallbackMethod
:- 指定熔断触发后调用的降级方法。
fallback
方法的参数类型为Throwable
,用于捕获异常信息。
RestTemplate.getForObject()
:- 模拟调用外部服务
http://external-service/api
。 - 如果服务不可用或失败,触发熔断逻辑。
- 模拟调用外部服务
测试熔断器
- 模拟
external-service
服务连续失败。 - 调用
/api/demo
,观察是否返回降级响应Service is unavailable.
。 - 等待熔断器进入半开状态后,观察是否尝试恢复调用。
5.2.2 服务降级
降级逻辑是在目标服务不可用时返回备用响应,保证系统部分功能可用。
降级代码实现
降级逻辑已通过 fallbackMethod
实现,下面补充其工作机制:
// 原服务调用方法
@CircuitBreaker(name = "demoCircuitBreaker", fallbackMethod = "fallback")
public String callExternalService() {
return restTemplate.getForObject("http://external-service/api", String.class);
}
// 降级方法
public String fallback(Throwable t) {
return "Fallback response: external service is down.";
}
详细解析:
- 当调用
http://external-service/api
时发生以下情况之一时,触发fallback
方法:- 超时:外部服务响应时间超过阈值。
- 异常:调用发生异常(如连接失败)。
- 失败率超过设定值。
5.2.3 重试机制
重试机制用于对失败的调用按照设定的规则重新尝试。
配置重试规则
在 application.yml
文件中:
resilience4j:
retry:
instances:
demoRetry: # 重试策略的名称
max-attempts: 3 # 最大重试次数
wait-duration: 1000ms # 每次重试间隔
代码实现重试
@RestController
public class RetryController {
@Retry(name = "demoRetry", fallbackMethod = "retryFallback")
@GetMapping("/api/retry")
public String callWithRetry() {
// 模拟调用外部服务
return restTemplate.getForObject("http://external-service/api", String.class);
}
// 重试失败后的降级逻辑
public String retryFallback(Throwable t) {
return "Retry failed after multiple attempts.";
}
}
详细解析:
@Retry(name = "demoRetry")
:- 指定重试策略名称为
demoRetry
。 - 配置中的
max-attempts
和wait-duration
控制重试行为。
- 指定重试策略名称为
retryFallback
:- 在所有重试失败后执行降级逻辑。
5.3 Sentinel
5.3.1 限流规则配置
Sentinel 支持多种限流方式,如 QPS 限制、并发线程限制。
配置 Sentinel
引入依赖 在 pom.xml
中添加:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
在 application.yml
中配置
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8080 # Sentinel Dashboard 地址
定义限流规则 使用 @SentinelResource
注解:
@RestController
public class SentinelDemoController {
@GetMapping("/api/sentinel")
@SentinelResource(value = "sentinelDemo", blockHandler = "blockHandler")
public String sentinelDemo() {
return "Request processed.";
}
// 限流触发时的逻辑
public String blockHandler(BlockException ex) {
return "Request has been limited. Please try again later.";
}
}
代码解释:
@SentinelResource(value = "sentinelDemo")
:- 定义资源名称为
sentinelDemo
。
- 定义资源名称为
blockHandler
:- 当限流规则触发时调用的逻辑。
- 参数类型为
BlockException
,表示限流异常。
测试限流
- 启动 Sentinel Dashboard(
java -jar sentinel-dashboard.jar
)。 - 在 Dashboard 中为
sentinelDemo
配置 QPS 限制为 5。 - 使用压力测试工具(如 JMeter)发送超过 5 个并发请求,观察是否触发限流逻辑。
5.3.2 Sentinel 实时监控
启动 Sentinel Dashboard 下载并启动 Sentinel Dashboard:
java -jar sentinel-dashboard.jar
连接到 Dashboard 服务启动后,Sentinel 会自动将实例信息注册到 Dashboard,可以在界面上查看限流规则和流量统计。
5.4 总结
功能 | Resilience4j | Sentinel |
---|---|---|
熔断降级 | 通过 @CircuitBreaker 实现,支持多种配置规则 | 提供丰富的熔断策略,结合 Dashboard 实时监控 |
限流 | 支持基本限流功能 | 提供多种限流方式(QPS、线程数),支持动态规则调整 |
重试 | 使用 @Retry 实现,配置灵活 | 不提供原生重试功能 |
监控界面 | 无原生监控界面,需结合 Prometheus 或 Micrometer 实现 | 提供完整的 Dashboard,可实时查看规则和流量数据 |
6. 分布式追踪
在微服务架构中,服务之间存在复杂的调用关系。排查问题时,单个服务的日志通常无法反映全局问题。分布式追踪通过记录请求在各服务中的流转过程,让开发者可以全面了解请求的执行路径和性能瓶颈。
- 使用 Spring Cloud Sleuth 配合 Zipkin,实现分布式追踪的基础功能。
- 在需要复杂性能分析和扩展性的场景下,可选择 Jaeger。
6.1 什么是分布式追踪
分布式追踪是一种记录请求跨服务调用行为的方法。通过为每个请求生成唯一标识,记录请求经过的服务、操作和耗时,从而提供一条完整的调用链。
核心概念
- Trace:一个请求的完整调用链路,由多个 Span 组成。
- Span:调用链中的一个片段,代表一次操作(如调用某个服务或执行某个方法)。
- TraceId:标识整个请求的唯一 ID,贯穿所有 Span。
- SpanId:标识某个具体操作的唯一 ID,是 Trace 的子集。
6.2 Spring Cloud Sleuth
Spring Cloud Sleuth 是 Spring 提供的分布式追踪工具,与 Zipkin、Jaeger 等工具无缝集成,用于生成和传递 TraceId 和 SpanId。
6.2.1 基本功能
- 为每个请求生成 TraceId,并在调用链中传递。
- 为每个操作生成 SpanId,记录操作的开始和结束时间。
- 集成日志系统,自动将 TraceId 和 SpanId 添加到日志中。
6.2.2 Sleuth 的使用
(1)引入依赖
在 pom.xml
中添加 Sleuth 和日志依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
(2)配置 Sleuth
在 application.yml
中添加以下配置:
spring:
application:
name: order-service # 服务名称
sleuth:
sampler:
probability: 1.0 # 采样率(1.0 表示记录所有请求)
zipkin:
base-url: http://localhost:9411 # Zipkin 服务地址
enabled: true # 启用 Zipkin
解释:
-
spring.application.name
:服务的名称,用于区分调用链中的服务。 -
sleuth.sampler.probability
:采样率,1.0
表示记录所有请求,0.5
表示记录 50% 的请求。 -
zipkin.base-url
:Zipkin 的服务地址,Sleuth 会自动将追踪数据发送到 Zipkin。
(3)日志集成
Sleuth 会自动将 TraceId 和 SpanId 添加到日志中,无需额外配置。示例日志输出格式:
2024-11-15 12:00:00.123 INFO [order-service,abc123,xyz456] 12 --- [nio-8080-exec-1] c.e.OrderController : Processing order
-
[order-service,abc123,xyz456]
:-
order-service
:当前服务名。 -
abc123
:TraceId,标识整个请求。 -
xyz456
:SpanId,标识当前操作。
-
(4)模拟服务间调用
假设有两个服务:order-service
和 payment-service
。
在 order-service
中通过 RestTemplate
调用 payment-service
。
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("/{id}")
public String createOrder(@PathVariable String id) {
// 调用 payment-service
String paymentStatus = restTemplate.getForObject("http://payment-service/payment/" + id, String.class);
return "Order created with Payment Status: " + paymentStatus;
}
}
配置 RestTemplate
:
@Configuration
public class RestTemplateConfig {
@Bean
@LoadBalanced // 支持服务发现
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
在日志中可以看到完整的调用链信息,包括 TraceId 和 SpanId。
6.3 Zipkin 或 Jaeger
6.3.1 什么是 Zipkin
Zipkin 是一个分布式追踪系统,用于收集、存储和可视化追踪数据。它与 Spring Cloud Sleuth 集成,可以展示请求的调用链路和每个操作的耗时。
6.3.2 使用 Zipkin
(1)安装 Zipkin
下载 Zipkin 的可执行 Jar 包:下载地址。
启动 Zipkin:
java -jar zipkin-server-2.23.2-exec.jar
访问 Zipkin 控制台:http://localhost:9411
(2)配置 Sleuth 发送数据到 Zipkin
在 application.yml
中添加:
spring:
zipkin:
base-url: http://localhost:9411 # Zipkin 地址
enabled: true # 启用 Zipkin
(3)调用链展示
启动 order-service
和 payment-service
。
调用 http://localhost:8080/order/123
。
打开 Zipkin 控制台,查询 TraceId
,可以看到完整的调用链:
Order-Service --> Payment-Service
每个服务的耗时会以图形化的形式展示。
6.3.3 Jaeger
Jaeger 是另一种分布式追踪系统,与 Zipkin 类似,提供分布式调用链的可视化功能。它更注重性能监控和扩展性,适合大规模系统。
安装 Jaeger
使用 Docker 启动 Jaeger:
docker run -d --name jaeger \
-e COLLECTOR_ZIPKIN_HTTP_PORT=9411 \
-p 5775:5775/udp \
-p 6831:6831/udp \
-p 6832:6832/udp \
-p 5778:5778 \
-p 16686:16686 \
-p 14268:14268 \
-p 14250:14250 \
-p 9411:9411 \
jaegertracing/all-in-one:1.33
访问 Jaeger 控制台:http://localhost:16686
集成 Jaeger
与 Zipkin 集成类似,将 Zipkin 的 base-url
替换为 Jaeger 的地址。
6.4 调用链性能分析
分布式追踪的一个重要功能是性能分析,通过展示每个服务的耗时帮助开发者优化系统。
示例分析
在 Zipkin 中查看某个 Trace。
点击某个服务(如 payment-service
),查看其详细耗时:
- 请求时间:10:00:00.123
- 响应时间:10:00:00.456
- 耗时:333 毫秒
如果某个服务的耗时明显较长,可以深入分析具体原因(如数据库查询慢、依赖服务超时)。
6.5 总结
功能 | Spring Cloud Sleuth | Zipkin | Jaeger |
---|---|---|---|
核心功能 | 生成并传递 TraceId 和 SpanId | 可视化调用链和性能数据 | 更强的扩展性和性能分析功能 |
日志集成 | 自动将 TraceId 和 SpanId 注入日志 | 无日志功能 | 无日志功能 |
界面 | 无可视化界面,需要结合 Zipkin 或 Jaeger | 提供简单直观的调用链可视化界面 | 提供调用链和性能分析界面,支持大规模分布式系统 |
适用场景 | 微服务系统的基础追踪 | 中小型系统,便于快速定位调用链问题 | 大型分布式系统,适合深入性能优化 |
7. 消息驱动
在微服务架构中,服务之间的异步通信是解耦服务、提高系统弹性的重要手段。通过消息队列,可以在不同服务之间传递数据,实现松耦合,同时支持削峰填谷、流量控制等功能。
7.1 什么是消息驱动
消息驱动是一种微服务之间的异步通信模型,通过消息中间件(如 RabbitMQ、Kafka 等)实现服务之间的解耦和消息传递。消息驱动使得微服务系统在高并发和高可靠性场景下更加灵活和高效。
7.1.1 为什么需要消息驱动
- 解耦服务:消息队列充当生产者和消费者之间的中间层,生产者只负责发送消息,而消费者根据需要消费消息,二者互不依赖。
- 异步处理:通过消息队列,生产者不需要等待消费者完成处理,提升系统的响应速度。
- 流量削峰:在高并发场景下,消息队列将请求暂存,消费者根据自身能力按需处理,避免系统崩溃。
- 可靠性:通过消息确认机制和重试机制,确保消息不会因网络故障或系统故障而丢失。
7.1.2 核心概念
- 消息队列:存储消息的缓冲区,生产者将消息放入队列,消费者从队列中取出消息进行处理。
- 生产者:发送消息到队列的服务。
- 消费者:从队列中拉取消息并处理的服务。
- 消息确认:
- 生产者确认:消息是否成功发送到队列。
- 消费者确认:消息是否成功消费。
- 消息模型:
- 点对点模型:每条消息只能被一个消费者消费(RabbitMQ)。
- 发布订阅模型:消息可以被多个消费者订阅(RabbitMQ、Kafka)。
7.1.3 消息驱动的典型应用场景
- 事件通知:用户下单后,通过消息驱动通知库存系统减库存、物流系统生成订单。
- 日志收集:将分布式系统的日志通过消息驱动集中存储和分析(如 Kafka)。
- 订单处理:处理订单支付和发货,异步通知第三方支付系统。
- 流式处理:对实时流数据进行分析和处理(如 Kafka)。
7.2 Spring Cloud Stream
Spring Cloud Stream 是 Spring 提供的消息驱动框架,用于统一处理消息的生产与消费。它支持多种消息中间件(如 RabbitMQ、Kafka),通过抽象化的 API 隐藏了底层消息队列的具体实现,降低了开发复杂度。
7.2.1 Spring Cloud Stream 的核心概念
-
Binding(绑定):
- 消息通道与消息中间件的桥梁。
- 定义消息生产者(
Source
)和消费者(Sink
)。
-
Binder(绑定器):
- 连接消息通道与具体消息中间件的组件。
- 支持 RabbitMQ、Kafka 等主流消息中间件。
-
消息通道:
- 输出通道(
output
):生产者通过输出通道发送消息。 - 输入通道(
input
):消费者通过输入通道接收消息。
- 输出通道(
-
Stream API:
- 提供统一的消息处理 API,屏蔽不同消息中间件的实现差异。
7.2.2 Spring Cloud Stream 的优点
- 中间件无关:支持多种消息中间件,开发者无需关注底层实现。
- 配置灵活:通过简单的配置即可实现消息生产和消费。
- 自动重试:内置消息重试机制,提升消息传递的可靠性。
- 集成简单:与 Spring Boot 无缝集成。
7.2.3 配置 Spring Cloud Stream
(1)引入依赖
以 RabbitMQ 为例,在 pom.xml
中添加以下依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
(2)配置 application.yml
spring:
application:
name: message-service # 服务名称
cloud:
stream:
bindings:
output: # 定义生产者通道
destination: my-topic # 指定目标队列/主题
content-type: application/json # 消息内容类型
input: # 定义消费者通道
destination: my-topic # 指定监听的队列/主题
group: message-group # 消费者组(Kafka 使用)
rabbit:
bindings:
input:
consumer:
acknowledge-mode: MANUAL # 手动确认消息
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
配置解析:
destination
:绑定的消息队列或主题。group
:消费者组(Kafka 场景)。acknowledge-mode
:消费者手动确认消息的模式,避免消息丢失。
7.2.4 消息生产与消费
(1)消息生产
使用 MessageChannel
发送消息。
@EnableBinding(Source.class) // 启用消息绑定
@RestController
@RequestMapping("/message")
public class MessageProducer {
@Autowired
private MessageChannel output; // 自动绑定到配置中的 output 通道
@PostMapping("/send")
public String sendMessage(@RequestBody String payload) {
// 使用 MessageBuilder 构建消息并发送
output.send(MessageBuilder.withPayload(payload).build());
return "Message sent: " + payload;
}
}
代码解析:
-
@EnableBinding(Source.class)
:-
启用生产者绑定,将
output
通道与my-topic
队列绑定。
-
-
MessageChannel
:-
Spring Cloud Stream 提供的消息通道接口,用于发送消息。
-
-
MessageBuilder
:-
构建消息对象,包含消息内容和元数据。
-
(2)消息消费
使用 @StreamListener
监听消息。
@EnableBinding(Sink.class) // 启用消费者绑定
public class MessageConsumer {
@StreamListener(Sink.INPUT) // 监听 input 通道
public void handleMessage(String message) {
System.out.println("Received message: " + message);
}
}
代码解析:
@EnableBinding(Sink.class)
:- 启用消费者绑定,将
input
通道与my-topic
队列绑定。
- 启用消费者绑定,将
@StreamListener(Sink.INPUT)
:- 监听
input
通道,处理接收到的消息。
- 监听
7.2.5 测试消息驱动
启动 RabbitMQ 服务。
启动 message-service
。
通过 Postman 或其他工具发送请求:
POST http://localhost:8080/message/send
Body: "Hello, RabbitMQ!"
消费端打印:
Received message: Hello, RabbitMQ!
7.3 RabbitMQ
RabbitMQ 是基于 AMQP(Advanced Message Queuing Protocol)协议的消息中间件,支持多种消息模型(如点对点、发布订阅)。它以可靠性和灵活性著称,适合中小型系统的消息驱动场景。
7.3.1 RabbitMQ 的核心概念
-
Producer(生产者)
- 负责发送消息到 RabbitMQ 队列。
- 生产者与队列之间通过交换机(Exchange)通信。
-
Consumer(消费者)
- 负责从 RabbitMQ 队列中获取消息并处理。
- 一个队列可以有多个消费者。
-
Queue(队列)
- 用于存储消息,生产者将消息发送到队列,消费者从队列获取消息。
-
Exchange(交换机)
- 接收生产者的消息并将其路由到绑定的队列。
- 常见交换机类型:
- Direct:按消息的路由键精确匹配队列。
- Fanout:将消息广播到所有绑定的队列。
- Topic:按路由键模式匹配队列(支持模糊匹配)。
- Headers:按消息头属性匹配队列。
-
Binding(绑定)
- 定义队列与交换机之间的关系,决定消息如何从交换机路由到队列。
-
Message(消息)
- 包含消息内容(Payload)和消息属性(Headers、TTL 等)。
7.3.2 RabbitMQ 的消息模型
(1)点对点模型
-
消息被发送到一个队列,且每条消息只能被一个消费者处理。
-
场景:订单服务发送订单创建消息到队列,库存服务从队列消费消息。
(2)发布订阅模型
-
消息通过 Fanout 类型的交换机广播到所有绑定的队列,每个队列的消费者都能收到消息。
-
场景:用户注册后,发送消息通知多个服务(如发送邮件、推送消息)。
(3)路由模型
-
消息通过 Direct 类型的交换机根据路由键分发到匹配的队列。
-
场景:日志系统按日志级别(如 INFO、ERROR)将日志消息分发到不同的队列。
(4)主题模型
-
消息通过 Topic 类型的交换机按路由键模式匹配分发到队列,支持模糊匹配。
- 场景:电商系统按订单类型(如
order.payment.success
)分发订单状态更新消息。
7.3.3 RabbitMQ 的集成
(1)安装 RabbitMQ
下载并安装 RabbitMQ:RabbitMQ 下载。
启动 RabbitMQ:
rabbitmq-server
启用管理插件:
rabbitmq-plugins enable rabbitmq_management
访问管理界面:http://localhost:15672
默认用户名和密码:guest
/guest
。
(2)Spring Boot 集成 RabbitMQ
引入依赖
在 pom.xml
中添加 RabbitMQ 的依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
配置 application.yml
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
(3)实现消息生产与消费
消息生产者
生产者将消息发送到队列。
@RestController
@RequestMapping("/producer")
public class ProducerController {
@Autowired
private RabbitTemplate rabbitTemplate;
@PostMapping("/send")
public String sendMessage(@RequestBody String message) {
// 发送消息到默认交换机和队列
rabbitTemplate.convertAndSend("my-exchange", "my-routing-key", message);
return "Message sent: " + message;
}
}
代码解释:
-
RabbitTemplate
:Spring 提供的 RabbitMQ 操作模板。 -
convertAndSend
:-
第一个参数是交换机名称。
-
第二个参数是路由键。
-
第三个参数是消息内容。
-
消息消费者
消费者从队列中接收消息。
@Component
public class Consumer {
@RabbitListener(queues = "my-queue")
public void receiveMessage(String message) {
System.out.println("Received message: " + message);
}
}
代码解释:
-
@RabbitListener
:声明消费者监听的队列名称。 -
receiveMessage
:当队列中有新消息时自动触发处理逻辑。
(4)完整配置队列与交换机
RabbitMQ 需要提前声明队列、交换机和绑定关系。Spring 提供了 @Bean
注解的方式进行配置。
@Configuration
public class RabbitConfig {
@Bean
public Queue queue() {
return new Queue("my-queue", true); // 队列名称,是否持久化
}
@Bean
public DirectExchange exchange() {
return new DirectExchange("my-exchange"); // 交换机名称
}
@Bean
public Binding binding(Queue queue, DirectExchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with("my-routing-key");
}
}
代码解释:
-
Queue
:声明队列my-queue
,并设置持久化。 -
DirectExchange
:声明 Direct 类型的交换机my-exchange
。 -
Binding
:将队列和交换机通过路由键my-routing-key
绑定。
7.3.4 RabbitMQ 的消息确认机制
RabbitMQ 提供三种消息确认机制,确保消息可靠传递:
(1)生产者确认
生产者发送消息到交换机后,交换机会返回确认。
配置生产者确认:
spring:
rabbitmq:
publisher-confirm-type: correlated # 启用发布确认
示例代码:
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
if (ack) {
System.out.println("Message delivered successfully.");
} else {
System.out.println("Message delivery failed: " + cause);
}
});
(2)消费者确认
消费者接收到消息后,需要手动确认消息已处理。
配置消费者手动确认:
spring:
rabbitmq:
listener:
simple:
acknowledge-mode: manual # 手动确认
示例代码:
@RabbitListener(queues = "my-queue")
public void receiveMessage(String message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) {
try {
System.out.println("Processing message: " + message);
channel.basicAck(deliveryTag, false); // 手动确认
} catch (Exception e) {
channel.basicNack(deliveryTag, false, true); // 拒绝并重新投递
}
}
7.3.5 RabbitMQ 的重试与幂等处理
(1)消息重试
在消息消费失败时,RabbitMQ 可以自动重试。
配置重试策略:
spring:
rabbitmq:
listener:
simple:
retry:
enabled: true
initial-interval: 1000 # 初始重试间隔
max-attempts: 3 # 最大重试次数
multiplier: 2.0 # 间隔倍增
(2)幂等处理
为避免消息重复消费,可引入唯一标识(如消息 ID),并记录到数据库进行去重。
示例代码:
@RabbitListener(queues = "my-queue")
public void handleMessage(String message) {
String messageId = extractMessageId(message);
if (!isProcessed(messageId)) {
processMessage(message);
markAsProcessed(messageId); // 标记消息已处理
} else {
System.out.println("Duplicate message ignored: " + messageId);
}
}
7.4 Kafka
Kafka 是一个分布式流处理平台,最初由 LinkedIn 开发,后开源成为 Apache 项目。它以高吞吐量、高可用性和持久化特性著称,广泛用于日志收集、实时数据流处理等场景。
7.4.1 Kafka 的核心概念
1. Producer(生产者)
生产者负责将消息发送到 Kafka 的指定主题(Topic)。生产者可以自定义分区策略,将消息分发到不同的分区。
2. Consumer(消费者)
消费者从 Kafka 的主题中拉取消息。一个主题可以有多个消费者,消费者可以组成消费者组(Consumer Group)。
3. Topic(主题)
主题是 Kafka 中消息的逻辑分类,每条消息必须归属于一个主题。主题可以有多个分区。
4. Partition(分区)
每个主题可以有多个分区,消息会分布到不同的分区中。
-
分区的好处:
-
提高并发处理能力。
-
实现消息的有序性(分区内有序)。
-
支持数据分布在不同的节点上。
-
5. Offset(偏移量)
Kafka 为每条消息分配一个唯一的 Offset,消费者通过 Offset 记录读取位置。
6. Broker
Kafka 集群中的节点称为 Broker,负责存储和管理消息。
7. ZooKeeper
Kafka 使用 ZooKeeper 管理集群的元数据(如分区、Broker 信息)。新版 Kafka 提供了 ZooKeeper-less 模式,支持无需 ZooKeeper 的运行。
7.4.2 Kafka 的消息模型
Kafka 的消息模型支持以下场景:
1. 点对点模式
-
消息被发送到一个主题,由一个消费者组中的某个消费者接收。
-
应用场景:订单服务发送消息,物流服务处理消息。
2. 发布/订阅模式
-
消息被发送到一个主题,多个消费者组可以同时接收消息。
-
应用场景:用户注册后发送消息,多个服务(如邮件、通知服务)订阅同一主题。
3. 分区消费
-
一个主题分成多个分区,消费者组中的消费者会分摊处理这些分区。
-
应用场景:高并发日志收集,每个消费者处理一部分日志。
7.4.3 Kafka 的安装与配置
1. 安装 Kafka
下载 Kafka:Kafka 官网。
解压后,启动 ZooKeeper:
bin/zookeeper-server-start.sh config/zookeeper.properties
启动 Kafka:
bin/kafka-server-start.sh config/server.properties
2. 创建主题
bin/kafka-topics.sh --create --topic my-topic --bootstrap-server localhost:9092 --partitions 3 --replication-factor 1
参数解析:
-
--topic
:主题名称。 -
--partitions
:分区数。 -
--replication-factor
:副本因子(推荐 >=2)。
3. 验证 Kafka
生产消息:
bin/kafka-console-producer.sh --topic my-topic --bootstrap-server localhost:9092
输入消息内容后按 Enter 发送。
消费消息:
bin/kafka-console-consumer.sh --topic my-topic --from-beginning --bootstrap-server localhost:9092
查看从主题 my-topic
中读取的消息。
7.4.4 Spring Boot 集成 Kafka
1. 引入依赖
在 pom.xml
中添加 Kafka 相关依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
2. 配置 Kafka
在 application.yml
文件中添加 Kafka 的基本配置:
spring:
kafka:
bootstrap-servers: localhost:9092 # Kafka 集群地址
consumer:
group-id: my-group # 消费者组 ID
auto-offset-reset: earliest # 从最早的消息开始消费
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
producer:
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.apache.kafka.common.serialization.StringSerializer
3. 消息生产者
创建 Kafka 消息生产者:
@RestController
@RequestMapping("/producer")
public class KafkaProducerController {
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
@PostMapping("/send")
public String sendMessage(@RequestParam String topic, @RequestBody String message) {
kafkaTemplate.send(topic, message); // 发送消息到指定主题
return "Message sent to topic: " + topic;
}
}
代码解释:
-
KafkaTemplate
:Spring 提供的 Kafka 消息发送模板。 -
send()
:向指定的主题发送消息。
4. 消息消费者
创建 Kafka 消息消费者:
@Component
public class KafkaConsumer {
@KafkaListener(topics = "my-topic", groupId = "my-group")
public void consumeMessage(String message) {
System.out.println("Received message: " + message);
}
}
代码解释:
-
@KafkaListener
:监听指定主题的消息。 -
topics
:指定要监听的主题。 -
groupId
:消费者组 ID,确保分区内消息只被同组中的一个消费者消费。
5. 测试 Kafka 集成
启动 Kafka 服务。
启动 Spring Boot 应用。
使用 Postman 发送 POST 请求:
POST http://localhost:8080/producer/send?topic=my-topic
Body: "Hello, Kafka!"
在控制台查看消费者输出:
Received message: Hello, Kafka!
7.4.5 Kafka 的消息确认机制
Kafka 提供两种确认机制:
生产者确认:
- 确保消息成功发送到 Kafka 的分区。
- 配置生产者的
acks
参数:spring: kafka: producer: properties: acks: all # 等待所有副本确认
消费者确认:
- 默认自动提交 Offset,可以设置为手动提交 Offset。
- 手动提交 Offset 示例:
@KafkaListener(topics = "my-topic", groupId = "my-group") public void consumeMessage(ConsumerRecord<String, String> record, Acknowledgment acknowledgment) { System.out.println("Processing message: " + record.value()); acknowledgment.acknowledge(); // 手动提交 Offset }
7.4.6 Kafka 的重试与幂等性
1. 消息重试
在消费消息失败时,Kafka 可以自动重新投递消息。
配置重试策略:
spring:
kafka:
listener:
retry:
max-attempts: 3 # 最大重试次数
backoff: 1000 # 重试间隔(毫秒)
2. 幂等处理
为避免消息重复消费,可使用消息 ID 进行去重。
示例代码:
@KafkaListener(topics = "my-topic", groupId = "my-group")
public void handleMessage(ConsumerRecord<String, String> record) {
String messageId = record.key(); // 消息唯一 ID
if (!isProcessed(messageId)) {
processMessage(record.value());
markAsProcessed(messageId); // 记录消息已处理
} else {
System.out.println("Duplicate message ignored: " + messageId);
}
}
7.5 总结
功能 | Spring Cloud Stream | RabbitMQ | Kafka |
---|---|---|---|
消息模型 | 抽象化消息生产和消费,适配多种中间件 | 支持点对点、发布订阅等模式 | 高吞吐量,支持日志存储和流式处理 |
消息确认机制 | 支持手动确认 | 提供自动和手动确认模式 | 提供 offset 手动提交机制 |
消息重试与幂等处理 | 自动支持重试逻辑,需手动处理幂等 | 配置重试策略,需额外实现幂等逻辑 | 支持 offset 去重逻辑 |
推荐场景 | 需要适配多种消息中间件场景 | 中小规模消息传递,推荐作为入门学习中间件 | 大数据、高吞吐场景,推荐用于日志处理与实时分析 |
8. 分布式事务
分布式事务是解决微服务架构中跨多个服务或数据库操作时数据一致性问题的重要手段。Seata 是阿里巴巴开源的分布式事务框架,其核心特性包括分布式事务协调、异常传播、事务日志记录等。
8.1 Seata 的事务模式
1. AT 模式
- 自动化程度高,适合大多数场景。
- 推荐用于操作集中在数据库的事务。
Seata AT 模式的工作原理
- 第一阶段:业务操作阶段
- 执行数据库操作,生成 Undo Log(记录前镜像和后镜像),但事务未提交。
- 第二阶段:提交或回滚阶段
- 提交:如果所有操作成功,TC 通知各分支事务提交本地事务。
- 回滚:如果某一分支事务失败,TC 通知所有分支事务通过 Undo Log 恢复数据库。
2. TCC 模式
- 更灵活,但实现复杂。
- 推荐用于需要高性能和精确控制的场景(如金融支付)。
Seata TCC 模式工作原理
TCC(Try-Confirm-Cancel)是一种分布式事务实现方式。开发者需要显式实现三个阶段:
- Try:预留资源(如冻结账户余额)。
- Confirm:确认资源操作(如扣减冻结的余额)。
- Cancel:取消资源操作(如释放冻结的余额)。
8.2 分布式事务的核心概念
8.2.1 什么是分布式事务
分布式事务保证跨多个服务的操作要么全部成功,要么全部回滚。它依赖于以下三大关键机制:
- 本地事务:每个服务的数据库操作由本地事务管理。
- 全局事务:由分布式事务框架协调各服务的事务状态。
- 异常传播:当某一服务的本地事务失败时,异常被捕获并触发全局事务的回滚。
8.2.2 本地事务与分布式事务的结合:
出现异常必须用throw显式地抛出异常。
涉及数据库操作的子方法
- 必须使用
@Transactional
,以确保数据库操作的原子性。 - Seata 会捕获本地事务并生成 Undo Log,支持全局回滚。
不涉及数据库操作的子方法
- 可以省略
@Transactional
,但需要确保异常能够正确传播到全局事务。
示例:
public void validateOrder(String userId, int count) {
if (count <= 0) {
throw new IllegalArgumentException("Invalid count");
}
}
- 子方法抛出的异常通过调用栈传播到
@GlobalTransactional
方法中。 - 全局事务捕获异常后,通知事务协调器(TC)回滚所有子事务。
8.2.3 Seata 的核心组件
- TM(Transaction Manager)事务管理器:
- 通过
@GlobalTransactional
启动全局事务。 - 捕获异常并通知 TC 触发全局回滚。
- 通过
- TC(Transaction Coordinator)事务协调器:
- 管理全局事务状态,协调所有分支事务的提交或回滚。
- RM(Resource Manager)资源管理器:
- 负责记录本地事务的 Undo Log,并根据 TC 指令进行提交或回滚。
8.3 配置 Seata 事务日志
8.3.1 安装 Seata
下载 Seata:Seata Releases。
启动 Seata 服务器:
sh bin/seata-server.sh
8.3.2 配置 Seata 日志存储
修改 Seata 的 registry.conf
文件,启用数据库存储模式:
store:
mode: db # 使用数据库存储事务日志
db:
datasource: druid
db-type: mysql
url: jdbc:mysql://localhost:3306/seata
user: root
password: root
在 Seata 的日志数据库中创建必要的事务表:
-- 全局事务表
CREATE TABLE `global_table` (
`xid` varchar(128) NOT NULL,
`transaction_id` bigint(20) NOT NULL,
`status` tinyint NOT NULL,
PRIMARY KEY (`xid`)
);
-- 分支事务表
CREATE TABLE `branch_table` (
`branch_id` bigint(20) NOT NULL,
`xid` varchar(128) NOT NULL,
`status` tinyint NOT NULL,
PRIMARY KEY (`branch_id`)
);
-- 锁表
CREATE TABLE `lock_table` (
`row_key` varchar(128) NOT NULL,
`xid` varchar(128) NOT NULL,
PRIMARY KEY (`row_key`)
);
-
Undo Log
- Seata 自动在业务数据库中记录数据库操作的前后状态(前镜像和后镜像)。
- 在事务回滚时,Seata 根据 Undo Log 恢复数据到前镜像状态。
-
事务日志表
global_table
:记录全局事务状态。branch_table
:记录每个分支事务(服务)的状态。lock_table
:记录全局事务中锁定的资源,避免冲突。
8.4 分布式事务的实现(AT 模式)
8.4.1 场景描述
假设有订单服务、库存服务和账户服务:
- 订单服务:负责创建订单。
- 库存服务:扣减库存。
- 账户服务:扣减余额。
8.4.2 全局事务定义
在订单服务中,定义全局事务逻辑:
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private RestTemplate restTemplate;
@GlobalTransactional(name = "create-order-tx", rollbackFor = Exception.class)
public void createOrder(String userId, String productId, int count) {
// 创建订单(本地事务)
saveOrder(userId, productId, count);
// 调用库存服务扣减库存(跨服务调用)
restTemplate.postForObject(
"http://inventory-service/inventory/reduce",
new InventoryRequest(productId, count),
String.class);
// 调用账户服务扣减余额(跨服务调用)
restTemplate.postForObject(
"http://account-service/account/debit",
new AccountRequest(userId, count * 10),
String.class);
// 模拟异常
if (count > 5) {
throw new RuntimeException("Order count exceeds limit!");
}
}
@Transactional
public void saveOrder(String userId, String productId, int count) {
Order order = new Order(userId, productId, count, "CREATED");
orderRepository.save(order);
}
}
解析:
@GlobalTransactional
:- 开启全局事务,名称为
create-order-tx
。 - 如果发生异常,通知 TC 回滚所有分支事务。
- 开启全局事务,名称为
- 子方法中的
@Transactional
:- 确保
saveOrder
方法的本地事务被捕获,并记录 Undo Log。
- 确保
- 异常传播:
- 如果子方法(如库存或账户服务)抛出异常,全局事务感知异常并触发回滚。
8.4.3 子服务实现
库存服务
@Service
public class InventoryService {
@Autowired
private InventoryRepository inventoryRepository;
@Transactional
public void reduceStock(String productId, int count) {
Inventory inventory = inventoryRepository.findByProductId(productId);
if (inventory.getStock() < count) {
throw new RuntimeException("Insufficient stock");
}
inventory.setStock(inventory.getStock() - count);
inventoryRepository.save(inventory);
}
}
账户服务
@Service
public class AccountService {
@Autowired
private AccountRepository accountRepository;
@Transactional
public void debit(String userId, int amount) {
Account account = accountRepository.findByUserId(userId);
if (account.getBalance() < amount) {
throw new RuntimeException("Insufficient balance");
}
account.setBalance(account.getBalance() - amount);
accountRepository.save(account);
}
}
9. 微服务安全
确保微服务的 API 访问安全和用户认证功能。
必学内容:
- Spring Security:
- 微服务中的权限校验。
- 使用 JWT(JSON Web Token)实现用户认证。
- OAuth2(备选,视项目需求):
- 单点登录(SSO)的实现。
- 授权机制的基本使用。
9. 微服务安全
在微服务架构中,安全性是一个关键问题。它需要确保微服务 API 的访问安全性,同时还要支持用户认证、授权和单点登录(SSO)等功能。
9.1 Spring Security 需要结合JWT 或 OAuth2实现跨服务认证
在微服务架构中,跨服务的认证与授权共享 是一个核心问题,而 Spring Security 作为一个功能强大的安全框架,虽然能实现精细的认证和授权管理,但它无法单独实现跨服务的认证与授权共享。
9.1.1 Spring Security 的局限性
1. Spring Security 的默认会话机制
- 默认依赖服务器会话(Session):
- Spring Security 默认通过
HttpSession
存储用户的认证和授权信息。 - 用户登录后,认证信息保存在服务器端,每次请求通过会话 ID(
JSESSIONID
)在服务器端查找用户的认证状态。
- 微服务架构下,多个服务独立运行,无法直接共享
HttpSession
。 - 用户每次访问新的微服务时,必须重新登录。
- Spring Security 默认通过
2. Spring Security 是有状态的
- Spring Security 默认的会话管理机制需要依赖服务器端的状态存储。
- 在分布式环境中,服务无状态化是基本要求,依赖状态存储会导致以下问题:
- 服务无法水平扩展(如负载均衡器无法将请求分发到任意实例)。
- 用户的会话信息可能丢失(如服务重启后)。
3. Spring Security 缺乏跨服务认证的支持
- Spring Security 没有内置的机制来支持分布式环境下的认证共享。
- 缺乏一种能够在服务间传递用户认证信息的方式。
9.1.2 跨服务认证的需求
1. 无状态认证
- 微服务间通常通过 HTTP 协议进行通信,HTTP 是无状态协议。
- 每个请求都需要独立携带用户认证信息,而不能依赖服务器端存储。
2. 认证共享
- 用户登录后,其认证信息需要能够跨多个服务共享:
- 认证信息在服务 A 和服务 B 之间传递时,每个服务都能验证用户身份。
- 避免用户在访问每个服务时重复登录。
3. 安全性
- 用户认证信息需要以安全的方式传递,避免中间人攻击或伪造身份的风险。
- 在分布式环境下,认证机制需要保证用户认证信息不会被篡改或泄露。
9.1.3. JWT(JSON Web Token)
1 JWT 的作用
- 无状态认证令牌:
- JWT 是一种自包含的令牌,包含用户的认证和授权信息。
- 客户端在每次请求中携带 JWT,无需依赖服务器端存储认证状态。
- 跨服务共享:
- JWT 是一个加密字符串,可以在多个微服务间传递,各服务通过验证 JWT 实现认证共享。
- 安全性:
- JWT 使用签名算法(如 HS256、RS256)加密,防止令牌被篡改。
2 JWT 的结构
JWT 包括三部分,分别用 .
分隔:
- Header:描述令牌类型和签名算法。
- Payload:包含用户信息(如用户名、角色)和令牌过期时间。
- Signature:签名部分,用于验证令牌的完整性和合法性。
3 JWT 在 Spring Security 中的作用
- 生成令牌:
- 用户登录后,认证服务使用 JWT 生成包含用户信息的令牌。
- 验证令牌:
- 各微服务通过验证 JWT 的签名和过期时间,确认用户身份。
- 授权:
- JWT 的
Payload
中可以包含用户的角色信息,各服务根据角色完成授权。
- JWT 的
9.1.4. OAuth2
1 OAuth2 的作用
OAuth2 是一个开放授权协议,允许第三方应用在用户授权的情况下访问用户资源。在微服务安全中,OAuth2 通常用于:
- 集中认证:
- 用户只需登录一次,多个微服务共享认证状态。
- 资源授权:
- 为用户的资源提供细粒度的访问控制。
- 单点登录(SSO):
- 用户登录后,可以无缝访问多个服务。
2 OAuth2 的核心角色
- 认证服务器(Authorization Server):
- 负责用户登录和生成访问令牌(Access Token)。
- 资源服务器(Resource Server):
- 提供 API 服务,验证访问令牌,确认用户权限。
- 客户端(Client):
- 发起用户登录请求,并使用访问令牌访问资源。
3 OAuth2 的核心流程
- 用户向认证服务器发起登录请求。
- 认证服务器验证用户信息,并生成访问令牌。
- 用户携带访问令牌请求资源服务器。
- 资源服务器通过认证服务器验证令牌合法性,并完成授权。
4 OAuth2 在 Spring Security 中的作用
- 认证服务的实现:
- 使用 Spring Authorization Server 搭建集中认证服务。
- 资源服务器的配置:
- 每个微服务作为资源服务器,验证 Access Token 的合法性。
- 客户端支持:
- Spring Security 支持 OAuth2 客户端配置,简化授权流程。
9.1.4 Spring Security 的角色
在结合 JWT 和 OAuth2 的实现中,Spring Security 主要负责:
- 认证管理:
- 验证用户身份,并生成认证令牌(如 JWT 或 Access Token)。
- 请求过滤:
- 在每个服务中拦截请求,验证令牌的合法性。
- 授权控制:
- 根据令牌中的角色或权限信息,限制资源访问。
9.2 Spring Security + JWT:实现无状态认证与授权
在微服务架构中,Spring Security 与 JWT 的结合是实现无状态认证和授权共享的常用方案。以下将详细讲解其工作流程和实现方法,包括代码实现和关键点解析。
9.2.1 Spring Security + JWT 的工作流程
登录认证流程(认证服务负责)
- 用户提交用户名和密码到 认证服务。
- 认证服务验证用户信息(通常从数据库查询)。
- 验证成功后,认证服务生成一个 JWT,并返回给用户。
- 用户将 JWT 存储在客户端(如浏览器的
LocalStorage
或Cookie
中)。
请求验证流程(微服务负责)
- 用户在后续请求中,将 JWT 附加到 HTTP 请求头中,格式如下:
Authorization: Bearer <JWT>
- 微服务通过
JwtFilter
拦截请求,从请求头中提取 JWT。 - 验证 JWT 的合法性(校验签名和过期时间)。
- 如果 JWT 验证通过:
- 提取 JWT 中的用户信息。
- 设置用户身份到 Spring Security 的上下文中。
- 如果 JWT 验证失败,返回
401 Unauthorized
错误。
9.2.2 实现步骤
以下是基于 Spring Security + JWT 的具体实现,包括代码和详细解析。
1. 登录认证服务:生成 JWT
认证服务负责用户登录和 JWT 的生成。
JWT 工具类
JWT 工具类用于生成和验证 JWT。
import io.jsonwebtoken.*;
import org.springframework.stereotype.Component;
import java.util.Date;
@Component
public class JwtUtil {
private static final String SECRET_KEY = "mySecretKey";
private static final long EXPIRATION_TIME = 86400000; // 1 day
// 生成 JWT
public String generateToken(String username) {
return Jwts.builder()
.setSubject(username) // 用户信息
.setIssuedAt(new Date()) // 签发时间
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) // 过期时间
.signWith(SignatureAlgorithm.HS256, SECRET_KEY) // 签名算法
.compact();
}
// 验证 JWT
public Claims validateToken(String token) {
return Jwts.parser()
.setSigningKey(SECRET_KEY) // 设置密钥
.parseClaimsJws(token) // 解析 JWT
.getBody();
}
}
代码解析:
generateToken
:- 根据用户名生成一个包含用户信息的 JWT。
- 设置签发时间(
issuedAt
)和过期时间(expiration
)。 - 使用密钥(
SECRET_KEY
)和签名算法生成签名。
validateToken
:- 验证 JWT 的合法性,并返回其中的
Claims
(用户信息)。
- 验证 JWT 的合法性,并返回其中的
登录控制器
登录控制器负责接收用户登录请求,验证用户名和密码,并返回 JWT。
@RestController
@RequestMapping("/auth")
public class AuthController {
@Autowired
private JwtUtil jwtUtil;
@PostMapping("/login")
public ResponseEntity<String> login(@RequestBody LoginRequest request) {
// 假设用户名和密码验证成功(实际应查询数据库)
if ("admin".equals(request.getUsername()) && "password".equals(request.getPassword())) {
// 生成 JWT
String token = jwtUtil.generateToken(request.getUsername());
return ResponseEntity.ok(token); // 返回 JWT
}
throw new BadCredentialsException("Invalid credentials");
}
}
代码解析:
- 接收登录请求:
- 接收包含用户名和密码的
LoginRequest
对象。
- 接收包含用户名和密码的
- 验证用户信息:
- 验证用户名和密码是否正确(实际应查询数据库)。
- 生成并返回 JWT:
- 调用
JwtUtil.generateToken
生成 JWT,返回给客户端。
- 调用
2. 微服务:验证 JWT
每个微服务需要通过 JWT 验证用户身份并完成授权。
创建 JWT 验证过滤器
过滤器拦截所有请求,验证请求头中的 JWT。
@Component
public class JwtFilter extends OncePerRequestFilter {
@Autowired
private JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
String token = request.getHeader("Authorization"); // 从请求头中获取令牌
if (token != null && token.startsWith("Bearer ")) {
token = token.substring(7); // 去掉 "Bearer " 前缀
Claims claims = jwtUtil.validateToken(token); // 验证令牌
// 设置用户身份到安全上下文
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(
claims.getSubject(), null, new ArrayList<>())
);
}
chain.doFilter(request, response); // 继续执行过滤链
}
}
代码解析:
- 提取 JWT:
- 从请求头中提取
Authorization
字段。 - 验证格式是否为
Bearer <JWT>
。
- 从请求头中提取
- 验证 JWT:
- 调用
JwtUtil.validateToken
验证令牌的合法性。
- 调用
- 设置用户身份:
- 将用户身份信息写入 Spring Security 的
SecurityContextHolder
,供后续授权逻辑使用。
- 将用户身份信息写入 Spring Security 的
配置 Spring Security
将 JwtFilter
添加到 Spring Security 的过滤器链中。
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable() // 禁用 CSRF
.authorizeRequests()
.antMatchers("/auth/**").permitAll() // 放行登录接口
.anyRequest().authenticated() // 其他接口需要认证
.and()
.addFilterBefore(new JwtFilter(), UsernamePasswordAuthenticationFilter.class); // 添加 JWT 过滤器
}
}
代码解析:
- 放行登录接口:
- 对
/auth/**
开放访问,无需认证。
- 对
- 其他接口需要认证:
- 所有其他接口都必须携带有效的 JWT。
- 添加 JWT 过滤器:
- 在 Spring Security 的过滤器链中,将
JwtFilter
添加到UsernamePasswordAuthenticationFilter
之前。
- 在 Spring Security 的过滤器链中,将
9.2.3 客户端调用流程
1.用户登录:
- 客户端发送用户名和密码到
/auth/login
。 - 服务端验证成功后,返回 JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIn0.vdM8f72CpHOBQ6cHrXqfnRLsjxOvpyu9FcItiJ0Uw
2.携带 JWT 请求:
- 客户端在每个请求的 HTTP 头中添加 JWT:
Authorization: Bearer <JWT>
3.微服务验证 JWT:
- 微服务通过
JwtFilter
验证 JWT 的合法性,完成用户认证和授权。
9.3 Spring Security + OAuth2:实现集中认证与授权
在微服务架构中,Spring Security 与 OAuth2 的结合是实现集中认证和细粒度资源授权的最佳实践。它不仅能够支持跨服务认证共享,还能实现 单点登录(SSO) 和 分布式资源授权。
9.3.1 Spring Security + OAuth2 的工作流程
1. OAuth2 的核心角色
- 认证服务器(Authorization Server):
- 管理用户认证。
- 生成并颁发访问令牌(Access Token)和刷新令牌(Refresh Token)。
- 资源服务器(Resource Server):
- 负责验证访问令牌的合法性。
- 提供受保护的资源。
- 客户端(Client):
- 发起用户认证请求。
- 使用访问令牌访问资源服务器。
2. 工作流程概览
用户认证与令牌生成
- 用户通过客户端向 认证服务器 提交用户名和密码。
- 认证服务器验证用户信息。
- 验证成功后,认证服务器返回 访问令牌(Access Token) 和可选的 刷新令牌(Refresh Token)。
- 客户端存储令牌(如浏览器或移动端存储)。
资源访问与令牌验证
- 客户端携带访问令牌向 资源服务器 发起资源请求。
- 资源服务器通过认证服务器验证令牌的合法性(通过远程调用或本地校验)。
- 如果验证通过,资源服务器返回受保护的资源。
9.3.2 OAuth2 的实现步骤
1. 搭建认证服务器
认证服务器负责用户登录和访问令牌的生成。
添加依赖
在 pom.xml
中引入 Spring Authorization Server 相关依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
</dependency>
配置认证服务器
通过 AuthorizationServerConfigurerAdapter
配置认证服务。
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("client-id") // 客户端 ID
.secret("{noop}client-secret") // 客户端密钥
.authorizedGrantTypes("password", "refresh_token") // 授权模式
.scopes("read", "write"); // 作用域
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager);
}
@Autowired
private AuthenticationManager authenticationManager;
}
代码解释:
ClientDetailsServiceConfigurer
:- 定义 OAuth2 客户端信息:
- 客户端 ID:
client-id
,客户端的唯一标识。 - 客户端密钥:
client-secret
,用于客户端验证。 - 授权模式:支持密码模式(
password
)和刷新令牌(refresh_token
)。 - 作用域:
read
和write
,定义客户端的权限范围。
- 客户端 ID:
- 定义 OAuth2 客户端信息:
AuthorizationServerEndpointsConfigurer
:- 配置认证端点,并绑定认证管理器(
authenticationManager
),用于验证用户信息。
- 配置认证端点,并绑定认证管理器(
定义用户信息
配置用户信息供认证服务器验证。
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
@Override
protected UserDetailsService userDetailsService() {
// 创建内存用户,生产环境应从数据库加载用户信息
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("user") // 用户名
.password("{noop}password") // 明文密码
.roles("USER") // 用户角色
.build());
return manager;
}
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManagerBean();
}
}
代码解释:
userDetailsService
:- 在内存中创建一个用户,用户名为
user
,密码为password
,角色为USER
。 - 生产环境应通过数据库加载用户信息。
- 在内存中创建一个用户,用户名为
authenticationManager
:- 配置 Spring Security 的认证管理器,用于验证用户信息。
2. 配置资源服务器
资源服务器负责验证访问令牌,并提供受保护的资源。
添加依赖
在 pom.xml
中引入 Spring Resource Server 相关依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
配置资源服务器
通过 ResourceServerConfigurerAdapter
配置资源服务器。
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/public/**").permitAll() // 开放接口
.anyRequest().authenticated(); // 受保护接口
}
}
代码解释:
- 配置接口访问权限:
/public/**
接口开放访问,无需认证。- 其他接口需要认证,必须携带有效的访问令牌。
- 默认通过请求头中的
Authorization: Bearer <token>
验证令牌。
添加测试接口
在资源服务器中添加受保护的测试接口。
@RestController
@RequestMapping("/api")
public class TestController {
@GetMapping("/public")
public String publicEndpoint() {
return "This is a public endpoint";
}
@GetMapping("/private")
public String privateEndpoint() {
return "This is a private endpoint, authentication required";
}
}
代码解释:
/public
:无需认证的接口。/private
:需要携带访问令牌的受保护接口。
10. 服务监控与健康检查
10.1 三者综合使用
在生产环境中,可以将 Actuator、Prometheus + Grafana 和 ELK Stack 结合使用,以实现从性能监控到日志分析的全方位监控体系。
1. Actuator 的角色
- 用于服务内部健康检查(如
/health
)和性能指标暴露(如/metrics
)。 - 是 Prometheus 的主要数据来源。
2. Prometheus + Grafana 的角色
- 通过 Actuator 的
/metrics
接口,采集服务性能数据。 - 用于全局监控和趋势分析。
- 提供实时告警功能,帮助快速响应问题。
3. ELK Stack 的角色
- 收集和存储分布式服务的日志。
- 提供日志的全文检索和堆栈分析功能。
- 作为 Prometheus + Grafana 的补充工具,用于定位具体问题。
10.2 Spring Boot Actuator
10.2.1. 什么是 Actuator?
Spring Boot Actuator 是 Spring 提供的一套开箱即用的工具,用于监控和管理服务。它的主要功能是:
- 健康检查:检测服务是否正常运行,比如数据库连接是否可用、磁盘空间是否充足等。
- 暴露核心指标:提供运行时的性能数据,比如内存使用率、CPU 使用率、HTTP 请求次数等。
10.2.2. Actuator 的核心功能
1. 健康检查
- 地址:
/actuator/health
- 返回服务的健康状态信息。
示例:
{
"status": "UP",
"components": {
"db": { "status": "UP", "details": { "database": "MySQL", "result": "ok" } },
"diskSpace": { "status": "UP", "details": { "total": 50000000000, "free": 20000000000 } }
}
}
解读:
status: UP
表示服务正常。components
显示健康检查的各项子系统状态,比如数据库连接和磁盘空间。
2. 核心指标
- 地址:
/actuator/metrics
- 返回服务的性能指标。
示例:
{
"names": [
"jvm.memory.used",
"system.cpu.usage",
"http.server.requests"
]
}
解读:
- 可以查询 JVM 内存使用量(
jvm.memory.used
)、CPU 使用率(system.cpu.usage
)等指标。
3. 自定义健康检查
开发者可以根据业务需求定义自定义的健康检查逻辑。
示例代码:
@Component
public class CustomHealthIndicator implements HealthIndicator {
@Override
public Health health() {
boolean dbConnectionOk = checkDatabase();
if (dbConnectionOk) {
return Health.up().withDetail("Database", "Connected").build();
}
return Health.down().withDetail("Database", "Disconnected").build();
}
private boolean checkDatabase() {
// 自定义检查逻辑,比如尝试连接数据库
return true;
}
}
功能:
- 如果自定义服务(比如数据库)不可用,Actuator 的
/health
会返回异常状态,方便及时发现问题。
10.3 Prometheus + Grafana
10.3.1. 什么是 Prometheus 和 Grafana?
- Prometheus:
- 开源的监控工具,专门用于收集和存储时间序列数据(比如每秒请求数、CPU 使用率)。
- 支持自定义告警规则,当指标超过阈值时发送通知。
- Grafana:
- 数据可视化工具,可以将 Prometheus 收集的数据通过图表、仪表盘实时展示。
10.3.2. Prometheus + Grafana 的核心功能
1. 数据采集与存储(Prometheus)
Prometheus 定期访问 Actuator 暴露的 /metrics
接口,采集服务性能数据并存储。
2. 数据可视化(Grafana)
Grafana 从 Prometheus 获取数据,并以图表、仪表盘形式展示服务性能。例如:
- 实时查看 CPU 使用率、内存占用等。
- 查看过去一小时内的请求数变化趋势。
3. 实时告警
Prometheus 支持基于指标配置告警规则,例如:
- 当 CPU 使用率超过 90% 持续 5 分钟时,发送邮件通知。
- 当 HTTP 请求失败率超过某个阈值时,触发警报。
10.3.3. Prometheus + Grafana 的整合流程
1. Prometheus 配置
Prometheus 需要配置一个 prometheus.yml
文件,用于指定要监控的服务:
scrape_configs:
- job_name: 'spring-boot'
static_configs:
- targets: ['localhost:8080'] # 采集 Actuator 暴露的指标
2. Spring Boot 集成 Micrometer
添加依赖:
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
在 application.properties
中启用 Prometheus 端点:
management.endpoints.web.exposure.include=prometheus
Actuator 的 /actuator/prometheus
接口会返回 Prometheus 格式的数据。
3. Grafana 配置
安装并启动 Grafana。
添加 Prometheus 数据源:
- 数据源 URL:
http://localhost:9090
(Prometheus 地址)。
创建仪表盘:
- 使用 PromQL 查询数据,例如:
rate(http_server_requests_seconds_sum[1m])
展示服务请求速率、错误率等数据。
10.4 ELK Stack
10.4.1. 什么是 ELK Stack?
ELK 是 Elasticsearch、Logstash 和 Kibana 的组合,主要用于 日志收集、存储和分析。
- Elasticsearch:
- 用于存储和检索日志数据。
- Logstash:
- 日志收集工具,将日志从各种来源(如文件、数据库)发送到 Elasticsearch。
- Kibana:
- 日志的图形化展示工具,支持全文检索和日志分析。
10.4.2. ELK Stack 的核心功能
- 日志聚合:集中收集所有微服务的日志,避免分散存储导致分析困难。
- 快速查询:支持复杂的日志搜索,例如根据时间范围检索错误日志。
- 问题定位:分析堆栈错误、慢查询、网络延迟等问题。
10.4.3. ELK Stack 的整合流程
1. 配置 Logstash
创建 logstash.conf
:
input {
file {
path => "/var/log/my-service.log" # 日志文件路径
start_position => "beginning"
}
}
output {
elasticsearch {
hosts => ["localhost:9200"] # Elasticsearch 地址
}
}
启动 Logstash:
./logstash -f logstash.conf
2. 配置 Elasticsearch
启动 Elasticsearch:
./elasticsearch
默认运行在 http://localhost:9200
。
3. 配置 Kibana
启动 Kibana:
./kibana
通过浏览器访问 http://localhost:5601
。
配置日志索引(如 logstash-*
),并创建仪表盘。
11. 容器化与部署
微服务架构强调快速部署与扩展,而容器化技术(如 Docker)是实现这一目标的关键工具。Docker 提供了轻量级、可移植的环境,可以轻松将应用打包成镜像,运行在任何支持 Docker 的平台上。同时,通过 Docker Compose 可以实现多个微服务的协调部署。
11.1 Docker:容器化 Spring Boot 项目
11.1.1. 什么是 Docker?
Docker 是一种容器化技术,可以将应用及其依赖打包到一个标准化的单元中,便于跨环境运行。Docker 的主要特点:
- 轻量级:一个容器仅包含运行所需的最小资源。
- 快速部署:启动容器的速度比传统虚拟机快得多。
- 隔离性:每个容器相互独立,互不干扰。
11.1.2. 容器化 Spring Boot 项目的基本流程
创建 Dockerfile
Dockerfile 是一个脚本文件,定义了如何构建 Docker 镜像的步骤。以下是容器化 Spring Boot 项目的 Dockerfile 示例:
# 使用官方的 Java 运行环境镜像
FROM openjdk:17-jdk-alpine
# 设置工作目录
WORKDIR /app
# 将 Spring Boot 项目的 JAR 文件复制到容器中
COPY target/my-app-1.0.jar app.jar
# 暴露服务端口
EXPOSE 8080
# 定义容器启动时的命令
ENTRYPOINT ["java", "-jar", "app.jar"]
代码解析:
FROM
:指定基础镜像,openjdk:17-jdk-alpine
是一个轻量级的 Java 运行环境。WORKDIR
:设置容器的工作目录为/app
。COPY
:将编译好的 JAR 文件复制到容器中。EXPOSE
:声明容器对外暴露的端口(与 Spring Boot 应用的端口一致)。ENTRYPOINT
:定义容器启动时的命令,运行 Spring Boot 项目的 JAR 文件。
构建 Docker 镜像
打包 Spring Boot 项目:
mvn clean package
输出的 JAR 文件通常位于 target
目录下。
构建 Docker 镜像:
docker build -t my-app:1.0 .
命令解析:
-t my-app:1.0
:为镜像指定名称和版本。.
:指向 Dockerfile 所在的目录。
查看生成的镜像:
docker images
示例输出:
REPOSITORY TAG IMAGE ID CREATED SIZE
my-app 1.0 abc12345 10 seconds ago 500MB
运行 Docker 容器
启动容器:
docker run -d -p 8080:8080 --name my-app-container my-app:1.0
命令解析:
-d
:后台运行容器。-p 8080:8080
:将宿主机的 8080 端口映射到容器的 8080 端口。--name my-app-container
:为容器指定名称。
查看容器状态:
docker ps
示例输出:
CONTAINER ID IMAGE COMMAND STATUS PORTS NAMES
12345abcde my-app:1.0 "java -jar app.jar" Up 2 minutes 0.0.0.0:8080->8080/tcp my-app-container
访问服务: 在浏览器或 Postman 中访问 http://localhost:8080
,验证服务是否正常运行。
11.2 Docker Compose:多服务部署
11.2.1. 什么是 Docker Compose?
Docker Compose 是一个工具,用于定义和运行多容器 Docker 应用。通过一个 YAML 文件,可以配置多个服务的部署参数,实现微服务的快速部署。
11.2.2. Docker Compose 的基本结构
以下是一个示例 docker-compose.yml
文件,定义了两个服务(app
和 db
)的部署。
version: '3.8'
services:
app:
image: my-app:1.0
build:
context: .
dockerfile: Dockerfile
ports:
- "8080:8080"
depends_on:
- db
db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: mydb
MYSQL_USER: user
MYSQL_PASSWORD: password
ports:
- "3306:3306"
文件解析
version
:- 定义 Compose 文件的版本,这里使用
3.8
。
- 定义 Compose 文件的版本,这里使用
services
:- 定义多个服务:
app
:表示 Spring Boot 服务。db
:表示 MySQL 数据库服务。
- 定义多个服务:
build
:- 指定
app
服务的镜像来源,可以通过Dockerfile
构建镜像。
- 指定
ports
:- 声明容器与宿主机的端口映射,例如将 MySQL 的 3306 端口映射到宿主机的 3306 端口。
depends_on
:- 定义服务间的依赖关系,
app
服务依赖于db
服务。
- 定义服务间的依赖关系,
environment
:- 定义环境变量,如 MySQL 的数据库名称、用户名和密码。
11.2.3. 使用 Docker Compose 部署
创建 docker-compose.yml
文件,并放置在项目根目录。
启动服务:
docker-compose up -d
命令解析:
up
:启动 Compose 定义的服务。-d
:后台运行。
查看服务状态:
docker-compose ps
停止服务:
docker-compose down
11.3 综合使用:生产环境部署建议
1. 基本流程
- 开发阶段:
- 使用 Dockerfile 将每个微服务容器化。
- 使用 Docker Compose 测试多服务的交互。
- 测试和生产阶段:
- 使用容器编排工具(如 Kubernetes 或 Docker Swarm)管理多个容器的部署、扩展和监控。
2. 容器化的好处
- 快速部署:一个命令即可启动整个微服务集群。
- 环境一致性:开发、测试和生产环境完全一致。
- 轻量级:相比虚拟机,容器的资源占用更少,启动更快。
- 易于扩展:通过配置即可轻松扩展服务的实例数量。
原文地址:https://blog.csdn.net/m0_73837751/article/details/142904554
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!