# SpringCloud

# 从单体到集群再到分布式

早期阶段,单体架构是主流选择,所有功能模块打包在一个应用中,开发简单直接,但是随着业务增长,代码变得臃肿,难以扩展特定功能模块,技术栈单一,难以采用新技术。

为了应对单体架构的性能瓶颈和高可用需求,集群架构应运而生。

实现方式:

  1. 水平扩展:部署多个相同的单体应用实例
  2. 通过负载均衡器 (Nginx、F5 等) 分配请求
  3. 共享数据库或数据库主从复制

但是仍然有缺陷,比如应用本身仍然是单体,业务复杂时扩展不灵活。

此时分布式架构与微服务应运而生,分布式架构通过将系统拆分为多个服务来解决上述问题。

本次学习使用尚硅谷 b 站开放课堂:https://www.bilibili.com/video/BV1UJc2ezEFU

框架(组件)学习与本套课程高度重合,但并不是课程资料的再复写。

相关技术:

  1. Nacos(注册中心、配置中心)来自 Spring Cloud Alibaba
  2. OpenFegin(远程调用)来自 Spring Cloud 官方
  3. Sentinel(异常处理、流控规则、熔断规则)来自 Spring Cloud Alibaba
  4. Gateway(路由、断言、过滤)来自 Spring Cloud 官方
  5. Seata(分布式事务)来自 Spring Cloud Alibaba

# Nacos

Nacos-Logo-Core.png

# 注册中心

# 服务注册

首先进行依赖导入

<!--    nacos 配置中心、注册中心    -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

使用 docker 或者直接运行的方式启动 Nacos

在 Windows 平台直接运行下使用命令:startup.cmd -m standalone(standalone 为使用单机模式)

在不同的服务下编写配置,如订单服务和产品服务:

spring:
  cloud:
    nacos:
      server-addr: 127.0.0.1:8848
  application:
    name: service-order
server:
  port: 8080
spring:
  cloud:
    nacos:
      server-addr: 127.0.0.1:8848
  application:
    name: service-product
server:
  port: 9000

其中 nacos.server-addr 为 nacos 服务的地址为 127.0.0.1:8848(本地测试)

访问 http://localhost:8848/nacos,在服务管理 - 服务列表可以看到现在已经注册上的服务

服务名 分组名称 集群数目 实例数 健康实例数 触发保护阈值 操作
service-order DEFAULT_GROUP 1 2 2 false 详情 | 示例代码 | 订阅者 | 删除
service-product DEFAULT_GROUP 1 3 3 false 详情 | 示例代码 | 订阅者 | 删除

# 服务发现

由于使用了 Nacos,服务发现方法的调用存在两套标准,分别是 Spring Cloud 的 DiscoveryClient 和 Nacos 的 NacosServiceDiscovery

@Resource
DiscoveryClient discoveryClient;
@Resource
NacosServiceDiscovery nacosServiceDiscovery;

下面为测试代码:

/**
 * spring 标准 discovery 使用 DiscoveryClient
 */
@Test
public void testDiscoveryClient() {
    for (String service : discoveryClient.getServices()) {
        /*
            循环输出服务列表(服务名)
                service-order
                service-product
        */
        System.out.println(service);
        // 获取所有实例、输出 IP 与端口号
        List<ServiceInstance> instances = discoveryClient.getInstances(service);
        for (ServiceInstance instance : instances) {
            System.out.println(instance.getHost() + ":" + instance.getPort());
        }
    }
}
/**
 * nacos 标准 discovery 使用 NacosServiceDiscovery
 */
@Test
public void testNacosServiceDiscovery() throws NacosException {
    for (String service : nacosServiceDiscovery.getServices()) {
        // 输出服务列表
        System.out.println(service);
        // 获取所有实例、输出 IP 与端口号
        List<ServiceInstance> instances = nacosServiceDiscovery.getInstances(service);
        for (ServiceInstance instance : instances) {
            System.out.println(instance.getHost() + ":" + instance.getPort());
        }
    }
}

输出:

service-order
192.168.25.1:8080
192.168.25.1:8001
service-product
192.168.25.1:9000
192.168.25.1:9002
192.168.25.1:9001

如果并没有如此多的输出可能是只启动了两个后端服务,还需要多启动几个来模拟分布式。

其次,在实际应用中,这个一般会被进一步封装,其发现的过程是自动进行的。

# 初见,远程调用

现在有一个实例,我们需要一个下单功能,当用户下单后对其商品进行结算这里我们对一些数据做出模拟。

订单实体:

@Data
public class Order {
    private Long id;
    private BigDecimal totalAmount;
    private Long userId;
    private String nickName;
    private String address;
    private List<Object> product;
}

商品实体:

@Data
public class Product {
    private Long id;
    private BigDecimal price;
    private String productName;
    private int num;
}

其次就是相应和 Controller 与 Service 代码,其较为简单不在此处详细展开,不过我想说一下订单部分的 Service:

@Service
public class OrderServiceImpl implements OrderService {
    @Override
    public Order createOrder(Long productId, Long userId) {
        Order order = new Order();
        order.setId(1L);
        // TODO 需要计算
        order.setTotalAmount(new BigDecimal("0"));
        order.setUserId(userId);
        order.setNickName("Karry.Liu");
        order.setAddress("北极");
        // TODO 需要远程查询
        order.setProduct(null);
        return order;
    }
}

由于 Order 与 Product 分别位于两个服务之中,其详细的金额 Amount 与产品详情列表 Product List 我们目前似乎无法获取,那我们应该怎么办呢?这个我们暂时按下不表,我们现在需要解决一个更加棘手的问题。

现在我们有如下项目结构(简略版)

- cloud-demo(基座项目)
|
| - services(服务层)
| | 
| |  - service-order(订单服务)(包含订单实体bean、服务service和控制controller)
| | 
| |  - service-product(商品服务)(包含商品实体bean、服务service和控制controller)
订单服务无法使用商品bean,反之商品服务无法使用订单bean,因为其每个服务均为独立的项目。

当 Order 服务需要 Product 服务时,其在 Order 的代码内一定会存在与 Product 相关的关键字,特别地,由于两个服务之间项链紧密,在 Product 的代码内也许也会出现 Order 相关的关键字。可是两套服务分别维护着自己的 bean(实体对象 / 实体类),在不同的服务之间甚至没有办法使用对方的实体类。

解决方案也很简单,将商品与订单的 Bean 剥离出来,形成一个独立的项目,与 services 等价地位,并在 services 添加 model 依赖。

<!--    模型依赖    -->
<dependency>
    <groupId>com.KarryCode</groupId>
    <artifactId>model</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

现在的结构为:

- cloud-demo(基座项目)
|
| - model(模型层)(包含订单实体bean与商品实体bean)
|
| - services(服务层)
| | 
| |  - service-order(订单服务)(包含服务service和控制controller)
| | 
| |  - service-product(商品服务)(包含服务service和控制controller)

至此,服务之间的实体使用已经被打通。

其次编写远程访问请求模板类 RestTemplate,由于 RestTemplate 是线程安全的,我们可以这样写:

@Configuration
public class ProductServiceConfig {
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}
// 上拉使他成为一个 Bean

其次编写远程访问方法:

private Product getProductFromRemote(Long productId) {
    // 获取商品服务所在的所有机器 IP + 端口
    List<ServiceInstance> instances = discoveryClient.getInstances("service-product");
    // 获取第一个机器(简单版)
    ServiceInstance serviceInstance = instances.get(0);
    // 拼接请求地址 http://192.168.25.1:9000/product/100
    String url = "http://" + serviceInstance.getHost() + ":" + serviceInstance.getPort() + "/product/" + productId;
    log.info("远程请求: {}", url);
    // 发送请求(远程)
    return restTemplate.getForObject(url, Product.class);
}

补充 createOrder 方法

public Order createOrder(Long productId, Long userId) {
    // 此处提前远程查询
    Product productFromRemote = getProductFromRemote(productId);
    Order order = new Order();
    order.setId(1L);
    // 计算
    order.setTotalAmount(productFromRemote.getPrice().multiply(new BigDecimal(productFromRemote.getNum())));
    order.setUserId(userId);
    order.setNickName("Karry.Liu");
    order.setAddress("北极");
    // 远程查询的结果
    order.setProduct(List.of(productFromRemote));
    return order;
}

此时双端服务已经打通了,注意到日志:2025-06-24T22:21:11.646+08:00 INFO 3496 — [service-order] [nio-8080-exec-1] c.K.service.impl.OrderServiceImpl : 远程请求: http://192.168.25.1:9000/product/100

{
  "id": 1,
  "totalAmount": 198,
  "userId": 2,
  "nickName": "Karry.Liu",
  "address": "北极",
  "product": [
    {
      "id": 100,
      "price": 99,
      "productName": "IPhone-100",
      "num": 2
    }
  ]
}

此时我们还有两个问题,一是我们每次只取了第一个服务器,二是这样写太复杂,没有实现负载均衡。

针对此第一个问题将在下一小节中解决,第二个问题将在下一个组件中解决。

# 实现负载均衡 APIs

首先是使用复杂一点的方式,后面将会介绍使用注解的形式。

先引入负载均衡环境依赖。

<!--    负载均衡    -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
# 基于方法的负载均衡

将 getProductFromRemote 方法改造为 getProductFromRemoteLoadBalancing:

private Product getProductFromRemoteLoadBalancing(Long productId) {
    // 获取商品服务所在机器 (负载均衡)
    ServiceInstance chooseLoadBalancing = loadBalancerClient.choose("service-product");
    // 拼接请求地址 http://192.168.25.1:9000/product/100
    log.info("服务地址(uri): {}", chooseLoadBalancing.getUri());//http://192.168.25.1:9000
    String url = "http://" + chooseLoadBalancing.getHost() + ":" + chooseLoadBalancing.getPort() + "/product/" + productId;
    log.info("远程请求: {}", url);
    // 发送请求(远程)
    return restTemplate.getForObject(url, Product.class);
}

多次请求 http://localhost:8080/create 后观察日志:

服务地址(uri): http://192.168.25.1:9001
远程请求: http://192.168.25.1:9001/product/100
服务地址(uri): http://192.168.25.1:9002
远程请求: http://192.168.25.1:9002/product/100
服务地址(uri): http://192.168.25.1:9000
远程请求: http://192.168.25.1:9000/product/100

可以观察到其负载均衡的使用了不同的端口,下面将介绍注解的形式。

# 基于注解的负载均衡

还记得我们之前使用的 RestTemplate 嘛?

@Resource
private RestTemplate restTemplate;

我们可以观察到,无论哪种方法,最终都会是去使用 restTemplate.getForObject (…) 这个方法,如果这个方法自己就可以进行负载均衡呢?我们是不是可以少些一点代码?

改造 ProductServiceConfig 配置类:

@Configuration
public class ProductServiceConfig {
    @LoadBalanced// 使用负载均衡
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

将 getProductFromRemoteLoadBalancing 方法改造为 getProductFromRemoteLoadBalancingWithAnnotation:

private Product getProductFromRemoteLoadBalancingWithAnnotation(Long productId) {
    String url = "http://service-product/product/" + productId;
    // 发送请求(远程)
    return restTemplate.getForObject(url, Product.class);
}

注意到我们的 url 中出现了 service-product,在由于 restTemplate 被追加了 @LoadBalanced 注解,使得整个 restTemplate 自带有负载均衡的能力,url 传过去的时候 service-product 会被自动替换为 IP+HOST 的形式,替换的结果符合负载均衡。

# 配置中心

# 基本用法

引入依赖

<!--    配置中心    -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

书写导入配置:nacos:service-order.yml

spring:
  config:
    import: nacos:service-order.yml
  cloud:
    nacos:
      server-addr: 127.0.0.1:8848
  application:
    name: service-order
server:
  port: 8080

在 nacos 中设置名为 service-order.yml

order:
  timeout: 120s
  autoConfirm: 7d

在 controller 加入 @RefreshScope 自动刷新注解

@Value("${order.timeout}")
private String orderTimeout;
@Value("${order.auto-confirm}")
private String orderAutoConfirm;
// 获取配置信息
@GetMapping("/getConfig")
public String getConfig() {
    return "order.timeout:" + orderTimeout + " order.autoConfirm:" + orderAutoConfirm;
}

访问后得到回应:order.timeout:120s order.autoConfirm:7d

但是配置中心的依赖导入方法具有广播性,有可能出现其他服务的无法启动问题,因此可以在 yml 中加入:

spring:
  cloud:
    nacos:
      config:
        import-check:
          enabled: false

来禁用检查。

# 无感动态刷新

手动配置很麻烦,通常使用统一导入的方法来实现,创建 OrderProperties:

@Data
@Component
// 批量获取配置,无需使用 @RefreshScope 即可自动刷新
@ConfigurationProperties(prefix = "order")
public class OrderProperties {
    String timeout;
    String autoConfirm;
}

@ConfigurationProperties (prefix = “order”) 中 prefix = "order" 获取前缀为 order 的配置,有了 ConfigurationProperties 无需使用 @RefreshScope 即可自动刷新。

改造 OrderController:

@Autowired
private OrderProperties orderProperties;
// 获取配置信息
@GetMapping("/getConfig")
public String getConfig() {
    return "order.timeout:" + orderProperties.getTimeout() + " order.autoConfirm:" + orderProperties.getAutoConfirm();
}

# 本地配置与 Nacos 配置冲突时

当本地配置与 Nacos 配置冲突时,优先以 Nacos 中的配置中心为准。

即先导入优先,外部优先。

当:import: nacos:service-order.yml,nacos:common.yml 出现时,仍然是优先以第一次出现的 nacos:service-order.yml 为准。

# 数据隔离

当出现不同环境需要不同配置时,比如 dev 环境、test 环境和 prod 环境,分别需要不同的配置,我们该如何组织?

首先 Nacos 提供了:命名空间 - 组织 - 配置单元的模式,命名空间可以对应到 dev 环境、test 环境和 prod 环境等,组开源对应到不同的微服务比如商品微服务、用户微服务等,配置单元即为具体的详细配置。

首先在 Nacos 命名空间、组织与配置。

Nacos-Logo-Core.png

这里已经创建了 dev 环境、test 环境和 prod 环境,下面创建详细的组与配置

properties-detail.png

然后改造 yml 配置文件

spring:
  profiles:
    active: dev
  cloud:
    nacos:
      server-addr: 127.0.0.1:8848
      config:
        namespace: ${spring.profiles.active:dev}
  application:
    name: service-order
server:
  port: 8080
---
spring:
  config:
    activate:
      on-profile: dev
    import:
      - nacos:common.yml?group=order
      - nacos:database.yml?group=order
---
spring:
  config:
    activate:
      on-profile: test
    import:
      - nacos:common.yml?group=order
      - nacos:database.yml?group=order
---
spring:
  config:
    activate:
      on-profile: prod
    import:
      - nacos:common.yml?group=order
      - nacos:database.yml?group=order
properties-smt.png

具体地:

namespace: ${spring.profiles.active:dev}

这里主要负责的时从项目到 Nacos 时我们应该选择哪套命名空间,是 Nacos 的命名空间

而对于:

profiles:
  active: dev

主要负责的是要激活哪套分片配置,是 on-profile: dev 还是 on-profile: test,还是 on-profile: prod,这里指的是项目的配置分片

此时只需要切换不同的 active: dev,即可完成不同环境的配置切换

# Nacos 总结

nacos-summary.png

来自尚硅谷课堂:https://www.bilibili.com/video/BV1UJc2ezEFU

# OpenFeign

OpenFeign 是 Spring Cloud 生态系统中的一个重要组件。Spring Cloud 是一个基于 Spring Boot 实现的分布式系统开发工具,它提供了一系列的工具来简化分布式系统开发,包括服务注册与发现、配置中心、断路器等功能,OpenFeign 默认集成了 Ribbon 负载均衡器。

# 远程调用

# 导入注解

<!--    openfeign 远程调用    -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

# 创建 feign 包与 XXXClient 接口

@FeignClient(value = "service-product")
public interface ProductFeignClient {
    //mvc 注解使用的两套逻辑,放在 Controller 上是接收请求,放在 FeignClient 上是发送请求
    @GetMapping("/product/{productId}")
    Product getProductById(@PathVariable String productId);
}

这里的 value = "service-product" 指的是给那个微服务发送请求,这里还标注了 GetMapping,指的是给那个微服务发送请求的路径是什么,其明确了接口。

# 改造订单创建方法

@Override
    public Order createOrder(Long productId, Long userId) {
//        Product productFromRemote = getProductFromRemoteLoadBalancingWithAnnotation(productId);
        Product productFromRemote = productFeignClient.getProductById(productId);
        Order order = new Order();
        order.setId(1L);
        // TODO 需要计算
        order.setTotalAmount(productFromRemote.getPrice().multiply(new BigDecimal(productFromRemote.getNum())));
        order.setUserId(userId);
        order.setNickName("Karry.Liu");
        order.setAddress("北极");
        // TODO 需要远程查询
        order.setProduct(List.of(productFromRemote));
        return order;
    }

# 也可以向外部(第三方)做出请求

比如下面是一个针对天气数据接口的代码实现

@FeignClient(value = "weather-client", url = "http://apis.juhe.cn")
public interface WeatherFeignClient {
    @GetMapping("/simpleWeather/query")
    String getWeatherByCityId(@RequestParam("city") String city,
                              @RequestParam("key") String key);
    default String getEncodedCity(String city) throws Exception {
        return URLEncoder.encode(city, StandardCharsets.UTF_8);
    }
}

# 小技巧(懒狗模式)

如果是要访问自己的设计的接口,着通常是业务接口,比如一个订单服务要访问商品服务,最简单的方式就是将,商品的 Controller 下的接口找到,然后直接复制方法名(以接口的方式进行复制就可以),然后把他放在地点服务的 OpenFegin 接口下就可以了,值得注意的是,OpenFegin 已经帮我们做好了负载均衡,相比上述 Nacos 中那种请求模板方便多了。

具体地如下面的操作所示:

  1. 找到商品服务接口的代码

    @Slf4j
    @RestController
    public class ProductController {
        @Autowired
        private ProductService productService;
        @GetMapping("/product/{productId}")
        public Product getProduct(@PathVariable Long productId) {
            log.info("查询商品信息: {}", productId);
            return productService.getProductById(productId);
        }
    }
  2. 就像接口的方式去复制代码

    @GetMapping("/product/{productId}")
    public Product getProduct(@PathVariable Long productId)
  3. 粘贴到订单服务之中

    @FeignClient(value = "service-product")
    public interface ProductFeignClient {
        //mvc 注解使用的两套逻辑,放在 Controller 上是接收请求,放在 FeignClient 上是发送请求
        @GetMapping("/product/{productId}")
        Product getProductById(@PathVariable Long productId);
        
        @GetMapping("/product/{productId}")
        public Product getProduct(@PathVariable Long productId);
    }

    可以观察到,粘贴到 ProductFeignClient 中的代码与我们之前自己定义的代码片段基本是完全一致,所以这是一个非常好用的小技巧。

# 面试题

问:客户端的负载均衡和服务端负载均衡有不用?

答:首先对于客户端的负载均衡来说,客户端服务会先去访问注册中心,首先获取到一些地址,然后选择一个地址,最后发起调用,这个过程完全是发生在客户端方面的,但是对于服务端的负载均衡来说,这个服务端只对外部暴露一个服务接口,那么所有的请求都需要通过这个唯一的接口来访问。然而,在这个接口背后有着一套负载均衡的逻辑,接口背后运行着许多的服务,而具体使用哪个,由不同的负载均衡算法决定,这一过程放生在服务端。

# 日志

可以在 Configuration 类下加入以下代码:

@Bean
Logger.Level feignLoggerLevel() {
    return Logger.Level.FULL;
}

在 yaml 下加入远程调用包的日志配置:

logging:
  level:
    com.KarryCode.feign: debug

再次请求即可观察到日志输出,可以看到是怎么请求的。

# 超时控制

分别有连接超时和读取超时,通过其源码实现可以观察到,连接超时是 10 秒,而读取超时是 60 秒。我们仍然可以通过配置来修改,这个具体的时间。

这里说一个题外话,我们可以使用多个配置文件来让其生效,比如我这里有一个 application-feign.yml,我们还有一个主文件 application.yml,如果我们想让 application-feign.yml 生效的话,我们可以在主配置文件加上这样的一句话:include: feign

spring:
  profiles:
    active: dev
    include: feign

其配置方式如下所示:

spring:
  cloud:
    openfeign:
      client:
        config:
          default:
            connectTimeout: 3000
            readTimeout: 5000
            logger-level: full

# 重试机制

默认情况下,OpenFeign 的重试策略是从不重试(是的没错),我们可以手动启动这个重试策略,其具体的有:我们可以设计间隔 100 毫秒,最大间隔 1 秒。最大尝试 5 次。类似计网里面的退避算法

同样在相应的配置下面加入这样的代码:

@Bean
Retryer feignRetryer() {
    return new Retryer.Default();
}

这是一个默认的重试器,其重试规则正如我上面所说那样:我们可以设计间隔 100 毫秒,最大间隔 1 秒。最大尝试 5 次。

# 拦截器

我们可以创建一个请求拦截器:

@Component
public class XTokenRequestInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate requestTemplate) {
        System.out.println("拦截器启动");
        requestTemplate.header("X-Token", UUID.randomUUID().toString());
    }
}

然后再接收请求时,我们可以解析一下:

@GetMapping("/product/{productId}")
public Product getProduct(@PathVariable Long productId, HttpServletRequest request) {
    System.out.println("token:" + request.getHeader("X-Token"));
    log.info("查询商品信息: {}", productId);
    return productService.getProductById(productId);
}

可以看到 Token 的输出:token:ebf2c253-7e63-483a-92bc-fc9d1819a418。

# Fallback 兜底返回

兜底返回机制需要配合我们还没有学到的 sentinel 框架,首先引入依赖:

<!--    sentinel    -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

加入配置:

feign:
  sentinel:
    enabled: true

在服务发起端加入兜底策略:

@Component
public class ProductFeignClientFallback implements ProductFeignClient {
    @Override
    public Product getProductById(Long productId) {
        Product product = new Product();
        product.setId(productId);
        product.setPrice(new BigDecimal("0"));
        product.setProductName("不到啊-" + productId);
        product.setNum(2);
        return product;
    }
}

关闭 Product 服务,只保留 Order 服务,可以看到返回了兜底数据:

{
  "id": 1,
  "totalAmount": 0,
  "userId": 777,
  "nickName": "Karry.Liu",
  "address": "北极",
  "product": [
    {
      "id": 888,
      "price": 0,
      "productName": "不到啊-888",
      "num": 2
    }
  ]
}

# Sentinel

11 11

# 基础场景

@SentinelResource(value = "createOrder")

添加上此注解,流量即被监控。

11

添加流控规则:

11

每秒只放行一次请求,如果你请求多了就会被打回。

Fail to send:http://localhost:8081/create?productId=777&userId=1
Blocked by Sentinel (flow limiting)

这是一个默认错误页面,我们能不能返回一个高度自定义的数据呢?

但是可以的!这涉及到 Sentinal 的异常处理机制。

# 异常处理

11
  1. Flow Exception:流控异常
  2. Param Flow Exception:热点参数异常
  3. Degrade Exception:熔断降级异常
  4. Authority Exception:权限控制类异常
  5. System Block Exception:系统阻塞异常

上述说的问题,我们需要自定义一个异常。

@Component
public class MyBlockException implements BlockExceptionHandler {
    private ObjectMapper objectMapper = new ObjectMapper();
    @Override
    public void handle(HttpServletRequest httpServletRequest,
                       HttpServletResponse httpServletResponse,
                       String resourceName, BlockException e) throws Exception {
        R error = R.error(500, resourceName + "被限流了", e.getMessage());
        httpServletResponse.setContentType("application/json;charset=utf-8");
        PrintWriter writer = httpServletResponse.getWriter();
        String json = objectMapper.writeValueAsString(error);
        writer.write(json);
    }
}
11

这个时候,如果你给 createOrder 方法添加流控规则的话,你会发现这个我们之前定义的 json 不生效了。

Fail to send:http://localhost:8081/create?productId=777&userId=1
<html><body><h1>Whitelabel Error Page</h1><p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p><div id='created'>Wed Nov 05 16:15:00 CST 2025</div><div>There was an unexpected error (type=Internal Server Error, status=500).</div></body></html>

这是因为它的异常类型不同,才导致的这样的结果。

# 流控规则

阈值类型:

  1. QPS:每秒请求数
  2. 并发线程数

我们优先推荐使用 QPS。

是否集群:

  1. 单机均摊
  2. 总体阈值

流控模式:

  1. 直接模式
  2. 关联模式
  3. 链路模式

流控效果

  1. 快速失败
  2. Warm Up
  3. 排队等待
11 11

# 熔断降级

11

# Gateway

# 参考文献

  1. 【尚硅谷 SpringCloud 速成】https://www.bilibili.com/video/BV1UJc2ezEFU
更新于

请我喝[茶]~( ̄▽ ̄)~*

KarryLiu 微信支付

微信支付

KarryLiu 支付宝

支付宝