从0到1创建一个微服务项目

创建微服务项目

项目结构如下所示

1. 首先创建一个父工程,用于管理spring-boot、spring-cloud、spring-cloud-alibaba 的版本。

删除父工程的src目录,只留下 pom.xml 文件,在pom文件按以下步骤修改

  • 在 project 根标签内添加 <packaging>pom</packaging>标签,表明这是一个聚合/父工程。
  • 在父工程pom文件的properties标签内管理 spring-boot、spring-cloud、spring-cloud-alibaba的版本。
  • 在 dependencyManagement 标签内导入依赖。

完整pom如下所示

复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <!-- 打包方式,表明这是一个聚合/父工程-->
    <packaging>pom</packaging>
    <modules>
        <module>services</module>
    </modules>
    <groupId>com.test</groupId>
    <artifactId>cloud-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>cloud-demo</name>
    <description>cloud-demo</description>
&lt;properties&gt;
    &lt;!-- 统一版本管理 --&gt;
    &lt;!-- java版本 --&gt;
    &lt;java.version&gt;1.8&lt;/java.version&gt;
    &lt;maven.compiler.source&gt;1.8&lt;/maven.compiler.source&gt;
    &lt;maven.compiler.target&gt;1.8&lt;/maven.compiler.target&gt;
    &lt;!-- 项目编码 --&gt;
    &lt;project.build.sourceEncoding&gt;UTF-8&lt;/project.build.sourceEncoding&gt;
    &lt;spring-boot.version&gt;2.7.18&lt;/spring-boot.version&gt;
    &lt;spring-cloud.version&gt;2021.0.9&lt;/spring-cloud.version&gt;
    &lt;spring-cloud-alibaba.version&gt;2021.0.6.1&lt;/spring-cloud-alibaba.version&gt;
&lt;/properties&gt;
&lt;dependencyManagement&gt;
&lt;dependencies&gt;
&lt;dependency&gt;
&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
&lt;artifactId&gt;spring-boot-dependencies&lt;/artifactId&gt;
&lt;version&gt;${spring-boot.version}&lt;/version&gt;
&lt;type&gt;pom&lt;/type&gt;
&lt;scope&gt;import&lt;/scope&gt;
&lt;/dependency&gt;
&lt;dependency&gt;
&lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt;
&lt;artifactId&gt;spring-cloud-dependencies&lt;/artifactId&gt;
&lt;version&gt;${spring-cloud.version}&lt;/version&gt;
&lt;type&gt;pom&lt;/type&gt;
&lt;scope&gt;import&lt;/scope&gt;
&lt;/dependency&gt;
&lt;dependency&gt;
&lt;groupId&gt;com.alibaba.cloud&lt;/groupId&gt;
&lt;artifactId&gt;spring-cloud-alibaba-dependencies&lt;/artifactId&gt;
&lt;version&gt;${spring-cloud-alibaba.version}&lt;/version&gt;
&lt;type&gt;pom&lt;/type&gt;
&lt;scope&gt;import&lt;/scope&gt;
&lt;/dependency&gt;
&lt;/dependencies&gt;
&lt;/dependencyManagement&gt;
&lt;build&gt;
&lt;plugins&gt;
&lt;plugin&gt;
&lt;groupId&gt;org.apache.maven.plugins&lt;/groupId&gt;
&lt;artifactId&gt;maven-compiler-plugin&lt;/artifactId&gt;
&lt;version&gt;3.8.1&lt;/version&gt;
&lt;configuration&gt;
&lt;source&gt;1.8&lt;/source&gt;
&lt;target&gt;1.8&lt;/target&gt;
&lt;encoding&gt;UTF-8&lt;/encoding&gt;
&lt;/configuration&gt;
&lt;/plugin&gt;
&lt;plugin&gt;
&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
&lt;artifactId&gt;spring-boot-maven-plugin&lt;/artifactId&gt;
&lt;version&gt;${spring-boot.version}&lt;/version&gt;
&lt;configuration&gt;
&lt;mainClass&gt;com.test.clouddemo.CloudDemoApplication&lt;/mainClass&gt;
&lt;skip&gt;true&lt;/skip&gt;
&lt;/configuration&gt;
&lt;executions&gt;
&lt;execution&gt;
&lt;id&gt;repackage&lt;/id&gt;
&lt;goals&gt;
&lt;goal&gt;repackage&lt;/goal&gt;
&lt;/goals&gt;
&lt;/execution&gt;
&lt;/executions&gt;
&lt;/plugin&gt;
&lt;/plugins&gt;
&lt;/build&gt;
</project>

2. 在父工程的基础上创建一个模块A,用于管理各子模块的公共依赖,这样各子模块就不用重复导入公共依赖。

在模块A中同样删除src目录,修改pom文件。

  • 在 project 根标签内添加 <packaging>pom</packaging>标签,表明这是一个聚合/父工程。

  • 在 dependencies 标签上引入 nacos 和 openfeign 的依赖。


    4.0.0

    com.test
    cloud-demo
    0.0.1-SNAPSHOT

    <packaging>pom</packaging>
    <modules>
    <module>services-product</module>
    <module>services-order</module>
    </modules>
    <artifactId>services</artifactId>
    <properties>
    <maven.compiler.source>8</maven.compiler.source>
    <maven.compiler.target>8</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>


    com.alibaba.cloud
    spring-cloud-starter-alibaba-nacos-discovery

    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>

3. 在模块A的基础上创建各子模块,同步骤2创建了order、product两个子模块。

4. 点击IDEA右侧的maven图标,点击右上角三个小点点,勾选 Group Modules,就会以层级的方式显示依赖关系。下图可以看到order、product模块继承了services模块的依赖。

一、Nacos注册中心

1. 安装Nacos

进入到官网下载 Nacos安装包,下载解压即可。找到startup.cmd 双击即可启动,随后复制命令窗口的管理网址(8848端口)可进入Nacos管理页面。也可以通过命令启动。

复制代码
startup.cmd -m standalone

2. 子模块服务注册

由于order子模块继承了根项目,所以order子模块也是一个springBoot项目,只需要在order子模块中引入web依赖即可。如果需要将各服务注册到Nacos,还需要Nacos依赖,但是我们还有一个管理公共依赖的服务services,在那已经引入了, 所以在子模块中会继承,不再重复引入了。

复制代码
<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
</dependencies>

在order子模块中创建一个主入口文件。

编写启动类。

复制代码
package com.aikumao;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class OrderMainApplication {
public static void main(String[] args) {
SpringApplication.run(OrderMainApplication.class, args);
}
}

编写配置文件,将nacos的服务地址配置到项目中,并且指定服务名(方便显示在nacos管理页面中),如果nacos获取不到服务名,则会注册失败。

复制代码
server:
  port: 6000  #程序端口号
spring:
application:
name: service-order  # 注册中心显示程序名称
cloud:
nacos:
server-addr: 127.0.0.1:8848 #nacos服务地址

启动order服务后,可以在Nacos管理页面看到注册的服务。

同理在product模块按同样步骤,修改对应的程序名称和端口即可。

3. 模拟集群服务

首先调试 idea 显示多个服务启动状态(services栏)

  1. 在view------> Tool windows------> services 中调出 services栏

在idea左下角按照如图所示, Add service------> Run configuration Type ------> spring Boot即可。

创建新的实例,修改端口号。选中需要复制的实例,copy configuration------> modify options------> program argements, 添加参数 --server.port=端口号

重新进入Nacos 管理页面,发现服务都被注册了。

4. 子模块服务发现

在程序的启动入口标注 @EnableDiscoveryClient 注解,作用是开启服务发现的功能。

在 product 中 加入测试依赖

复制代码
<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
</dependency>

创建一个测试类,注入 DiscoveryClient 实例, 通过 getServices 就能获取每个服务services 。每个服务可以获取IP、端口等信息。

复制代码
package com.aikumao;
@SpringBootTest
public class DiscoverTest {
@Autowired
private DiscoveryClient discoveryClient;
@Test
void discoverClient() {
for (String service : discoveryClient.getServices()) {
System.out.println("service = " + service);
List&lt;ServiceInstance&gt; instances = discoveryClient.getInstances(service);
for (ServiceInstance instance : instances) {
System.out.println("instance = " + instance.getInstanceId() + ", port = " + instance.getPort());
}
}
}
}

5. 远程调用服务(编程式)

编程式远程调用可以理解为整个远程调用需要自行手动编码,如先获取远程地址列表,在地址列表挑选一个地址,再对这个地址发送请求,然后接受返回的结果。

  1. 在product、order模块分别创建一个接口(controller层、service、serviceImpl、pojo),product是获取商品信息的接口,order是创建订单接口,通过测试后。在order模块中获取不到product实体 (因为order模块没有product实体), 因此,把product、order模块的实体部分抽取出来 ,在顶级模块目录下(cloud-demo模块下,与service模块同级目录)新建一个modle模块存放实体。目录结构如下:

product 接口实现

复制代码
public class ProductServiceImpl implements ProductService {
@Override
public Product queryProductById(Long id) {
    return new Product(id, new BigDecimal(1000), "华为手机-" + id, 10);
}
}

order 接口实现

复制代码
@Slf4j
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private RestTemplate restTemplate;
@Autowired
private DiscoveryClient discoveryClient;
@Override
public Order createOrder(Long productId, Long userId) {
Product product = getProductFromRemote(productId);  // 调用私有方法获取商品信息
Order order = new Order();
order.setId(1L);
order.setTotalAmount(new BigDecimal(product.getPrice().doubleValue() * product.getNum()));  // 总金额
order.setUserId(userId);
order.setAddress("浙江省");
order.setProductList(Arrays.asList(product));
return order;
}
private Product getProductFromRemote(Long productId) {
// 从注册中心获取到服务提供者地址
List&lt;ServiceInstance&gt; instances = discoveryClient.getInstances("service-product");  // service-product 是服务名
// 每次获取第一个服务提供者实例对象
ServiceInstance serviceInstance = instances.get(0);
// 构建远程调用地址
String url = "http://" + serviceInstance.getHost() + ":" + serviceInstance.getPort() + "/getProductById/" + productId;
log.info("远程请求: {}", url);
// 远程调用product商品信息
return restTemplate.getForObject(url, Product.class);
}
}

重新启动服务,在浏览器输入 127.0.0.1: 服务端口?参数=XXX&参数2=XXX。可以看到总金额是通过单价和数量计算出来的。

6. 负载均衡的调用

上面通过获取第一个节点来调用,后续所有请求都在第一个节点上,而不会到其他节点上,或者某个节点宕机了,通过负载均衡就可以让请求很好的分布在各个节点上。在调用远程服务方添加负载均衡loadbalancer 依赖。这里是订单服务调用商品服务,因此需要在订单服务上添加负载均衡依赖。

具体步骤:

  1. 调用服务方引入负载均衡依赖:spring-cloud-starter-loadbalancer

  2. 调用服务方在需要使用负载均衡的实现类引入客户端:LoadBalancerClient

  3. 通过 choose方法获取实例对象(只返回一个实例对象),loadBalancerClient.choose("远程服务名") ,而 discoveryClient.getInstances("service-product") 是返回所有实例对象。

方式一(推荐):注解方式

  1. 在配置类上添加 LoadBalanced 注解

    @Configuration
    public class RestTemplateConf {
    @LoadBalanced
    @Bean
    public RestTemplate restTemplate() {
    return new RestTemplate();
    }
    }

构建远程地址时不再通过拼接 IP + 端口的方式,而是直接写远程服务名,负载均衡底层会指定使用哪个服务IP。

复制代码
private Product getProductFromRemoteWithLoadBalancer1(Long productId) {
        // 构建远程调用地址
        String url = "http://service-product/getProductById/" + productId;
        log.info("远程请求: {}", url);
        // 远程调用product商品信息
        return restTemplate.getForObject(url, Product.class);
    }

方式二:

负载均衡核心代码,在需要位置调用该方法即可。

复制代码
 // 负载均衡发送请求
    private Product getProductFromRemoteWithLoadBalancer(Long productId) {
        // 从注册中心获取到服务提供者地址
        ServiceInstance serviceInstance = loadBalancerClient.choose("service-product");  // service-product 是服务名
        // 构建远程调用地址
        String url = "http://" + serviceInstance.getHost() + ":" + serviceInstance.getPort() + "/getProductById/" + productId;
        log.info("远程请求: {}", url);
        // 远程调用product商品信息
        return restTemplate.getForObject(url, Product.class);
    }

访问地址:http://127.0.0.1:6001/createOrder?productId=1&userId=300

可以发现每次的端口都不一致,实现了负载均衡。

7. 配置中心使用

  1. 启动 Nacos

  2. 引入 Nacos 配置中心依赖

  3. 项目的application.yml 配置文件指定 nacos 导入的文件名

  4. 在 nacos 管理页面创建 data-id (数据集)

引入依赖

复制代码
 <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

配置文件如下

复制代码
spring:
  application:
    name: service-order  # 注册中心显示程序名称
  cloud:
    nacos:
      server-addr: 127.0.0.1:8848 #nacos服务地址
  config:
    import:
      - optional:nacos:services-order.yaml  #从nacos导入配置文件

nacos管理页面配置文件

在controller层编写一个接口作为测试

复制代码
@RestController
public class OrderController {
@Value("${order.timeout}")
String timeout;
@Value("${order.auto-confirm}")
String autoConfirm;
@GetMapping("getConfig")
public String config() {
return "timeout=" + timeout + " autoConfirm=" + autoConfirm;
}
}

动态刷新

1. 方式一:动态刷新

@Value("${XX}") 获取配置 + @RefreshScope 实现自动刷新

在类上添加一个 @RefreshScope 注解即可,通过@Value注入属性。但如果有多个属性,通过一个个注入很麻烦,方式二可解决这个问题。

复制代码
@RefreshScope
@RestController
public class OrderController {
@Value("${order.timeout}")
String timeout;
@Value("${order.auto-confirm}")
String autoConfirm;
@GetMapping("getConfig")
public String config() {
return "timeout=" + timeout + " autoConfirm=" + autoConfirm;
}
}

2. 方式二:动态刷新

自定义一个类专门存放属性,通过@Component + @ConfigurationProperties 无感自动刷新

复制代码
@Component
@ConfigurationProperties(prefix = "order")
public class OrderProperties {
private String timeout;
private String autoConfirm;
// 省略getter\setter方法
}

测试接口,不再单个导入属性,而是注入存放属性的类的方式获取。

复制代码
@RestController
public class OrderController {
@Autowired
private OrderProperties orderProperties;
@GetMapping("getConfig")
public String config() {
return "timeout=" + orderProperties.getTimeout() + " autoConfirm=" + orderProperties.getAutoConfirm();
}

8. 配置中心---数据隔离(多环境配置)

一个项目有多个配置环境,如开发环境、测试环境、生产环境。项目可以切换到不同的环境让配置生效,通过 Namespace 区分配置环境,通过Group 区分哪个项目的配置,通过数据集(Data-id) 按需加载。

  1. 创建命名空间
  1. 在配置管理中选择对应的命名空间添加配置
  1. 在项目的配置文件动态导入

    server:
    port: 6000 #程序端口号
    spring:
    profiles:
    active: dev
    application:
    name: service-order # 注册中心显示程序名称
    cloud:
    nacos:
    server-addr: 127.0.0.1:8848 #nacos服务地址
    config:
    import-check:
    enabled: false # 关闭导入检查
    namespace: ${spring.profiles.active:dev} # nacos命名空间, 根据 profiles.active 判断当前的环境,如果未配置则使用 dev
    spring:
    config:
    import:

    • optional:nacos:common.yaml?group=order # 加载 nacos common.yaml 配置
    • optional:nacos:database.yaml?group=order # 加载 nacos database.yaml 配置
      activate:
      on-profile: dev # 动态激活条件
      spring:
      config:
      import:
    • optional:nacos:common.yaml?group=order # 加载 nacos common.yaml 配置
    • optional:nacos:database.yaml?group=order # 加载 nacos database.yaml 配置
      activate:
      on-profile: test
      spring:
      config:
      import:
    • optional:nacos:common.yaml?group=order # 加载 nacos common.yaml 配置
    • optional:nacos:database.yaml?group=order # 加载 nacos database.yaml 配置
      activate:
      on-profile: prod

二、OpenFeign 远程调用

声明式远程调用就是通过注解的方式指定远程地址,请求方式,携带参数等。

指定远程地址:@FeignClient

指定请求方式:@GetMapping 、@PostMapping 、 @ DeleteMapping 、 @PutMapping

指定携带数据:@RequestHeader 、@RequestParam 、 @RequestBody

只当结果返回:响应模型

  1. 添加 OpenFeign 依赖

    org.springframework.cloud spring-cloud-starter-openfeign
  2. 在远程调用启动类上添加 @EnableFeignClients 注解

    @EnableFeignClients
    @SpringBootApplication
    public class OrderMainApplication {
    public static void main(String[] args) {
    SpringApplication.run(OrderMainApplication.class, args);
    }
    }

  3. 创建远程调用的接口 ProductOpenFeignClient ,表示向 Product 发送远程调用

    @FeignClient(value = "service-product", path="/product") // feign 客户端, path 是controller类上的路径
    public interface ProductOpenFeignClient {
    @GetMapping("/getProductById/{id}")
    // mvc注解两套逻辑
    // 1. 标注在controller上,是接受请求
    // 2. 标注在feignClient上,是发送请求
    Product getProductId(@PathVariable("id") Long id);
    }

当执行到远程调用的方法时,会通过 @FeignClient(value = "service-product") 去注册中心找到活跃的地址列表,然后通过默认的负载均衡选取一个地址发送请求,通过 Product 接收返回的参数是一个实体,如果用String 去接收,则返回原生 json数据

  1. 注入 ProductOpenFeignClient 接口,调用 getProductId 方法

    @Service
    public class OrderServiceImpl implements OrderService {
    @Autowired
    private ProductOpenFeignClient productOpenFeignClient;
    @Override
    public Order createOrder(Long productId, Long userId) {
    // 使用 OpenFeign 发送服务
    Product product = productOpenFeignClient.getProductId(productId);
    Order order = new Order();
    order.setId(1L);
    order.setTotalAmount(new BigDecimal(product.getPrice().doubleValue() * product.getNum())); // 总金额
    order.setUserId(userId);
    order.setAddress("浙江省");
    order.setProductList(Arrays.asList(product));
    return order;
    }
    }

服务提供者controller层

复制代码
@RequestMapping("/product")
@RestController
public class ProductController {
@Autowired
private ProductService productService;
@GetMapping("/getProductById/{id}")
public Product queryProductById(@PathVariable("id") Long id) {
System.out.println("hello");
return productService.queryProductById(id);
}
}

1. OpenFeign 日志配置

  1. 在项目的配置信息中添加 SpringBoot 的 日志。 logging.level=包路径: 级别

  2. 自定义一个配置类定义 OpenFeign 的日志。

复制代码
logging:
  level:
    com.aikumao.feign: debug

@Configuration
public class OpenFeignConf {
@Bean
public Logger.Level feignLoggerLevel() {
    return Logger.Level.FULL;
}
}

调用接口,控制台详细日志就打印了

2. 超时控制

OpenFiegn 超时有连接超时 (connectTimeout)和读取超时(readTimeout),默认连接超时时间为10秒,读取超时为60秒。

复制代码
feign:
  client:
    config:
      default:   # 默认配置
        logger-level: full
        connect-timeout: 1000   # 改为连字符
        read-timeout: 2000      # 改为连字符
      service-product:  # 指定客户端的配置, service-product 是 @FeignClient注解 的 value 值
        logger-level: full
        connect-timeout: 3000  # 默认连接超时时间为10秒,读取超时为60秒
        read-timeout: 5000

服务提供方通过睡眠模拟超时

复制代码
@Service
public class ProductServiceImpl implements ProductService {
@Override
public Product queryProductById(Long id) {
    try {
        Thread.sleep(20000);
} catch (InterruptedException e) {
    throw new RuntimeException(e);
}
return new Product(id, new BigDecimal(1000), "华为手机-" + id, 10);
}
}

3. 重试机制

如果远程调用失败,可以进行多次尝试,如果某次成功返回ok,如果多次失败结束远程调用,返回fail。默认配置为下图代码:

100L表示首次间隔100毫秒重试,如果还是失败,则等待时间为原来间隔的1.5倍再重试,如果还是失败,则等待时间为原来间隔的1.5倍再重试,SECONDS.toMillis(1L) 表示最大等待时间为1秒,如果等待时间超过1秒,则按1秒间隔重试;5表示重试5次结束。

在配置类中添加一个配置类,即可重试

复制代码
@Configuration
public class OpenFeignConf {
@Bean
Retryer retryer() {
    return new Retryer.Default();
}
}

4. 拦截器

请求拦截器就是在请求到达对方服务前,先对请求进行定制改造后再到对方服务器。

  1. 编写拦截器

    @Slf4j
    public class XTokenRequestInterceptor implements RequestInterceptor {
    /请求拦截器/
    @Override
    public void apply(RequestTemplate requestTemplate) {
    log.info("请求拦截器启动了......");
    requestTemplate.header("X-Token", UUID.randomUUID().toString());
    }
    }

  2. 要让拦截器配置生效,有2中方式:

2.1方式1局部生效 : 在yml 配置文件 通过 request-interceptors 参数引入刚才编写的 XTokenRequestInterceptor 拦截器的类路径。但这个只对某个服务生效

复制代码
feign:
  client:
    config:
      default:   # 默认配置
        logger-level: full
      service-product:  # 指定客户端的配置, service-product 是 @FeignClient注解 的 value 值
        logger-level: full
        request-interceptors:
          - com.aikumao.interceptor.XTokenRequestInterceptor

2.2 方式2全局生效 : 在拦截器中添加 @Component 注解,OpenFeign 会在容器中找 requestInterceptors 拦截器。

获取请求头的值

5. Fcallback 兜底返回

如果远程调用时超时或错误,就可以返回默认的数据进行兜底,不至于没有响应。但Fallback 需要整合 Sentinel (熔断)才能实现。

  1. 引入sentinel的依赖

    com.alibaba.cloud spring-cloud-starter-alibaba-sentinel
  2. 配置文件开启 sentinel

    feign:
    sentinel:
    enabled: true #开启 Feign 对 Sentinel 的支持

  3. 编写一个类 XXXOpenFeignClientFallback 实现远程调用的接口类 ProductOpenFeignClient ,重写接口方法返回兜底数据,并且需要将该类交给Spring管理,类上编写@Common注解。

    @Slf4j
    @Component
    public class ProductOpenFeignClientFallback implements ProductOpenFeignClient {
    @Override
    public Product getProductId(Long id) {
    log.info("兜底回调");
    return new Product(id, new BigDecimal(0), "未知商品-" + id, 0);
    }
    }

  4. 在远程调用的接口类 ProductOpenFeignClient 的 @FeignClient 注解上,通过 fallback 指定兜底的接口。

    @FeignClient(value = "service-product", fallback = ProductOpenFeignClientFallback.class, path = "/product") // feign 客户端
    public interface ProductOpenFeignClient {
    ............
    }

如果没有引入 sentinel 依赖,会报500错误码连接失败。

复制代码

三、Sentinel

服务和服务之间的稳定性很重要,Spring Cloud Alibaba Sentinel 以流量切入点,从流量控制、流量路由、熔断降级、系统自适应过载保护、热点流量防护等多个维度保护的稳定行。

1. 基础使用

下载sentinel管理的控制台(sentinel-dashboard-1.8.9.jar),下载后通过 javar -jar 包名启动,在浏览器访问 http://localhost:8080/ 可进入管理页面。默认账号密码是 sentinel

sentinel 国内下载地址 https://gitee.com/folio_ou/sentinel-dashboard

引入 sentinel 依赖

复制代码
<dependency>
           <groupId>com.alibaba.cloud</groupId>
           <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

在配置文件配置 sentinel 地址

复制代码
spring:
  cloud:
    sentinel:
      transport:
        dashboard: localhost:8080  # sentinel 管理页面地址
      eager: true  #默认是懒加载,这里设置为true,即项目启动时就连接 sentinel 管理页面

在管理页面的 "簇点链路" 下,会识别controller 层的 RequestMapping、GetMapping 、PostMapping、PutMapping、DeleteMapping注解上的路径,以及service实现类的各个方法上自定义@SentinelResource(value = "XXX") 上的路径。可以在每个资源上进行限流。

在每个资源上进行限流,然后在 流控规则 里就可以看到设置的策略规则了。访问接口,我这里设置了1秒只执行一个请求,超过就报错。

2. sentinel 异常处理

如果没有异常处理,sentinel会直接将错误显示在浏览器,但用户也看不懂这是啥,因此我们需要自定义异常处理,显示最直白信息让用户看。

web接口异常

web接口异常是在controller层的接口上添加了流控规则,请求量过大导致报错,可以定义一个结果集封装报错信息,以流的方式把报错信息显示在浏览器上。

  1. 定义一个结果集

    public class Result {
    private int code;
    private String msg;
    private Object data;
    public Result() {
    }
    public static Result ok() {
    Result r = new Result();
    r.setCode(200);
    return r;
    }
    public static Result ok(String msg, Object data) {
    Result r = new Result();
    r.setCode(200);
    r.setMsg(msg);
    r.setData(data);
    return r;
    }
    public static Result error() {
    Result r = new Result();
    r.setCode(500);
    return r;
    }
    public static Result error(Integer code, String msg) {
    Result r = new Result();
    r.setCode(code);
    r.setMsg(msg);
    return r;
    }
    // getter \ setter 省略
    }

  2. 创建一个类实现 BlockExceptionHandler 接口,并提供给Spring 管理。

    @Component
    public class MyBlockExceptionHandler implements BlockExceptionHandler {
    ObjectMapper objectMapper = new ObjectMapper();
    @Override
    public void handle(HttpServletRequest httpServletRequest,
    HttpServletResponse httpServletResponse, BlockException e) throws Exception {
    httpServletResponse.setContentType("application/json;charset=utf-8");
    PrintWriter writer = httpServletResponse.getWriter();
    Result result = Result.error(429, "请求过于频繁,请稍后再试");
    String json = objectMapper.writeValueAsString(result);
    writer.write(json);
    writer.flush();
    writer.close();
    }
    }

实现方法接口异常

如在 OrderServiceImpl 实现类上添加了 @SentinelResource 注解,然后在 sentinel 上对这个接口设置了流控规则,如果没有回调兜底,那就会将异常交给SpringBoot的默认报错处理显示。

下图是springboot的默认报错页面

在原有的 @SentinelResource(value = "createOrder")注解上添加 blockHandler 属性,值为兜底方法名,然后在兜底方法的参数最后添加 BlockException ex 参数,这样才能被 sentinel识别。blockHandler 的作用是:当方法因触发了 Sentinel 配置的流控、熔断降级等规则 而被拦截时,指定一个兜底方法来处理这种"规则拦截"情况 ,而不是让 BlockException 直接抛给调用方。

1. 专门处理 Sentinel 的 BlockException

它只负责处理 Sentinel 规则触发的异常(如限流 FlowException、熔断 DegradeException 等)。如果代码本身抛出了业务异常(如空指针),blockHandler 不会 介入,那是 fallback 的职责。

2. 方法签名有严格要求

指定的 createOrderHandler 方法必须满足:

  • 访问权限 :public。

  • 返回值 :与原方法 createOrder 一致。

  • 参数 :与原方法参数列表一致,最后额外加一个 BlockException 参数。

  • 位置 :默认需与原方法在同一个类中。如果放在其他类,需额外指定 blockHandlerClass,且该方法必须是 static。

3. 与 fallback 的优先级

如果你同时配置了 blockHandler 和 fallback,当因 Sentinel 规则(限流/熔断)被拦截时,只会进入 blockHandler ,不会进入 fallback。

实现逻辑如下:

sentinel管理页面添加流控规则

远程调用异常

远程调用接口

复制代码
@FeignClient(value = "service-product", fallback = ProductOpenFeignClientFallback.class, path = "/product")  // feign 客户端
public interface ProductOpenFeignClient {
@GetMapping("/getProductById/{id}")
// mvc注解两套逻辑
// 1. 标注在controller上,是接受请求
// 2. 标注在feignClient上,是发送请求
Product getProductId(@PathVariable("id") Long id);
}

兜底实现类

复制代码
@Slf4j
@Component
public class ProductOpenFeignClientFallback implements ProductOpenFeignClient {
@Override
public Product getProductId(Long id) {
    log.info("兜底回调");
    return new Product(id, new BigDecimal(0), "未知商品-" + id, 0);
}
}

3. sentinel 流控规则

QPS:每秒请求数(Queries Per Second),表示单位时间内允许通过的请求数量。例如设置 QPS 为 10,表示每秒最多放行 10 个请求,超过的请求会被拦截。

并发线程数:表示同时处理的请求线程数量。当并发线程数超过设定值时,多余的请求会被拦截,适用于控制资源占用、防止线程池被打满的场景。

均摊阈值:在集群模式下,将总体阈值平均分摊到每个节点上的值。例如集群总共有 4 个节点,总体阈值设为 100,则每个节点均摊阈值为 25。

单机阈值:针对单个节点设置的阈值,即该节点独立允许的最大流量。单机模式下,流控规则直接使用该值作为判断依据。例如总共有 4 个节点,阈值设为 100,则每个节点均摊阈值为 100


集群阈值模式:用于集群部署时,决定阈值如何在整个集群中生效。

单机均摊:将集群总体阈值平均分配到每个节点,每个节点按各自分配到的阈值进行限流。

总体阈值:整个集群所有节点共享一个总阈值,所有节点的流量合计不能超过该值。


流控模式:定义流控规则针对的资源范围。

直接:只对当前资源本身进行限流,不关心其他资源的调用关系。

关联:当关联资源达到阈值时,对当前资源进行限流。例如订单接口和支付接口存在关联,当支付接口流量过大时,限制订单接口的流量。

链路:针对从某个入口资源到当前资源的调用链路进行限流,只统计该链路下的流量。如普通购物接口和秒杀购物接口都会调用创建订单的接口,可以针对普通购物接口和秒杀购物接口限流,需要设置 spring.cloud.sentinel.web-context-unify = false(是否需要分割上下文,设置为false即不进行分割,也就是说可以针对秒杀购物接口限流)

入口资源:链路的起点资源,用于区分不同的调用来源,实现更精细的链路限流。


流控效果:当请求超过阈值时,采取的处理策略。

快速失败:请求超过阈值后立即拒绝,直接抛出异常或返回错误信息,不做任何等待。

Warm Up:预热模式,系统启动初期阈值较低,随着时间推移逐步提升到设定阈值,避免冷启动时流量突然涌入导致系统崩溃。如预时长为3秒,单机阈值为10个,则系统启动初期的3秒内,每秒接收 3 / 10 个请求(约3个请求),到达预热时间后,每秒处理10个请求。

排队等待:请求超过阈值后不直接拒绝,而是进入队列排队,按固定速率依次放行,适用于需要平滑处理突发流量的场景。例如设置QPS单机阈值为2,表示每秒只接收2个请求,设置超时时间为1000ms, 表示排队1秒后还没有被接收,请求被丢弃

4. 熔断降级

熔断降级是 Sentinel 提供的另一项核心能力,用于保护服务在依赖的下游服务出现异常、超时或不可用时,能够快速失败并返回兜底结果,避免故障在调用链路上持续扩散,从而保障整个系统的稳定性。

与流控规则不同,流控关注的是流量大小 ,而熔断降级关注的是调用结果的质量。当某个资源的错误率、慢调用比例或异常数达到设定的阈值时,Sentinel 会触发熔断,让后续请求直接走降级逻辑,不再真正调用下游服务。可以理解为服务A调用服务B时,服务B经常超时,响应慢,服务A需要调用服务B时,就不再调用服务B, 而是自己返回兜底数据,进而保证服务的稳定性。

1. 熔断降级的核心概念

熔断:当被调用的资源持续出现异常、超时或错误率达到阈值时,Sentinel 会打开熔断器,在熔断时间内直接拒绝对该资源的调用,快速返回降级结果。

降级:熔断触发后,为请求提供一个兜底的处理逻辑(如返回默认数据、缓存数据或提示信息),保证调用方依然能拿到响应,而不是一直等待或报错。

恢复:熔断状态并非永久,经过设定的熔断时间后,Sentinel 会允许少量请求通过探测,若探测成功则逐步恢复对该资源的正常调用。

2. 熔断降级规则配置

在 Sentinel 管理控制台的「降级规则」菜单中,可以针对某个资源添加熔断降级规则。主要配置项如下:

资源名:需要熔断降级的资源,通常是接口路径或 @SentinelResource 注解指定的资源名。

熔断策略:支持慢调用比例、异常比例、异常数三种策略。

阈值:触发熔断的临界值,不同策略含义不同。

熔断时长:熔断器打开后持续的时间,单位为秒,熔断结束后进入半开状态探测恢复。

3. 三种熔断策略

慢调用比例:当请求的响应时间超过设定的最大 RT(响应时间)时,视为慢调用。若在统计窗口内慢调用比例达到阈值(如 50%),则触发熔断。适用于下游服务响应变慢的场景。

异常比例:当统计窗口内请求的异常比例达到阈值(如 50%)时,触发熔断。适用于下游服务频繁报错的场景。

异常数:当统计窗口内请求的异常数量达到阈值(如 5 次)时,触发熔断。适用于异常数量少但影响明显的场景。

4. 代码示例:使用 @SentinelResource 实现熔断降级

在业务方法上添加 @SentinelResource 注解,并通过 fallback 指定降级方法,当熔断触发时自动返回兜底数据。

java 复制代码
@Slf4j
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private ProductOpenFeignClient productOpenFeignClient;
/**
* blockHandler 处理 BlockException(熔断/限流触发)
* fallback 处理 Throwable(Feign 异常触发)
* */
@SentinelResource(value = "createOrderServer",
fallback = "createOrderFallback",
blockHandler = "createOrderBlockHandler")
@Override
public Order createOrder(Long productId, Long userId) {
//        Product product = getProductFromRemote(productId);  // 调用私有方法获取商品信息
//        Product product = getProductFromRemoteWithLoadBalancer1(productId);
// 使用 OpenFeign 发送服务
Product product = productOpenFeignClient.getProductId(productId);
Order order = new Order();
order.setId(1L);
order.setTotalAmount(new BigDecimal(product.getPrice().doubleValue() * product.getNum()));  // 总金额
order.setUserId(userId);
order.setAddress("浙江省");
order.setProductList(Arrays.asList(product));
return order;
}
/**
 * 熔断/限流触发的兜底方法
 * 要求:
 * 1. 必须是 public
 * 2. 返回值与原方法一致 (Order)
 * 3. 参数列表与原方法一致,且最后加上 BlockException 参数
 * 4. 默认必须与原方法在同一个类中
 */
public Order createOrderBlockHandler(Long productId, Long userId, BlockException ex) {
    log.error("触发熔断/限流,进入兜底方法,异常信息:{}", ex.getClass().getSimpleName());
    Order fallbackOrder = new Order();
    fallbackOrder.setId(-1L);
    fallbackOrder.setTotalAmount(BigDecimal.ZERO);
    fallbackOrder.setAddress("Sentinel 熔断兜底地址");
    return fallbackOrder;
}
}

5. 与 OpenFeign Fallback 的配合

在远程调用场景中,OpenFeign 的 Fallback 需要整合 Sentinel 才能生效。当远程调用超时或异常时,Sentinel 会触发熔断,OpenFeign 的 Fallback 类会返回兜底数据,避免调用方长时间等待或无响应。

远程调用接口代码如下

java 复制代码
@FeignClient(value = "service-product", fallback = ProductOpenFeignClientFallback.class, path = "/product")
public interface ProductOpenFeignClient {
    @GetMapping("/getProductById/{id}")
    Product getProductId(@PathVariable("id") Long id);
}

兜底数据如下:

复制代码
@Slf4j
@Component
public class ProductOpenFeignClientFallback implements ProductOpenFeignClient {
@Override
public Product getProductId(Long id) {
    log.info("兜底回调");
    return new Product(id, new BigDecimal(0), "未知商品-" + id, 0);
}
}
  • @SentinelResource 的 blockHandler :

    它只拦截 Sentinel 规则(限流、熔断、系统保护)抛出的 BlockException 。它的本质是:Sentinel 发现 createOrderServer 这个资源的流量太大了,或者慢调用比例超过了阈值,于是主动拦截 了请求,不让它进去执行,并转交给你写的 createOrderBlockHandler 方法。

  • Feign 的 fallback :

    它是 OpenFeign 内部用来处理远程调用真实的业务异常 的。比如:商品服务返回了 500 错误、网络超时(SocketTimeoutException)、服务宕机连不上(ConnectException)。这些异常发生在 Feign 发起 HTTP 请求的过程中。此时,Sentinel 的 blockHandler 是捕获不到这些异常的。

通过以上配置,当商品服务出现故障或响应过慢时,订单服务能够快速降级返回默认商品信息,保证核心业务流程不被阻塞。

5. 热点规则

热点规则(热点参数限流)是 Sentinel 针对某个资源的特定参数值 进行限流的规则。它不关心整体 QPS 有多高,而是关心「同一个参数值被访问了多少次」。最典型的场景就是秒杀:同一个商品被大量用户同时抢购,而其他商品几乎没人访问,此时用普通流控会把整个接口限死,用热点规则则可以只限制这个热门商品。

1. 热点规则与普通流控的区别

普通流控:以资源(接口)为单位统计流量,只要接口总 QPS 超过阈值就限流,无法区分是哪个商品、哪个用户。

热点规则:以资源 + 参数为单位统计流量,可以精确到「商品 ID = 1001 的请求每秒超过 5 次就限流」,其他商品不受影响。

举个秒杀的例子:商品 1001 是爆款,1 秒内涌入 1000 个请求;商品 1002 是冷门款,1 秒只有 2 个请求。如果给整个秒杀接口设置 QPS = 10,那么商品 1002 的正常请求也会被误伤。而热点规则只针对参数值为 1001 的请求限流,商品 1002 依然可以正常下单。

2. 代码准备:给秒杀接口标注 @SentinelResource

热点规则必须配合 @SentinelResource 注解使用,并且要在注解中通过 blockHandler 指定被限流后的兜底方法。下面是一个秒杀下单接口:

java 复制代码
@Slf4j
@Service
public class SeckillServiceImpl implements SeckillService {
/**
 * 秒杀下单接口
 * value:资源名,热点规则就是针对这个资源配置的
 * blockHandler:被热点规则限流后执行的兜底方法
 */
@SentinelResource(value = "seckillOrder", blockHandler = "seckillOrderBlockHandler")
@Override
public String seckillOrder(Long productId, Long userId) {
    log.info("用户 {} 抢购商品 {}", userId, productId);
    // 模拟扣减库存、创建订单等业务逻辑
    return "抢购成功,商品ID=" + productId + ",用户ID=" + userId;
}
/**
热点限流兜底方法
要求:public、返回值与原方法一致、参数列表与原方法一致,最后额外加 BlockException
*/
public String seckillOrderBlockHandler(Long productId, Long userId, BlockException ex) {
log.warn("商品 {} 触发热点限流,异常类型:{}", productId, ex.getClass().getSimpleName());
return "当前商品太火爆了,请稍后再试!商品ID=" + productId;
}
}

对应的 Controller 层:

java 复制代码
@RestController
@RequestMapping("/seckill")
public class SeckillController {
@Autowired
private SeckillService seckillService;
@GetMapping("/order")
public String order(@RequestParam("productId") Long productId,
@RequestParam("userId") Long userId) {
return seckillService.seckillOrder(productId, userId);
}
}

启动项目后,在 Sentinel 控制台的「簇点链路」中就能看到 seckillOrder 这个资源,点击右侧的「热点」按钮即可添加热点规则。

3. 热点规则的核心配置项

资源名 :需要限流的资源,即 @SentinelResource(value = "seckillOrder") 中的 value。

参数索引 :指定对第几个参数做限流,从 0 开始计数。例如方法签名是 seckillOrder(Long productId, Long userId),参数索引 0 表示 productId,参数索引 1 表示 userId。

单机阈值:该参数值在单位时间(1 秒)内允许通过的最大请求数。

统计窗口时长:统计的时间窗口,单位为秒,默认为 1 秒。

参数类型:参数的数据类型,支持 int、long、String 等,需要与方法参数类型保持一致。

参数值:可选,用于配置「参数例外项」,即对某个具体参数值单独设置阈值。

4. 秒杀场景实战:只限制爆款商品

第一步:配置基础热点规则

在「热点规则」页面新增规则,资源名填 seckillOrder,参数索引填 0(即 productId),单机阈值填 5,统计窗口时长填 1 秒。含义是:同一个商品 ID 每秒最多放行 5 个请求,超过的请求走兜底方法。

第二步:验证效果

快速刷新访问 http://127.0.0.1:6001/seckill/order?productId=1001&userId=1,当 1 秒内请求超过 5 次时,页面会返回兜底信息「当前商品太火爆了,请稍后再试!」。而访问 productId=1002 时,由于它是另一个参数值,拥有独立的计数,依然可以正常下单。

这正是热点规则在秒杀场景中的价值:热点商品被保护,冷门商品不受影响。

5. 参数例外项:给爆款商品单独放宽或收紧阈值

实际业务中,不同商品的秒杀库存和承载能力不同。例如商品 1001 库存有 1000 件,可以放宽到每秒 100 个请求;而商品 1002 库存只有 10 件,需要收紧到每秒 2 个请求。这时就可以使用「参数例外项」。

在热点规则编辑页面勾选「参数例外项」,然后添加:

参数类型:long

参数值 :1001,限流阈值:100

参数值 :1002,限流阈值:2

配置后,商品 1001 每秒可放行 100 个请求,商品 1002 每秒只放行 2 个请求,其他商品则使用基础阈值 5。这样就能针对不同商品做精细化的流量控制。

6. 热点规则 + 熔断降级:完整的秒杀防护链路

热点规则解决的是「单个商品流量过大」的问题,但秒杀场景往往还伴随着下游服务响应变慢、数据库压力过大等问题。因此通常需要把热点规则和熔断降级组合使用:

flowchart TD A[用户发起秒杀请求] --> B{热点规则判断} B -- 超过商品阈值 --> C[执行 blockHandler 兜底返回] B -- 未超过阈值 --> D[调用商品服务查询库存] D --> E{熔断降级判断} E -- 慢调用比例超阈值 --> F[熔断打开, 走 fallback 兜底] E -- 正常 --> G[扣减库存并创建订单] G --> H[返回抢购成功]

对应的代码可以同时指定 blockHandler 和 fallback:

java 复制代码
@SentinelResource(value = "seckillOrder",
        blockHandler = "seckillOrderBlockHandler",
        fallback = "seckillOrderFallback")
@Override
public String seckillOrder(Long productId, Long userId) {
    // 远程调用商品服务校验库存
    Product product = productOpenFeignClient.getProductId(productId);
    if (product.getNum() <= 0) {
        throw new RuntimeException("库存不足");
    }
    return "抢购成功,商品ID=" + productId;
}
// 热点/流控/熔断规则拦截时执行
public String seckillOrderBlockHandler(Long productId, Long userId, BlockException ex) {
return "当前商品太火爆了,请稍后再试!";
}
// 业务异常(如远程调用失败、库存不足)时执行
public String seckillOrderFallback(Long productId, Long userId, Throwable t) {
return "抢购失败,请稍后重试:" + t.getMessage();
}

blockHandler :只处理 Sentinel 规则(热点、流控、熔断)抛出的 BlockException。

fallback:处理业务代码本身抛出的异常,如远程调用超时、空指针、库存不足等。

两者同时配置时,被规则拦截走 blockHandler,业务异常走 fallback,互不干扰。

7. 使用热点规则的注意事项

  • 必须配合 @SentinelResource 使用:热点规则依赖注解中的资源名和参数信息,普通 Controller 接口无法直接配置参数索引。
  • 参数索引从 0 开始:配置时容易数错,建议对照方法签名逐个确认。
  • 参数类型要匹配:如果方法参数是 Long,规则中的参数类型也要选 long,否则例外项不生效。
  • 兜底方法签名要严格 :public、返回值一致、参数列表一致,最后额外加 BlockException 参数,否则 Sentinel 找不到兜底方法会直接抛异常。
  • 热点规则只做入口限流:真正的库存扣减还需要配合 Redis 预扣、数据库乐观锁等手段,Sentinel 只负责挡住超额流量。

至此,Sentinel 的热点规则就介绍完了。在秒杀这类「单点热点」场景中,热点规则比普通流控更精准,能够在不影响其他商品的前提下保护系统稳定。

四、Gateway 网关

前面的章节里,服务之间的调用已经能跑通了:Nacos 负责服务注册与发现,OpenFeign 负责声明式远程调用,Sentinel 负责流控与熔断降级。但这些都是服务与服务之间的治理。当请求来自浏览器、App、小程序等外部客户端时,如果让客户端直接记住每个微服务的地址和端口,会带来几个明显的问题:服务地址暴露在外、跨域处理分散在各个服务里、鉴权逻辑要在每个服务重复写一遍、服务拆分或扩容后客户端还得跟着改。

Gateway 就是用来解决这类问题的:它是整个微服务集群的统一入口,所有外部请求先打到网关,由网关完成路由转发、鉴权、限流、跨域、日志等横切逻辑,再转发给后端的各个微服务。

1. Gateway 是什么

Spring Cloud Gateway 是 Spring Cloud 官方推出的第二代网关组件,用来替代已经停止维护的 Zuul 1.x。它基于 Spring WebFlux 和 Reactor Netty 构建,采用响应式非阻塞模型,在高并发场景下比传统的同步阻塞网关更省线程资源。

核心定位:微服务架构的统一入口,负责把外部请求路由到内部服务,并在转发过程中完成各种横切处理。

技术基础:Spring WebFlux + Reactor Netty,非阻塞 IO。

与 Zuul 的区别:Zuul 1.x 基于 Servlet 同步阻塞模型,Gateway 基于响应式模型,性能更好,且是 Spring Cloud 官方主推方案。

需要特别注意的是:Gateway 本身也是一个 Spring Boot 应用,需要注册到 Nacos 中,但它不应该引入 spring-boot-starter-web 依赖。因为 Gateway 基于 WebFlux,而 spring-boot-starter-web 会引入 Spring MVC,两者同时存在会导致 Gateway 启动失败。

2. Gateway 的核心概念

Gateway 的整个工作模型由三个核心概念组成,理解这三个概念基本就理解了 Gateway 的工作原理。

Route(路由):网关的基本组成单元,由 id、目标 URI、断言集合、过滤器集合组成。一个路由表示「满足某些条件的请求,转发到某个地址」。

Predicate(断言):路由的匹配条件,决定什么样的请求会命中这条路由。常用的有 Path、Method、Header、Query、Host、Cookie、时间等断言。

Filter(过滤器):在请求转发前后对请求和响应做加工处理,如添加请求头、修改路径、限流、记录日志、鉴权等。

三者的关系可以简单理解为:请求进来后,先由 Predicate 判断是否命中某条 Route,命中后经过一系列 Filter 处理,最后转发到 Route 指定的 URI。

flowchart LR A[客户端请求] --> B{Gateway 接收请求} B --> C[Predicate 断言匹配] C -- 匹配失败 --> D[返回 404] C -- 匹配成功 --> E[执行前置 Filter] E --> F[转发到目标微服务] F --> G[执行后置 Filter] G --> H[返回响应给客户端]

3. 搭建 Gateway 模块

在之前的多模块工程中,新建一个 gateway 子模块,用于承载网关服务。

第一步:引入依赖

网关模块需要引入 Gateway 依赖和 Nacos 服务发现依赖,注意不要引入 web 依赖。

xml 复制代码
<dependencies>
    <!-- 网关核心依赖 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
    <!-- Nacos 服务发现,用于按服务名路由 -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <!-- 负载均衡,lb:// 协议需要 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-loadbalancer</artifactId>
    </dependency>
</dependencies>

第二步:编写启动类

java 复制代码
package com.aikumao;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@EnableDiscoveryClient
@SpringBootApplication
public class GatewayMainApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayMainApplication.class, args);
}
}

第三步:编写配置文件

网关需要注册到 Nacos,同时配置路由规则。下面把订单服务和商品服务都通过网关暴露出去。

application.yml

复制代码
server:
  port: 80  #程序端口号
spring:
profiles:
include: route  # 引入route配置文件
application:
name: service-gateway  # 注册中心显示程序名称
cloud:
nacos:
server-addr: 127.0.0.1:8848 #nacos服务地址

application-route.yml

yaml 复制代码
spring:
  cloud:
    gateway:
      routes:
        - id: order-route   # 路由名字
          uri: lb://service-order  # 被访问的地址,请求转到此处, lb表示负载均衡, service-order表示服务名
          predicates:  # 断言
            - Path=/api/order/**  # 表示匹配路径, /api/order/ 开头的请求都会被转到service-order服务
    - id: product-route   # 路由名字
      uri: lb://service-product  # 被访问的地址,请求转到此处, lb表示负载均衡, service-product 表示服务名
      predicates:  # 断言
        - Path=/api/product/**  # 表示匹配路径, /api/product/ 开头的请求都会被转到service-product服务

启动网关后,访问 http://127.0.0.1:80/api/order/createOrder?productId=1&userId=300,请求会先到网关,网关根据 Path=/api/order/** 命中 order-route,再通过 lb://service-order 从 Nacos 拿到订单服务的实例列表,负载均衡选一个实例转发过去。

uri 的两种写法:

http://127.0.0.1:6001:直接写死地址,不经过注册中心,服务扩容或迁移时需要改配置。

lb://service-order:按服务名从注册中心获取实例并负载均衡,推荐使用。

4. 常用断言 Predicate

断言决定请求是否命中路由,Gateway 内置了多种断言工厂,配置时只需要写断言名加参数即可。

断言 作用 配置示例
Path 按请求路径匹配 - Path=/api/order/**
Method 按请求方法匹配 - Method=GET,POST
Header 按请求头匹配 - Header=X-Token, \d+
Query 按请求参数匹配 - Query=userId
Host 按域名匹配 - Host=**.example.com
After / Before / Between 按时间匹配 - After=2026-01-01T00:00:00+08:00[Asia/Shanghai]

多个断言之间是与的关系,必须全部满足才会命中路由。例如同时限制路径和方法:

yaml 复制代码
spring:
  cloud:
    gateway:
      routes:
        - id: order-route
          uri: lb://service-order
          predicates:
            - Path=/api/order/**
            - Method=GET,POST

5. 过滤器 Filter

过滤器用于在请求转发前后做加工处理。Gateway 的过滤器分为两类:

GatewayFilter(局部过滤器):只作用于某一条路由,配置在具体路由下面。

GlobalFilter(全局过滤器) :作用于所有路由,通常通过实现 GlobalFilter 接口自定义,适合做鉴权、统一日志等。

常用内置过滤器示例

下面这条路由在转发前给请求加一个请求头,并去掉路径前缀。

yaml 复制代码
spring:
  cloud:
    gateway:
      routes:
        - id: product-route
          uri: lb://service-product
          predicates:
            - Path=/api/product/**
          filters:
            - StripPrefix=1                       # 去掉第一层路径 /api
            - AddRequestHeader=X-Gateway, true    # 添加请求头

配置后,请求 /api/product/getProductById/1 转发到商品服务时会变成 /product/getProductById/1,同时带上 X-Gateway: true 请求头。

自定义全局过滤器

实际项目中最常见的需求是统一鉴权:请求必须携带 token 才允许放行。下面实现一个简单的全局鉴权过滤器。

java 复制代码
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {

    private static final List<String> WHITE_LIST = List.of("/api/auth/login", "/api/auth/register");

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String path = exchange.getRequest().getURI().getPath();
        // 白名单直接放行
        for (String white : WHITE_LIST) {
            if (path.startsWith(white)) {
                return chain.filter(exchange);
            }
        }

        String token = exchange.getRequest().getHeaders().getFirst("X-Token");
        if (token == null || token.isEmpty()) {
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);
            String body = "{\"code\":401,\"msg\":\"token missing\"}";
            DataBuffer buffer = exchange.getResponse().bufferFactory()
                    .wrap(body.getBytes(StandardCharsets.UTF_8));
            return exchange.getResponse().writeWith(Mono.just(buffer));
        }
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return -100;
    }
}

注意 :Gateway 基于 WebFlux,过滤器方法返回的是 Mono<Void>,不能像 Spring MVC 那样写同步阻塞代码。如果需要在过滤器中调用远程服务,应使用 WebClient 而不是 RestTemplate。

6. 跨域处理

前后端分离项目中,浏览器直接访问网关会涉及跨域。Gateway 提供了统一的跨域配置,配置一次即可对所有路由生效,不需要在每个微服务里单独处理。

yaml 复制代码
spring:
  cloud:
    gateway:
      globalcors:
        cors-configurations:
          '[/**]':
            allowed-origin-patterns: "*"   # 允许的源,生产环境建议写具体域名
            allowed-methods: "*"           # 允许的方法
            allowed-headers: "*"           # 允许的请求头
            allow-credentials: true        # 是否允许携带 cookie
            max-age: 3600                  # 预检请求缓存时间,单位秒

7. 与 Sentinel 整合限流

网关是所有流量的入口,在网关层做限流可以挡住绝大部分无效流量,避免压力传导到后端服务。Gateway 可以整合 Sentinel 实现网关限流。

第一步:引入依赖

xml 复制代码
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

第二步:配置 Sentinel 控制台地址

yaml 复制代码
spring:
  cloud:
    sentinel:
      transport:
        dashboard: localhost:8080
      eager: true

启动网关后,在 Sentinel 控制台的「簇点链路」中可以看到网关的路由资源,在「流控规则」中针对路由 id 配置阈值即可。当请求超过阈值时,网关会直接返回限流提示,不再转发到后端服务。

8. 使用 Gateway 的注意事项

  • 不要引入 spring-boot-starter-web:Gateway 基于 WebFlux,与 Spring MVC 冲突,会导致启动报错。
  • 路由 uri 优先使用 lb://:写死 IP 会失去注册中心和负载均衡的意义,服务扩容后还要改配置。
  • 过滤器里不要写阻塞代码:WebFlux 的线程模型下,阻塞调用会拖垮整个网关,远程调用请使用 WebClient。
  • 鉴权放在网关统一做:token 校验、签名验证等横切逻辑集中在网关,后端服务只关注业务。
  • 网关本身要做高可用:网关是单点入口,生产环境至少部署两个实例,前面再挂 Nginx 做负载均衡。
  • 注意路径前缀的处理:前后端约定好统一前缀(如 /api),配合 StripPrefix 过滤器去掉前缀后再转发,避免后端服务路径混乱。

五、Seata

前面的章节解决了服务注册发现、远程调用、流量控制和统一入口的问题,但还有一个绕不开的难题:分布式事务。一个业务操作往往要跨多个微服务、多个数据库完成,比如下单要同时扣减库存、扣减账户余额、创建订单,任何一步失败都可能导致数据不一致。Seata 就是 Spring Cloud Alibaba 体系中用来解决分布式事务的组件。

1. Seata 是什么

Seata(Simple Extensible Autonomous Transaction Architecture)是阿里开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。Seata 通过对本地事务的封装,把多个微服务的本地事务组合成一个全局事务,保证要么全部成功,要么全部回滚。

核心定位:为微服务架构提供分布式事务一致性保障,让跨服务、跨数据库的业务操作具备类似本地事务的原子性。

技术基础:基于两阶段提交(2PC)思想做了优化,通过 undo_log 记录数据快照实现自动回滚,对业务代码侵入较小。

与本地事务的区别:本地事务由数据库的 ACID 保证,分布式事务需要协调多个数据源,Seata 承担的就是这个协调者的角色。

2. Seata 的核心角色

Seata 的整个事务模型由三个核心角色组成,理解这三个角色基本就理解了 Seata 的工作原理。

TC(Transaction Coordinator,事务协调者):独立部署的 Seata Server,维护全局事务和分支事务的状态,负责驱动全局事务的提交或回滚。

TM(Transaction Manager,事务管理器) :定义全局事务的边界,负责开启全局事务、提交或回滚全局事务。通常就是业务发起方,通过 @GlobalTransactional 注解标注。

RM(Resource Manager,资源管理器):管理分支事务处理的资源,与 TC 通信注册分支事务、上报分支状态,并负责分支事务的提交和回滚。通常就是各个参与业务的微服务。

三者的关系可以简单理解为:TM 向 TC 申请开启一个全局事务,业务调用过程中各个 RM 向 TC 注册分支事务,全部成功后 TM 通知 TC 提交,任一失败则 TM 通知 TC 回滚所有分支。

flowchart LR A[TM 发起方] -->|1. 开启全局事务| B[TC 事务协调者] A -->|2. 调用业务| C[RM 服务A] A -->|2. 调用业务| D[RM 服务B] C -->|3. 注册分支事务| B D -->|3. 注册分支事务| B A -->|4. 提交或回滚全局事务| B B -->|5. 驱动分支提交或回滚| C B -->|5. 驱动分支提交或回滚| D

3. Seata 的四种事务模式

Seata 针对不同场景提供了多种事务模式,实际使用时按业务特点选择即可。

AT 模式(默认):对业务无侵入,基于关系型数据库,通过解析 SQL 生成前后镜像记录到 undo_log,回滚时用镜像数据恢复。适合大多数基于数据库的业务场景,是使用最广泛的模式。

TCC 模式:Try、Confirm、Cancel 三个阶段由业务自己实现,不依赖数据库回滚,性能好、隔离性强,但需要改造业务代码,适合对一致性要求高、需要控制资源的场景。

SAGA 模式:长事务解决方案,把业务拆成一系列本地事务,每个事务都有对应的补偿操作,失败时按相反顺序执行补偿。适合流程长、参与方多的业务。

XA 模式:基于数据库的 XA 协议实现,强一致性,但性能相对较低,适合对一致性要求极高的场景。

本节的实战以 AT 模式 为主,因为它对业务代码的侵入最小,也是入门 Seata 最常用的方式。

4. 搭建 Seata Server

Seata 需要先部署一个独立的 TC 服务端,所有微服务都向它注册和协调事务。

第一步:下载 Seata Server

从 Seata 官网或 GitHub Release 页面下载对应版本的 seata-server 压缩包,解压即可。注意 Seata 版本要与项目中引入的 seata 依赖版本保持一致,避免协议不兼容。

第二步:修改配置文件

进入 conf 目录,修改 application.yml,主要配置注册中心和存储模式。Seata Server 需要注册到 Nacos,让各个微服务能够发现它。

yaml 复制代码
server:
  port: 7091  # Seata 控制台端口
seata:
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
application: seata-server
group: DEFAULT_GROUP
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
store:
mode: db  # 存储模式,生产环境建议用 db,入门可用 file

第三步:启动 Seata Server

Windows 下双击 bin 目录的 seata-server.bat,Linux 下执行 sh seata-server.sh。启动成功后访问 http://localhost:7091 可以进入 Seata 控制台,默认账号密码是 seata。

5. 微服务整合 Seata

以订单服务调用商品服务扣减库存为例,演示 AT 模式的整合步骤。

第一步:引入依赖

在需要参与分布式事务的模块中引入 seata 依赖。

xml 复制代码
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>

第二步:配置事务分组

在 application.yml 中配置 Seata 的注册地址和事务分组,事务分组用于把微服务与 TC 集群对应起来。

yaml 复制代码
seata:
  enabled: true
  application-id: service-order
  tx-service-group: my_tx_group  # 事务分组名
  service:
    vgroup-mapping:
      my_tx_group: default  # 分组映射到 TC 集群名
  registry:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      application: seata-server
      group: DEFAULT_GROUP

第三步:在业务库中创建 undo_log 表

AT 模式依赖 undo_log 表记录数据快照,每个参与事务的数据库都要建这张表。

sql 复制代码
CREATE TABLE `undo_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(100) NOT NULL,
  `context` varchar(128) NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int(11) NOT NULL,
  `log_created` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

6. 代码示例:@GlobalTransactional 实现分布式事务

在业务发起方的方法上添加 @GlobalTransactional 注解,即可把该方法标记为全局事务的起点。

java 复制代码
@Slf4j
@Service
public class OrderServiceImpl implements OrderService {
    @Autowired
    private ProductOpenFeignClient productOpenFeignClient;
    @Autowired
    private OrderMapper orderMapper;
/**
 * 下单接口
 * @GlobalTransactional 开启全局事务,name 用于在 Seata 控制台标识事务
 */
@GlobalTransactional(name = "createOrder", rollbackFor = Exception.class)
@Override
public Order createOrder(Long productId, Long userId) {
    log.info("开始创建订单,productId={}, userId={}", productId, userId);
    // 1. 远程调用商品服务扣减库存(分支事务)
    productOpenFeignClient.deductStock(productId, 1);
    // 2. 本地创建订单(分支事务)
    Order order = new Order();
    order.setId(1L);
    order.setUserId(userId);
    order.setProductId(productId);
    orderMapper.insert(order);
    // 3. 模拟异常,验证全局回滚
    // int i = 1 / 0;
    return order;
}
}

商品服务的扣减库存方法就是普通业务方法,不需要额外加注解,Seata 会自动把它纳入全局事务的分支。

java 复制代码
@Service
public class ProductServiceImpl implements ProductService {
    @Autowired
    private ProductMapper productMapper;
@Override
public void deductStock(Long productId, Integer num) {
    // 普通本地事务,Seata 会自动代理并注册为分支事务
    productMapper.deductStock(productId, num);
}
}

验证方式 :正常调用时订单和库存都成功;把 int i = 1 / 0; 的注释放开,订单服务抛出异常后,商品服务已经扣减的库存会被 Seata 自动回滚,两个库的数据保持一致。

7. 使用 Seata 的注意事项

  • undo_log 表不能少:AT 模式依赖 undo_log 记录快照,缺少这张表会导致回滚失败。
  • 全局事务要尽量短:全局事务持有锁的时间越长,并发冲突越大,应避免在事务中做远程调用之外的耗时操作。
  • 注意主键和唯一索引:AT 模式回滚依赖主键定位数据,业务表必须有主键,且避免在事务中修改主键。
  • 版本要统一:Seata Server 与客户端依赖版本不一致容易出现协议不兼容,建议统一版本。
  • 不是所有场景都要用:能用本地事务、消息最终一致性解决的场景,优先选择更轻量的方案,Seata 适合强一致性要求较高的核心业务。

至此,Seata 的基本介绍和 AT 模式的实战就完成了。配合前面的 Nacos、OpenFeign、Sentinel 和 Gateway,一个微服务项目从服务治理到分布式事务的完整链路就基本打通了。

相关推荐
瑞雪兆丰年兮18 分钟前
JavaWeb-day13-SpringBootWeb事务管理&AOP
java·开发语言·springboot·javaweb·事务·web·aop
曲径海盗27 分钟前
Try-Except怎么用,3个追问解答在自动化下的应用
java·jvm·oracle
lsc_blog1 小时前
听到优化风声,先准备能由自己掌控的事
数据库·分布式·中间件·职场和发展·重构
Cosolar1 小时前
极简智库:快速搭建你的开源项目教程阅读平台,让 docs/ 目录被更多人看见
java·架构·github
墨痕诉清风2 小时前
SSRF漏洞实战记录+OSS桶命令执行+MCP漏洞
linux·服务器·数据库
nice_lcj5202 小时前
【从零理解 Spring AI · 16】Agent 与工作流:什么该写死,什么该让模型决定
java·人工智能·后端·spring ai·ai应用开发
htzyl2062 小时前
别急着上微前端:从 6 个场景倒推 Module Federation 2.0 与 qiankun 2.x 的选型
前端·微服务
北京云帆互联科技2 小时前
云帆培训考试系统更新说明:v9.3.0
java·学习
学长毕设3 小时前
基于SpringBoot的景点管理系统-附源码
java·spring boot·后端