自学内容网 自学内容网

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-clienteureka-client)向注册中心发送注册请求,包含以下信息:

  • 服务名称:唯一标识服务的逻辑名称(如 user-service)。
  • 实例信息:实例的 IP 地址、端口号、元数据(如版本号)。
  • 健康信息:服务是否健康的标记。

2.注册中心保存信息: 注册中心将这些信息存储在注册表中,以供其他服务发现。

3.动态更新和维护:

  • 心跳机制:服务实例定期向注册中心发送心跳包,表明服务正常运行。
  • 服务下线:服务实例关闭或异常时,注册中心会移除该实例。

1.1.3 示例:使用 Nacos 实现服务注册

(1)安装 Nacos 注册中心

下载 NacosNacos 官方下载链接

启动 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 服务发现的工作原理

  1. 查询服务列表:服务消费者通过注册客户端向注册中心发送查询请求,指定服务名(如 product-service)。
  2. 返回实例列表:注册中心返回目标服务的所有实例信息(包括 IP、端口和元数据)。
  3. 负载均衡选择实例:消费者根据负载均衡策略(如轮询、随机)选择一个实例并发起调用。

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-serviceuser-service
  • 访问 http://localhost:8082/user/product/123
  • 结果应返回:
    Product ID: 123
    

1.3 服务健康检查

1.3.1 什么是服务健康检查

服务健康检查用于检测服务实例的运行状态,确保注册表中的服务是健康可用的。如果某服务实例不可用,注册中心会将其从注册表中移除,避免请求失败。

1.3.2 Nacos 健康检查机制

主动心跳检测

  • 服务实例定期向注册中心发送心跳包(默认每 5 秒),通知注册中心自己在线。

被动健康检查

  • 注册中心主动向服务实例发送探测请求(如 HTTP),检查其运行状态。

1.3.3 示例:模拟服务宕机

  1. 启动服务后,手动关闭 product-service
  2. 在 Nacos 控制台的 服务列表 中,可以看到 product-service 的实例状态变为不健康,并被移除。

2. 服务间通信

服务间通信是微服务架构中的核心环节,用于实现服务之间的协作。在微服务中,服务之间通常通过 HTTP 通信来实现数据交互。Spring Cloud 提供了多种方式来实现服务间通信,包括 FeignRestTemplate,同时结合 Spring Cloud LoadBalancer 实现负载均衡。


2.1 Feign(声明式服务调用)

2.1.1 什么是 Feign

Feign 是 Spring Cloud 提供的声明式服务调用工具,它将 HTTP 请求抽象成 Java 接口,开发者只需定义接口方法和注解,就可以调用远程服务,无需手动构造 HTTP 请求。

2.1.2 Feign 的特点

  1. 声明式调用:通过接口方法调用远程服务,代码清晰易读。
  2. 集成负载均衡:与 Spring Cloud LoadBalancer 集成,可实现多实例负载均衡。
  3. 自动解析返回值:支持将远程服务返回的 JSON 自动映射为 Java 对象。

2.1.3 Feign 的工作原理

  1. 接口定义:开发者定义一个接口,使用 @FeignClient 注解标注目标服务名称。
  2. 方法映射:通过注解(如 @GetMapping@PostMapping)指定 HTTP 请求的路径和参数。
  3. 代理实现:Spring Cloud 自动生成接口的代理类,将接口方法调用转换为 HTTP 请求。
  4. 负载均衡: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-serviceuser-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 网关的核心功能:

  1. 路由转发:将客户端的请求根据规则转发到后端服务。
  2. 统一认证:实现集中式认证和权限校验,避免重复开发。
  3. 流量控制:通过限流和熔断保护后端服务。
  4. 日志监控:记录请求日志,便于分析和排查问题。

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 的实例。
测试动态路由
  1. 启动注册中心(如 Nacos)和各微服务。
  2. 访问网关地址:
    • 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 控制台添加配置:

  • DataIdconfig-demo.yaml(对应服务名称加后缀)
  • GroupDEFAULT_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 实现
多环境支持支持命名空间、分组管理环境支持环境的配置文件分层(如 devprod
操作界面提供友好的 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-attemptswait-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 总结

功能Resilience4jSentinel
熔断降级通过 @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-servicepayment-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-servicepayment-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 SleuthZipkinJaeger
核心功能生成并传递 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");
    }
}

代码解释:

  1. Queue:声明队列 my-queue,并设置持久化。

  2. DirectExchange:声明 Direct 类型的交换机 my-exchange

  3. 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 StreamRabbitMQKafka
消息模型抽象化消息生产和消费,适配多种中间件支持点对点、发布订阅等模式高吞吐量,支持日志存储和流式处理
消息确认机制支持手动确认提供自动和手动确认模式提供 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
    • 用户每次访问新的微服务时,必须重新登录。

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 中可以包含用户的角色信息,各服务根据角色完成授权。

9.1.4. OAuth2

1 OAuth2 的作用

OAuth2 是一个开放授权协议,允许第三方应用在用户授权的情况下访问用户资源。在微服务安全中,OAuth2 通常用于:

  • 集中认证
    • 用户只需登录一次,多个微服务共享认证状态。
  • 资源授权
    • 为用户的资源提供细粒度的访问控制。
  • 单点登录(SSO)
    • 用户登录后,可以无缝访问多个服务。

2 OAuth2 的核心角色

  • 认证服务器(Authorization Server)
    • 负责用户登录和生成访问令牌(Access Token)。
  • 资源服务器(Resource Server)
    • 提供 API 服务,验证访问令牌,确认用户权限。
  • 客户端(Client)
    • 发起用户登录请求,并使用访问令牌访问资源。

3 OAuth2 的核心流程

  1. 用户向认证服务器发起登录请求。
  2. 认证服务器验证用户信息,并生成访问令牌。
  3. 用户携带访问令牌请求资源服务器。
  4. 资源服务器通过认证服务器验证令牌合法性,并完成授权。

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 的工作流程

登录认证流程(认证服务负责)
  1. 用户提交用户名和密码到 认证服务
  2. 认证服务验证用户信息(通常从数据库查询)。
  3. 验证成功后,认证服务生成一个 JWT,并返回给用户。
  4. 用户将 JWT 存储在客户端(如浏览器的 LocalStorageCookie 中)。

请求验证流程(微服务负责)
  1. 用户在后续请求中,将 JWT 附加到 HTTP 请求头中,格式如下:
    Authorization: Bearer <JWT>
    
  2. 微服务通过 JwtFilter 拦截请求,从请求头中提取 JWT。
  3. 验证 JWT 的合法性(校验签名和过期时间)。
  4. 如果 JWT 验证通过:
    • 提取 JWT 中的用户信息。
    • 设置用户身份到 Spring Security 的上下文中。
  5. 如果 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。

@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

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 之前。

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. 工作流程概览

用户认证与令牌生成

  1. 用户通过客户端向 认证服务器 提交用户名和密码。
  2. 认证服务器验证用户信息。
  3. 验证成功后,认证服务器返回 访问令牌(Access Token) 和可选的 刷新令牌(Refresh Token)
  4. 客户端存储令牌(如浏览器或移动端存储)。

资源访问与令牌验证

  1. 客户端携带访问令牌向 资源服务器 发起资源请求。
  2. 资源服务器通过认证服务器验证令牌的合法性(通过远程调用或本地校验)。
  3. 如果验证通过,资源服务器返回受保护的资源。

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 客户端信息:
      • 客户端 IDclient-id,客户端的唯一标识。
      • 客户端密钥client-secret,用于客户端验证。
      • 授权模式:支持密码模式(password)和刷新令牌(refresh_token)。
      • 作用域readwrite,定义客户端的权限范围。
  • 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 三者综合使用

在生产环境中,可以将 ActuatorPrometheus + GrafanaELK 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 文件,定义了两个服务(appdb)的部署。

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
  • 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)!