SpringCloud微服务入门

1.Nacos

1.1注册中心

服务注册与发现整体流程

在这里插入图片描述

1.2 配置中心

在这里插入图片描述

后配置的>先配置的 远程>本地项目配置

1.3 总结

在这里插入图片描述

2.OpenFeign

承担远程调用的组件

在这里插入图片描述

2.1声明式实现

mvc注解的两套使用逻辑:

1、在restcontroller下是接受这样的请求

2、在feignclient上是发送这样的请求

和springmvc共用注解,例如:

@GetMapping标注在Controller上,是来接收get请求;标在FeignClient上,是用来发送get请求

@PathVariable
场景ControllerFeignClient
角色服务端(接收请求)客户端(发起请求)
@PathVariable从 URL 路径中提取参数值(如/user/123向 URL 路径中注入参数值(生成/user/123
执行方向被动接收 HTTP 请求并处理主动构建 HTTP 请求并发送
@FeignClient(value = "service-product") //feign客户端
public interface ProductFeignClient {

    @GetMapping("/product/{id}")
    Product getProductById(@PathVariable("id") Long id);
}
对比项Controller 中的 @PathVariableFeignClient 中的 @PathVariable
参数流向从 URL 路径 → 方法参数从方法参数 → URL 路径
URL 作用定义服务端接收的路径格式定义客户端生成的路径格式
必传性默认必传(路径中必须包含变量)必须提供参数值(否则 URL 路径不完整)
参数名映射@PathVariable("id") 与路径变量{id}对应必须与路径变量{id}完全一致
错误场景路径格式不匹配时返回 404参数缺失时生成非法 URL(如/product/null

2.2第三方API

@FeignClient(value = "weather-client",  url = "http://aliv18.data.moji.com") //是域名
public interface WeaterFeignClient {
    @PostMapping("/whapi/json/alicityweather/condition")
    String getWeather(@RequestHeader("Authorization") String auth,
                    @RequestParam("token") String token,
                    @RequestParam("cityId") String cityId);
}
@SpringBootTest
public class WeatherTest {

    @Autowired
    WeaterFeignClient weaterFeignClient;
    @Test
    void test01(){
        String weather = weaterFeignClient.getWeather("APPCODE 93b7e19861a24c519a7548b17dc16d75",
                "50b53ff8dd7d9fa320d3d3ca32cf8ed1",
                "2182");
        System.out.println("weather = " + weather);
    }
}
weather = {"code":0,"data":{"city":{"cityId":2182,"counname":"中国","ianatimezone":"Asia/Shanghai","name":"西安市","pname":"陕西省","secondaryname":"陕西省","timezone":"8"},"condition":{"condition":"晴","conditionId":"5","humidity":"15","icon":"0","pressure":"956","realFeel":"33","sunRise":"2025-06-15 05:32:00","sunSet":"2025-06-15 19:58:00","temp":"33","tips":"有些热了,记得多喝水。","updatetime":"2025-06-15 16:30:08","uvi":"5","vis":"23800","windDegrees":"225","windDir":"西南风","windLevel":"4","windSpeed":"5.69"}},"msg":"success","rc":{"c":0,"p":"success"}}

客户端负载均衡和服务端负载均衡的区别

在这里插入图片描述

2.3进阶配置

1.日志

logging:
  level:
    com.atguigu.order.feign: debug
@Bean
Logger.Level feignLoggerLevel() {
    return Logger.Level.FULL;
}

2.超时控制

在这里插入图片描述

在这里插入图片描述

spring:
  cloud:
    openfeign:
      client:
        config:
          default:
            logger-level: full
            connect-timeout: 1000
            read-timeout: 2000
          service-product:
            logger-level: full
            connect-timeout: 3000
            read-timeout: 5000

3.重试机制

远程调用超时失败后,还可以进行多次尝试,如果某次成功返回OK,如果多次依然失败则结束调用,返回错误

在这里插入图片描述

重试机制可以放进容器中

@Configuration

public class OrderServiceConfig {
    @LoadBalanced //注解式负载均衡
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

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

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

Retryer源码如下:

在这里插入图片描述

4.拦截器

在这里插入图片描述

@Component
public class XTokenRequestInterceptor implements RequestInterceptor {

    /**
     * 请求拦截器
     * @param template 请求模板
     */
    @Override
    public void apply(RequestTemplate template) {
        template.header("X-Token", UUID.randomUUID().toString());
    }
}

改造商品服务controller

@RestController
public class ProductController {

    @Autowired
    private ProductService productService;
    // 查询商品

    @GetMapping("/product/{id}")
    public Product getProduct(@PathVariable("id") Long productId,
                                HttpServletRequest request) {
        String header = request.getHeader("X-Token");
        System.out.println("hello.....[" + header + "]");
        Product product = productService.getProductById(productId);
        return product;
    }
}

输出

hello.....[0500d10e-643a-4262-a222-388fd1fdb55b]

5.Fallback

在这里插入图片描述

定义商品服务 Feign 接口 fallback 实现类

package com.atguigu.order.feign.fallback;

import com.atguigu.order.feign.ProductFeignClient;
import com.atguigu.product.bean.Product;
import org.springframework.stereotype.Component;

import java.math.BigDecimal;

@Component
public class ProductFeignClientFallback implements ProductFeignClient {
    @Override
    public Product getProductById(Long id) {
        System.out.println("兜底回调........");
        Product product = new Product();
        product.setId(id);
        product.setPrice(new BigDecimal("0"));
        product.setProductName("未知商品");
        product.setNum(0);

        return product;
    }
}

在feignclient客户端实现

@FeignClient(value = "service-product",fallback = ProductFeignClientFallback.class) //feign客户端
public interface ProductFeignClient {

    @GetMapping("/product/{id}")
    Product getProductById(@PathVariable("id") Long id);
    //这部分写好后,可以自动注入到订单服务的实现类orderserviceimpl里
}

不启动商品服务的情况下,发送http://localhost:8000/create?userId=2&productId=100请求,会返回下面数据:

{
  "id": 1,
  "totalAmount": 0,
  "userId": 2,
  "nickName": "zhangsan",
  "address": "尚硅谷",
  "productList": [
    {
      "id": 100,
      "price": 0,
      "productName": "未知商品",
      "num": 0
    }
  ]
}

3.Sentinel

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

在这里插入图片描述

3.1架构原理

在这里插入图片描述

定义资源:

  • 主流框架自动适配(Web Servlet、Dubbo、Spring Cloud、gRPC、Spring WebFlux、Reactor);所有 Web 接口均为资源
  • 编程式:SphU API
  • 声明式:@SentinelResource

定义规则:

  • 流量控制(FlowRule)
  • 熔断降级(DegradeRule)
  • 系统保护(SystemRule)
  • 来源访问控制(AuthorityRule)
  • 热点参数(ParamFlowRule)

3.2工作原理

在这里插入图片描述

3.3整合使用

在这里插入图片描述

controller里的请求路径,可以被sentinel自动探测到,并识别为一种资源,展示在sentinel控制台

3.4异常处理

在这里插入图片描述

在这里插入图片描述

3.4.1Web接口的sentinel异常处理

自定义BlockExceptionHandler
//自定义重写BlockExceptionHandler接口
@Component
public class MyBlockExceptionHandler implements BlockExceptionHandler {
    // 初始化Jackson的ObjectMapper实例,用于JSON与Java对象间的序列化和反序列化
    private ObjectMapper objectMapper = new ObjectMapper();
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, String resourceName, BlockException e) throws Exception {
        // 设置响应内容类型为JSON,并指定UTF-8编码,确保中文正常显示
        response.setContentType("application/json;charset=utf-8");
        // 获取响应输出流,用于返回JSON数据
        PrintWriter writer = response.getWriter();
        // 构建统一的错误响应对象,500表示服务器内部错误状态码
        //resourceName标识被限流的资源名称,e.getMessage()包含具体的限流原因(如QPS超过阈值)
        R error = R.error(500, resourceName + "被Sentinel限流了...,原因" + e.getClass());
        // 使用ObjectMapper将错误响应对象序列化为JSON字符串
        String json = objectMapper.writeValueAsString(error);
        // 将JSON格式的错误信息写入响应流,客户端将收到 {"code":500,"message":"xxx被限流..."} 的响应
        writer.write(json);
        writer.flush();
        writer.close();
    }
}
定义通用的响应结果封装类
@Data
public class R {
    private Integer code;
    private String msg;
    private Object data;

    public static R ok() {
        R r = new R();
        r.setCode(200);
        return r;
    }


    public static R ok(String msg, Object data) {
        R r = new R();
        r.setCode(200);
        r.setMsg(msg);
        r.setData(data);
        return r;
    }

    public static R error() {
        R r = new R();
        r.setCode(500);
        return r;
    }

    public static R error(Integer code, String msg) {
        R r = new R();
        r.setCode(code);
        r.setMsg(msg);
        return r;
    }
}

3.4.2@SentinelResource的sentinel异常处理

@SentinelResource(value = "createOrder")  //sentinel声明式注解

在sentinel控制台给createorder创建流控规则

在这里插入图片描述

SentinelResourceAspect切面

SentinelResourceAspect 是 Sentinel 用于处理 @SentinelResource 注解的切面类,其核心逻辑围绕资源调用、规则校验与异常抛出展开,结合 Spring 异常处理体系(如 @RestControllerAdvice)的工作流程可概括为:

1. SentinelResourceAspect 核心流程

SentinelResourceAspect 通过 AOP 切面,拦截被 @SentinelResource 标注的方法:

  • 资源 entry 创建:调用 SphU.entry(...)SphO.entry(...),触发 Sentinel 的规则校验(流控、熔断等)。
  • 规则触发时抛异常:若触发限流 / 熔断规则,Sentinel 会抛出 BlockException(或其子类,如 FlowExceptionDegradeException 等 )。
  • 无兜底时直接抛:若方法未配置 blockHandler(流控兜底)或 fallback(异常兜底),BlockException直接向上抛出,进入 Spring 的异常处理链路。

2. 结合 Spring 全局异常处理(@RestControllerAdvice

BlockException 未被 @SentinelResource 自身的 blockHandler/fallback 捕获时,异常会穿透到 Spring 容器

  • @RestControllerAdvice 接管:项目中自定义的 @RestControllerAdvice 全局异常处理器,可通过捕获 BlockException(或更细粒度的子类),统一格式化响应(如封装成 R.error(...) 结构 )。
  • 最终兜底:若全局处理器也未特殊处理,异常会继续上抛,由 Spring 内置的异常解析器处理,最终可能返回默认的错误页面或 JSON(取决于项目配置 )。

简单来说:SentinelResourceAspect 先通过 AOP 拦截方法、触发 Sentinel 规则校验并抛 BlockException;若注解未配置兜底逻辑,异常会 “漏” 到 Spring 层,此时 @RestControllerAdvice 可作为全局兜底,捕获并自定义异常响应,实现统一的异常处理流程。

自定义全局异常处理器
//@ResponseBody
//@ControllerAdvice
//可被一个合成注解替换
@RestControllerAdvice
public class GlobalExecptionHandler {
    @ExceptionHandler(Throwable.class)
    public String error(Throwable e) {
        e.printStackTrace();
        return "error";
    }
}

每个项目中的全局异常处理器不尽相同,但都可以使用@RestControllerAdvice注解来处理@SentinelResource异常

blockHandler使用样例

OrderServiceImpl类

    @SentinelResource(value = "createOrder", blockHandler = "createOrderFallback")  //sentinel声明式注解
    @Override
    public Order createOrder(Long productId, Long userId) {
        //以前是使用使用restemplate获取商品数据,现在使用feignclient,下面注释掉
//        Product product = getProductFromRemoteWithLoadBalanceAnnotation(productId);
        //现在使用feignclient完成远程调用
        Product product = productFeignClient.getProductById(productId);

        Order order = new Order();
        order.setId(1L);
        //TODO 总金额
        BigDecimal sumPrice = product.getPrice().multiply(new BigDecimal(product.getNum()));
        order.setTotalAmount(sumPrice);
        order.setUserId(userId);
        order.setNickName("zhangsan");
        order.setAddress("尚硅谷");
        // 远程查询商品列表
        order.setProductList(Arrays.asList(product));

        return order;
    }

    //兜底回调
    public Order createOrderFallback(Long productId, Long userId, BlockException e) {
        Order order = new Order();
        order.setId(0L);
        order.setTotalAmount(new BigDecimal(0));
        order.setUserId(userId);
        order.setNickName("未知用户");
        order.setAddress("异常信息:" + e.getClass());
        return order;
    }

如果被@SentinelResource标注的资源,没有违反规则,则调用真实的业务逻辑,返回真实的数据;

违反规则,被sentinel限制了,则调用blockhandler指定的方法,返回兜底数据,也就是controller要么得到一个真实数据,要么得到一个兜底数据,但最终都会得到一个order。

运行显示

{
  "id": 0,
  "totalAmount": 0,
  "userId": 2,
  "nickName": "未知用户",
  "address": "异常信息:class com.alibaba.csp.sentinel.slots.block.flow.FlowException",
  "productList": null
}
最佳实践

总结就是,@SentinelResource一般标注在非controller的层,想要给哪些方法进行保护,就加上这个注解,一旦违反规则,如果业务规定有兜底回调的数据,那么就用blockhandler指定兜底回调,如果业务没规定使用兜底回调,那么也可以不用任何一种回调机制,直接让异常抛给全局,由项目的springboot全局异常处理器进行处理。

3.4.3OpenFeign调用的sentinel异常处理

OrderServiceImpl类里的openfeign,sentinel也能自动探测到

//现在使用feignclient完成远程调用
Product product = productFeignClient.getProductById(productId);

在这里插入图片描述

对其添加流控规则,发现返回

{
  "id": 1,
  "totalAmount": 0,
  "userId": 2,
  "nickName": "zhangsan",
  "address": "尚硅谷",
  "productList": [
    {
      "id": 100,
      "price": 0,
      "productName": "未知商品",
      "num": 0
    }
  ]
}

是由于在编写openfeign客户端的时候,指定了一个fallback回调(openfeign的兜底返回),产生了一个未知商品的订单

但如果没有的话,还是和上面一样层层往上抛,直到由springboot全局异常处理器来处理

原理在SentinelFeignAutoConfiguration类里

3.4.4Sphu硬编码

try {
    SphU.entry("hahah");
    // 总金额
    // order.setTotalAmount(product.getPrice().multiply(new BigDecimal(product.getNum())));
    order.setUserId(userId);
    order.setNickName("zhangsan");
    order.setAddress("尚硅谷");
    // 远程查询商品列表
    // order.setProductList(Arrays.asList(product));
} catch (BlockException e) {
    // 编码处理
}

// 1.5.0 版本开始可以利用 try-with-resources 特性(使用有限制)
// 资源名可使用任意有业务语义的字符串,比如方法名、接口名或其它可唯一标识的字符串。
try (Entry entry = SphU.entry("resourceName")) {
    // 被保护的业务逻辑
    // do something here...
} catch (BlockException ex) {
    // 资源访问阻止,被限流或被降级
    // 在此处进行相应的处理操作
}

可以查看Sentinel 文档查看更多用法

3.5流控规则

在这里插入图片描述

3.5.1流控模式

在这里插入图片描述

直接策略

直接策略就是之前做的最简单的流控规则,这里不再赘述

链路策略

链路策略演示

//创建订单
@GetMapping("/create")
public Order createOrder(@RequestParam("productId") Long productId,@RequestParam("userId") Long userId) {
    Order order = orderService.createOrder(productId, userId);
    return order;
}

//秒杀创建订单
@GetMapping("/seckill")
public Order createSeckillOrder(@RequestParam("productId") Long productId,@RequestParam("userId") Long userId) {
    Order order = orderService.createOrder(productId, userId);
    order.setId(Long.MAX_VALUE);
    return order;
}

配置文件修改:

sentinel:
  transport:
    dashboard: localhost:8080
  eager: true
  web-context-unify: false #取消将资源放进默认目录中

在这里插入图片描述

连续访问http://localhost:8000/seckill?userId=2&productId=100

{
  "id": 9223372036854775807,
  "totalAmount": 0,
  "userId": 2,
  "nickName": "未知用户",
  "address": "异常信息:class com.alibaba.csp.sentinel.slots.block.flow.FlowException",
  "productList": null
}

而多次访问http://localhost:8000/create?userId=2&productId=100,也不会出现上述问题,这就是链路策略,仅对于某个路径下的访问失效。

关联策略
@GetMapping("/writeDb")
public String writeDb() {
    return "writeDb success....";
}

@GetMapping("/readDb")
public String readDb() {
    return "readDb success....";
}

重启订单,设置流控规则

在这里插入图片描述

当对关联资源 /writeDb 进行大量访问后,因 Sentinel 配置了以 QPS 为阈值、关联模式、快速失败的流控规则,后续访问 /readDb 时会触发限流,返回限流异常提示 。 简单说就是:通过关联策略,大量访问 /writeDb 后触发 /readDb 的流控规则,使 /readDb 被 Sentinel 限流

当大量访问http://localhost:8000/writeDb之后,马上访问http://localhost:8000/readDb会返回如下

{
  "code": 500,
  "msg": "/readDb被Sentinel限流了...,原因class com.alibaba.csp.sentinel.slots.block.flow.FlowException",
  "data": null
}

3.5.2流控效果

在这里插入图片描述

注意:只有快速失败支持流控模式(直接,关联,链路)的设置

快速失败

引入Slf4j

@Slf4j //日志
public class OrderController {
    @GetMapping("/readDb")
    public String readDb() {
        log.info("readDb....");
        return "readDb success....";
    }
}

改造MyBlockExceptionHandler

@Component
public class MyBlockExceptionHandler implements BlockExceptionHandler {
    private ObjectMapper objectMapper = new ObjectMapper();
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, String resourceName, BlockException e) throws Exception {

        //请求太多了
        response.setStatus(429); //too many request
        
        response.setContentType("application/json;charset=utf-8");
        PrintWriter writer = response.getWriter();
        R error = R.error(500, resourceName + "被Sentinel限流了...,原因" + e.getClass());
        String json = objectMapper.writeValueAsString(error);
        writer.write(json);
        writer.flush();
        writer.close();
    }

使用ApiPost进行压测,可以看到只通过了7个请求

在这里插入图片描述

Warm Up

在这里插入图片描述

WarmUp 流控规则下,系统先处于闲置,突增请求到来时,并非直接达阈值处理,
而是经预热阶段逐步提升处理能力,
待预热结束,按规则设定阈值稳定处理请求,避免突发流量冲击致系统异常 。

编辑流控规则

在这里插入图片描述

进行压测

在这里插入图片描述

查看控制台可以看到趋势在慢慢往上涨

匀速排队

在这里插入图片描述

漏桶算法可以查看Sentinel 文档进行了解

3.6熔断降级规则

流量限制是请求与服务之间的,熔断是服务于服务之间的

在这里插入图片描述

切断不稳定调用,快速返回不积压,避免雪崩效应

最佳实践:熔断降级作为保护自身的手段,通常在客户端(调用端)进行配置

在这里插入图片描述

3.6.1断路器工作原理

在这里插入图片描述

3.6.2熔断策略

慢调用比例

在这里插入图片描述

  1. 模拟服务延迟:在 ProductController 中对应接口逻辑里,添加 2 秒(2000ms ,超过最大 RT 阈值 1000ms )的线程休眠,使请求成为慢调用。
  2. 触发熔断:多次刷新请求 URL http://localhost:8000/create?userId=2&productId=100 ,当满足最小请求数(5 次 )且慢调用比例达到阈值(0.8 )时,触发熔断,此时 Feign 客户端会执行 ProductFeignClientFallback 中的兜底回调逻辑,返回降级后的商品数据。
  3. 熔断状态验证:熔断触发后进入 30 秒熔断时长,期间若尝试发送请求,断路器会直接拒绝,维持熔断状态;可观察到半开状态下的试探请求,若试探请求仍不满足响应要求,继续保持熔断。
  4. 恢复正常服务:关闭 ProductController 中的休眠逻辑,重启商品服务(或等待服务自身健康检查恢复 ),此时断路器检测到服务恢复正常,关闭熔断状态,后续请求可正常调用商品服务接口,获取正常响应 。
异常比例

我们在远程 product 服务的业务逻辑里,主动注入一个数学异常(如除数为 0 等运算) 。当 order 服务通过 Feign 调用 product 服务时,由于 product 服务抛出未处理的异常导致远程调用 “爆炸”(服务不可用或返回错误响应 ),此时 order 服务中配置的 Feign 客户端兜底回调(ProductFeignClientFallback )会被触发,返回降级后的默认数据 。

在这里插入图片描述

场景无熔断规则有熔断规则
调用链路必须完整执行 A→B 调用,失败后才走 fallback熔断触发时,直接切断 A→B 链路,优先走 fallback
资源消耗持续浪费线程、网络资源(即使 B 故障)熔断后释放资源,保障核心业务
故障影响易引发调用链雪崩(A、B 及关联服务连环故障)隔离 B 的故障,保护 A 及其他服务稳定运行
响应速度需等待 B 超时 / 报错,响应延迟高直接走 fallback,响应更可控

配置异常比例

在这里插入图片描述

在完成熔断规则配置后进行接口测试,流程如下:

起初,order 服务正常发起对 product 服务的远程调用,product 服务中因预设逻辑抛出数学异常,order 服务随即触发兜底回调;

多次发送请求后,基于所配置的异常比例熔断规则,当异常比例等指标达到熔断条件时,熔断机制生效,order 服务不再向 product 服务发送实际请求,直接执行兜底回调逻辑:

在这里插入图片描述

此时 product 服务不会接收到相关调用请求,也就看不到对应调用产生的信息 ,此可验证熔断规则对请求的拦截与服务降级保护效果。

在这里插入图片描述

异常数

在这里插入图片描述

3.6.3热点规则(热点参数限流)

可查看热点规则官方文档

何为热点?热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如:

  • 商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制
  • 用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制

热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。

在这里插入图片描述

Sentinel 利用 LRU 策略统计最近最常访问的热点参数,结合令牌桶算法来进行参数级别的流控。热点参数限流支持集群模式。

案例

在这里插入图片描述

**注意:**目前 Sentinel 自带的 adapter 仅 Dubbo 方法埋点带了热点参数,其它适配模块(如 Web)默认不支持热点规则,可通过自定义埋点方式指定新的资源名并传入希望的参数。注意自定义埋点的资源名不要和适配模块生成的资源名重复,否则会导致重复统计。

需求1

修改秒杀controller

//秒杀创建订单
@GetMapping("/seckill")
@SentinelResource(value = "seckill-order",fallback = "seckillFallback")
public Order createSeckillOrder(@RequestParam("productId") Long productId,@RequestParam("userId") Long userId) {
    Order order = orderService.createOrder(productId, userId);
    order.setId(Long.MAX_VALUE);
    return order;
}

public Order seckillFallback(Long productId,Long userId, BlockException exception) {
    System.out.println("进入seckillfallback兜底回调方法");
    Order order = new Order();
    order.setId(productId);
    order.setUserId(userId);
    order.setAddress("异常信息:" + exception.getClass());
    return order;
}

创建热点规则

在这里插入图片描述

然后修改代码

@GetMapping("/seckill")
@SentinelResource(value = "seckill-order",fallback = "seckillFallback")
public Order createSeckillOrder(@RequestParam(value = "productId",defaultValue = "1000") Long productId,
                                @RequestParam(value = "userId",required = false) Long userId) {
    Order order = orderService.createOrder(productId, userId);
    order.setId(Long.MAX_VALUE);
    return order;
}

@RequestParam(value = “userId”,required = false)的意思是,携带参数就会被流控,不携带,不流控(无论是谁每秒最多一个)
查看效果

在这里插入图片描述

在这里插入图片描述

需求2

6号用户是vvip,不限制QPS,修改热点规则

在这里插入图片描述

这样多次刷新userid为6,也不会有什么问题

需求3

666号是下架商品,不允许访问,新增一个热点规则

在这里插入图片描述

这样直接抛出异常java.lang.reflect.UndeclaredThrowableException

3.7fallback与blockHandler兜底回调

兜底回调有两种写法,有block优先使用block,没有指定block,才走fallback,但是fallback能处理业务异常

看代码:

public Order seckillFallback(Long productId,Long userId, BlockException exception) {
    System.out.println("进入seckillfallback兜底回调方法");
    Order order = new Order();
    order.setId(productId);
    order.setUserId(userId);
    order.setAddress("异常信息:" + exception.getClass());
    return order;
}

如果是BlockException exception,会调用全局异常处理器,返回error

//@ResponseBody
//@ControllerAdvice
//可被一个合成注解替换
@RestControllerAdvice
public class GlobalExecptionHandler {
    @ExceptionHandler(Throwable.class)
    public String error(Throwable e) {
        e.printStackTrace();
        return "error";
    }
}

而改成Throwable exception时,才会触发seckillFallback方法。原理如下:

Sentinel 熔断降级回调方法的参数类型必须与原方法一致,且异常类型必须能捕获触发的异常

Sentinel 回调机制
Sentinel 的 @SentinelResource 注解中,fallback 方法的异常参数类型需要 显式兼容 实际抛出的异常。在你的场景中:

  • 原问题seckillFallback 方法参数为 BlockException exception,但实际触发的是 热点参数限流异常ParamFlowException),而 ParamFlowExceptionBlockException 的子类,因此 理论上应该匹配
  • 矛盾点:但实际却未触发 seckillFallback,而是被全局异常处理器 GlobalExecptionHandler 捕获。这是因为:

Spring AOP 代理对异常的包装
当 Sentinel 拦截器抛出 ParamFlowException 时,若当前方法被 Spring AOP 代理(如你使用了 @SentinelResource),且代理方法 未显式声明抛出该异常,Spring 会将异常包装为 UndeclaredThrowableException(这是 JDK 动态代理的行为)。此时:

  • BlockException 无法捕获被包装后的 UndeclaredThrowableException,因此 fallback 方法不触发;
  • 而全局异常处理器 @ExceptionHandler(Throwable.class) 可以捕获所有异常,导致请求被拦截到全局处理逻辑。

在 Sentinel 熔断降级场景中,建议 始终使用 Throwable 作为 fallback 方法的异常参数

4.Gateway网关

在这里插入图片描述

spring cloud gateway

在这里插入图片描述

4.1路由

4.1.1路由规则

需求:

  • 1.客户端发送 /api/order/** 转到 service-order
  • 2.客户端发送 /api/product/** 转到 service-product
  • 3.以上转发有负载均衡效果

在这里插入图片描述

配置文件编写路由规则

源码RouteDefinition.class

private String id; // 路由唯一标识
@NotEmpty
@Valid
private List<PredicateDefinition> predicates = new ArrayList(); // 路由断言列表,用于匹配请求条件
@Valid
private List<FilterDefinition> filters = new ArrayList(); // 路由过滤器列表,用于处理请求和响应
@NotNull
private URI uri; // 路由目标URI,请求匹配后转发的目的地
private Map<String, Object> metadata = new HashMap(); // 路由元数据,用于存储自定义配置信息
private int order = 0; // 路由执行顺序,数值越小优先级越高

按照上面定义的,新建application-route.yml文件,包含在application.yml中

spring:
  cloud:
    gateway:
      routes:
        - id: order_route
          uri: lb://service-order
          predicates: #断言机制
            - Path=/api/order/**
        - id: product_route
          uri: lb://service-product
          predicates:
            - Path=/api/product/**

由于在 Spring Cloud Gateway 中配置了以上路由规则,需在对应服务的 Controller 类上添加请求映射注解,以确保路由匹配生效:

  • 对于service-order服务的 Controller,需添加 @RequestMapping("/api/order")
  • 对于service-product服务的 Controller,需添加 @RequestMapping("/api/product")

测试发现,配置成功后,依旧可以有负载均衡效果

4.1.2工作原理

在这里插入图片描述

4.2predicate断言

4.2.1长短写法

短写法:- Path = /api/order/**

spring:
  cloud:
    gateway:
      routes:
      - id: after_route
        uri: https://example.org
        predicates:
        - Cookie=mycookie,mycookievalue

全写法:

spring:
  cloud:
    gateway:
      routes:
      - id: after_route
        uri: https://example.org
        predicates:
        - name: Cookie
          args:
            name: mycookie
            regexp: mycookievalue

所有的断言都是断言工厂的一个实现,RoutePredicateFactory,ctrl+H打开它的实现,就可以看到
在这里插入图片描述

4.2.2Query

参数(个数/类型)作用
After1 / datetime在指定时间之后
Before1 / datetime在指定时间之前
Between2 / datetime在指定时间区间内
Cookie2 / string, regexp包含 cookie 名且必须匹配指定值
Header2 / string, regexp包含请求头且必须匹配指定值
HostN / string请求 host 必须是指定枚举值
MethodN / string请求方式必须是指定枚举值
Path2 / List, bool请求路径满足规则,是否匹配最后的 /
Query2 / string, regexp包含指定请求参数
RemoteAddr1 / List请求来源于指定网络域(CIDR 写法)
Weight2 / string, int按指定权重负载均衡
XForwarded RemoteAddr1 / List从 X-Forwarded-For 请求头中解析请求来源,并判断是否来源于指定网络域

query写法

- id: bing-route
  uri: https://cn.bing.com/
  predicates:
    - name: Path
      args:
          patterns: /search
    - name: Query
      args:
        param: q
        regexp: haha
  order: 999
  metadata:
    hello: world

配置了多个predicate后,必须完全匹配才能到指定的路由

在这里插入图片描述

4.2.3自定义断言工厂

在这里插入图片描述

编写自定义断言工厂类

package com.atguigu.gateway.predicate;

import jakarta.validation.constraints.NotEmpty;
import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory;
import org.springframework.cloud.gateway.handler.predicate.GatewayPredicate;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.server.ServerWebExchange;

import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;

/**
 * 自定义路由断言工厂,用于判断请求参数是否包含特定值
 * 例如:判断请求中是否包含名为"user"且值为"vip"的参数
 */
@Component
public class VipRoutePredicateFactory extends AbstractRoutePredicateFactory<VipRoutePredicateFactory.Config> {

    public VipRoutePredicateFactory() {
        // 调用父类构造函数,传入配置类类型
        super(Config.class);
    }

    @Override
    public List<String> shortcutFieldOrder() {
        // 定义配置参数的顺序,对应YAML配置中的快捷参数写法
        // 例如:- Vip=user,vip 对应 param=user, value=vip
        return Arrays.asList("param", "value");
    }

    @Override
    public Predicate<ServerWebExchange> apply(Config config) {
        // 实现核心断言逻辑
        return new GatewayPredicate() {
            @Override
            public boolean test(ServerWebExchange serverWebExchange) {
                // 获取当前请求对象
                ServerHttpRequest request = serverWebExchange.getRequest();
                // 从请求参数中获取指定名称的第一个值
                // 例如:对于请求 localhost/search?q=haha&user=zhangtengyue
                // config.param为"user"时,first的值为"zhangtengyue"
                String first = request.getQueryParams().getFirst(config.param);
                
                // 判断参数值是否存在且等于配置的值
                // 若存在且相等,返回true(匹配成功);否则返回false
                return StringUtils.hasText(first) && first.equals(config.value);
            }
        };
    }

    /**
     * 配置类,用于存储断言所需的参数
     * 通过@Validated和@NotEmpty确保参数合法性
     */
    @Validated
    public static class Config {
        @NotEmpty // 参数名不能为空
        private String param; // 要检查的请求参数名
        @NotEmpty // 参数值不能为空
        private String value; // 期望的参数值

        // getters and setters
        public @NotEmpty String getParam() {
            return param;
        }
        
        public void setParam(@NotEmpty String param) {
            this.param = param;
        }
        
        public @NotEmpty String getValue() {
            return value;
        }
        
        public void setValue(@NotEmpty String value) {
            this.value = value;
        }
    }
}

配置文件写法如下:

spring:
  cloud:
    gateway:
      routes:
        - id: order-route
          uri: lb://service-order
          predicates:
            #短写法
#            - Path=/api/order/**
            #全写法
            - name: Path
              args:
                patterns: /api/order/**
                matchTrailingSlash: true  #为true,路径匹配会忽略尾部斜杠
          order: 1
        - id: product-route
          uri: lb://service-product
          predicates:
            - Path=/api/product/**
          order: 2
        - id: bing-route
          uri: https://cn.bing.com/
          predicates:
            - name: Path
              args:
                  patterns: /search
            - name: Query
              args:
                param: q
                regexp: haha
#            - Vip=user, zhangtengyue
            - name: Vip
              args:
                param: user
                value: zhangtengyue
          order: 999
          metadata:
            hello: world

运行测试
在这里插入图片描述

4.3过滤器

在这里插入图片描述

4.3.1基本使用

在这里插入图片描述

删除之前在controller上添加的requestmapping,添加以下的过滤器规则

filters:  #/api/order/ab/c => /ab/c
  - RewritePath=/api/order/(?<segment>.*), /${segment}
  - AddResponseHeader=X-Response-Abc, 123

在这里插入图片描述

在这里插入图片描述

4.3.2默认filter

spring:
  cloud:
    gateway:
      default-filters:
        - AddRequestHeader=X-Request-Abc, 123

4.3.3GlobalFilter

package com.atguigu.gateway.filter;

import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
@Slf4j
public class RtGlobalFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

        // ==================== 前置逻辑 ====================
        // 执行时机:请求进入过滤器链之前
        // 用途:日志记录、参数校验、权限检查等
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
        String uri = request.getURI().toString();
        long start = System.currentTimeMillis();
        log.info("请求【{}】开始:时间:{}", uri, start);
        
        
        // 放行请求并注册后置逻辑
        // 注意:chain.filter(exchange) 是异步非阻塞的,立即返回Mono<Void>,不会等待请求完成
        // 因此,后置逻辑不能直接写在调用之后,而必须通过响应式操作符注册回调
        return chain.filter(exchange)
                .doFinally(signalType -> {
                    // doFinally 会在请求完成(正常返回或异常)时执行
                    // 无论请求结果如何,都会记录结束时间和耗时
                    long end = System.currentTimeMillis();
                    log.info("请求【{}】结束:时间:{},耗时:{}ms", uri, end, end - start);

                    // signalType 表示终止信号类型,可能的值:
                    // - ON_COMPLETE:正常完成
                    // - ON_ERROR:发生异常
                    // - CANCEL:被取消
                    // 可根据需要添加不同处理逻辑
                });

        // ==================== 错误示范 ====================
        // 以下代码会立即执行,而非请求完成后执行
        // long end = System.currentTimeMillis();
        // log.info("请求耗时:{}ms", end - start);
    }

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE; // 设置过滤器执行顺序
    }
}
为什么必须用 doFinally
  1. 异步非阻塞特性
    chain.filter(exchange) 不会阻塞当前线程,而是立即返回 Mono<Void> 表示未来完成的操作。如果直接在调用后写后置逻辑(如计算耗时),这些代码会在请求处理完成前就执行,导致计时错误。
  2. 响应式操作符的回调机制
    通过 doFinallydoOnSuccessdoOnError 等操作符,可以注册当数据流终止时触发的回调。这些回调会在请求真正完成(无论是正常响应还是抛出异常)时执行,确保计时准确。
  3. 与传统 Servlet API 的对比
    • 在 Servlet API 中,请求处理是同步的(一个线程处理完整请求),因此可以在方法末尾写后置逻辑。
    • 在响应式 API 中,请求处理可能涉及多个线程和异步操作,必须通过响应式操作符显式注册回调。
其他可用的响应式操作符
  • doOnSuccess:仅在请求成功完成时执行。
  • doOnError:仅在发生异常时执行。
  • doAfterTerminate:与 doFinally 类似,但无法区分终止类型。
  • thenReturn:修改最终返回值(不常用,因为 filter 方法必须返回 Mono<Void>

测试结果

2025-06-24T12:15:32.531+08:00  INFO 18228 --- [gateway] [ctor-http-nio-2] c.atguigu.gateway.filter.RtGlobalFilter  : 请求【http://localhost/api/order/readDb】开始:时间:1750738532531
2025-06-24T12:15:32.538+08:00  INFO 18228 --- [gateway] [ctor-http-nio-2] c.atguigu.gateway.filter.RtGlobalFilter  : 请求【http://localhost/api/order/readDb】结束:时间:1750738532538,耗时:7ms

4.3.4自定义过滤器

配置文件写:

filters:  #/api/order/ab/c => /ab/c
  - RewritePath=/api/order/(?<segment>.*), /${segment}
  - OnceToken=X-Response-Token, uuid

自定义网关过滤器工厂,注意响应式编程的写法

package com.atguigu.gateway.filter;

import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.factory.AbstractNameValueGatewayFilterFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.UUID;
/**
 * 一次性令牌过滤器工厂
 * 功能:在响应头中添加一次性令牌(支持UUID或JWT格式)
 * 继承自AbstractNameValueGatewayFilterFactory,该抽象类提供了基于名称-值对的过滤器配置方式
 */
@Component
public class OnceTokenGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {

    /**
     * 创建并返回实际的过滤器实例
     * @param config 过滤器配置,包含名称和值(通过配置文件传入)
     * @return GatewayFilter实例,实现具体的过滤逻辑
     *
     * 方法签名解析:
     * 1. 该方法是过滤器工厂的核心方法,负责生成过滤器实例
     * 2. NameValueConfig是AbstractNameValueGatewayFilterFactory提供的配置类,
     *    包含getName()和getValue()方法获取配置参数
     * 3. 返回的GatewayFilter是函数式接口(仅含一个filter抽象方法),
     *    因此可以通过匿名内部类或Lambda表达式实现
     */
    @Override
    public GatewayFilter apply(NameValueConfig config) {
        return new GatewayFilter() {
            /**
             * 过滤器核心逻辑
             * @param exchange 请求-响应上下文,包含请求和响应对象
             * @param chain 过滤器链,用于传递请求到下一个过滤器
             * @return Mono<Void> 表示异步操作完成的信号
             *
             * 响应式编程解析:
             * 1. chain.filter(exchange)是异步操作,返回Mono<Void>表示请求处理完成
             * 2. then()操作符在请求处理完成后执行后续逻辑(添加响应头)
             * 3. Mono.fromRunnable()将Runnable转换为Mono,实现非阻塞回调
             */
            @Override
            public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                // 每次响应之前添加一个一次性令牌,支持UUID,JWT等各种格式
                return chain.filter(exchange).then(Mono.fromRunnable(() -> {
                    // 从上下文中获取响应对象
                    ServerHttpResponse response = exchange.getResponse();
                    // 获取响应头集合
                    HttpHeaders headers = response.getHeaders();
                    // 从配置中获取令牌类型("uuid"或"jwt")
                    String value = config.getValue();
                    // 根据配置生成实际的令牌值
                    if ("uuid".equalsIgnoreCase(value)) {
                        // 生成UUID作为一次性令牌
                        value = UUID.randomUUID().toString();
                    }
                    if ("jwt".equalsIgnoreCase(value)) {
                        // 示例JWT令牌(实际应用中应动态生成)
                        value = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNjg4NjQwMDAwLCJleHAiOjE2ODg2NDQ0MDAiLCJhdWQiOiJodHRwczovL2V4YW1wbGUuY29tIiwiaXNzIjoiYXV0aDAifQ.0aW5fZ3J2aG14c3RjdzV6c292dGV3dzVxY292dGV3dzVxY292dGV3ZA";
                    }
                    // 将令牌添加到响应头中,名称由配置决定
                    headers.add(config.getName(), value);
                }));
            }
        };
    }
}

4.3.5全局跨域

配置如下:

spring:
  cloud:
    gateway:
      globalcors:
        corsConfigurations:
            '[/**]':
              allowed-origin-patterns: '*'
              allowed-headers: '*'
              allowed-methods: '*'

测试,可以发现多了一些跨域字段,前端就可以放心访问了

在这里插入图片描述

5.Seata分布式事务

在这里插入图片描述

在这里插入图片描述

5.1环境搭建

在这里插入图片描述

在services的pom文件中添加依赖

在这里插入图片描述

修改每个微服务的配置文件连接本地数据库

启动nacos

startup.cmd -m standalone

启动sentinel

java -jar sentinel-dashboard-1.8.8.jar

启动seata

seata-server.bat

5.2独立接口测试

5.2.1订单-创建订单

OrderRestController.java
package com.atguigu.order.controller;


import com.atguigu.order.bean.OrderTbl;
import com.atguigu.order.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class OrderRestController {

    @Autowired
    OrderService orderService;


    /**
     * 创建订单
     * @param userId
     * @param commodityCode
     * @param orderCount
     * @return
     */
    @GetMapping("/create")
    public String create(@RequestParam("userId") String userId,
                         @RequestParam("commodityCode") String commodityCode,
                         @RequestParam("count") int orderCount)
    {
        OrderTbl tbl = orderService.create(userId, commodityCode, orderCount);
        return "order create success = 订单id:【"+tbl.getId()+"】";
    }

}
OrderServiceImpl.java
package com.atguigu.order.service.impl;

import com.atguigu.order.bean.OrderTbl;
import com.atguigu.order.feign.AccountFeignClient;
import com.atguigu.order.mapper.OrderTblMapper;
import com.atguigu.order.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class OrderServiceImpl implements OrderService {

    @Autowired
    OrderTblMapper orderTblMapper;



    @Autowired
    AccountFeignClient accountFeignClient;

    @Transactional
    @Override
    public OrderTbl create(String userId, String commodityCode, int orderCount) {
        //1、计算订单价格
        int orderMoney = calculate(commodityCode, orderCount);

        //2、扣减账户余额
        accountFeignClient.debit(userId, orderMoney);
        //3、保存订单
        OrderTbl orderTbl = new OrderTbl();
        orderTbl.setUserId(userId);
        orderTbl.setCommodityCode(commodityCode);
        orderTbl.setCount(orderCount);
        orderTbl.setMoney(orderMoney);

        //3、保存订单
        orderTblMapper.insert(orderTbl);

//        int i = 10/0;

        return orderTbl;
    }

    // 计算价格
    private int calculate(String commodityCode, int orderCount) {
        return 9*orderCount;
    }
}

结果

在这里插入图片描述

5.2.2库存-扣减库存

梳理流程

package com.atguigu.storage.controller;


import com.atguigu.storage.service.StorageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class StorageRestController {

    @Autowired
    StorageService  storageService;

    @GetMapping("/deduct")
    public String deduct(@RequestParam("commodityCode") String commodityCode,
                         @RequestParam("count") Integer count) {

        storageService.deduct(commodityCode, count);
        return "storage deduct success";
    }
}

实现

<update id="deduct">
    update storage_tbl
    set count = count - #{count}
    where commodity_code = #{commodityCode}
</update>

在这里插入图片描述

5.2.3账户-扣减余额

package com.atguigu.account.controller;

import com.atguigu.account.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class AccountRestController {


    @Autowired
    AccountService accountService;
    /**
     * 扣减账户余额
     * @return
     */
    @GetMapping("/debit")
    public String debit(@RequestParam("userId") String userId,
                        @RequestParam("money") int money){
        accountService.debit(userId, money);
        return "account debit success";
    }
}

在这里插入图片描述

5.3本地事务测试

package com.atguigu.storage.service.impl;

import com.atguigu.storage.mapper.StorageTblMapper;
import com.atguigu.storage.service.StorageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class StorageServiceImpl implements StorageService {

    @Autowired
    StorageTblMapper storageTblMapper;



    @Transactional
    @Override
    public void deduct(String commodityCode, int count) {
        storageTblMapper.deduct(commodityCode, count);
        if (count == 5) {
            throw new RuntimeException("库存不足");
        }
    }
}

为storage添加事务注解@Transactional,并在主程序添加@EnableTransactionManagement,添加判断逻辑,@Transactional 开启事务,先执行 storageTblMapper.deduct 扣减库存。若 count 为 5,抛出 RuntimeException,Spring 捕获到该未检查异常(默认规则),触发事务回滚,让扣减库存的操作撤销,数据库恢复之前状态;若 count 非 5,方法正常结束,事务提交,扣减操作生效 。

在这里插入图片描述

同样的对每个微服务下的涉及数据库操作的都添加声明式事务@EnableTransactionManagement,在每个业务方法里标注@Transactional

目前只是单接口本地事务,自己跟自己玩

5.4打通分布式事务链路

采购business发送purchase采购请求,带上商品编码,用户id,商品数量,采购开始远程调用,完成整个采购流程

PurchaseRestController

package com.atguigu.business.controller;

import com.atguigu.business.service.BusinessService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class PurchaseRestController {

    @Autowired
    BusinessService businessService;


    /**
     * 购买
     * @param userId 用户ID
     * @param commodityCode 商品编码
     * @param orderCount 数量
     * @return
     */
    @GetMapping("/purchase")
    public String purchase(@RequestParam("userId") String userId,
                           @RequestParam("commodityCode") String commodityCode,
                           @RequestParam("count") int orderCount){
        businessService.purchase(userId, commodityCode, orderCount);
        return "business purchase success";
    }
}

在主程序添加@EnableFeignClients(basePackages = “com.atguigu.business.feign”),把feignclient放在这个包下

先写一个库存的feignclient

StorageFeignClient.java

package com.atguigu.business.feign;


import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(value = "seata-storage")
public interface StorageFeignClient {

    /**
     * 扣减库存
     * @param commodityCode
     * @param count
     * @return
     */
    @GetMapping("/deduct")
    String deduct(@RequestParam("commodityCode") String commodityCode,
                         @RequestParam("count") Integer count);
}

再写一个订单的feignclient

OrderFeignClient

package com.atguigu.business.feign;


import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(value = "seata-order")
public interface OrderFeignClient {


    /**
     * 创建订单
     * @param userId
     * @param commodityCode
     * @param orderCount
     * @return
     */
    @GetMapping("/create")
    String create(@RequestParam("userId") String userId,
                         @RequestParam("commodityCode") String commodityCode,
                         @RequestParam("count") int orderCount);
}

BusinessService

package com.atguigu.business.service;

public interface BusinessService {

    /**
     * 采购
     * @param userId            用户id
     * @param commodityCode     商品编号
     * @param orderCount        购买数量
     */
    void purchase(String userId, String commodityCode, int orderCount);
}

BusinessServiceImpl

package com.atguigu.business.service.impl;

import com.atguigu.business.feign.OrderFeignClient;
import com.atguigu.business.feign.StorageFeignClient;
import com.atguigu.business.service.BusinessService;
import org.apache.seata.spring.annotation.GlobalTransactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;


@Service
public class BusinessServiceImpl implements BusinessService {

    @Autowired
    StorageFeignClient storageFeignClient;

    @Autowired
    OrderFeignClient orderFeignClient;




    @GlobalTransactional
    @Override
    public void purchase(String userId, String commodityCode, int orderCount) {
        //1. 扣减库存
        storageFeignClient.deduct(commodityCode, orderCount);

        //2. 创建订单
        orderFeignClient.create(userId, commodityCode, orderCount);
    }
}

5.5seata架构原理

全局事务和分支事务

在这里插入图片描述

新增file.conf

service {
  #transaction service group mapping
  vgroupMapping.default_tx_group = "default"
  #only support when registry.type=file, please don't set multiple addresses
  default.grouplist = "127.0.0.1:8091"
  #degrade, current not support
  enableDegrade = false
  #disable seata
  disableGlobalTransaction = false
}

这段是 Seata 的配置,

vgroupMapping.default_tx_group = "default" 把名为 default_tx_group 的事务组映射到 default 集群;

default.grouplist = "127.0.0.1:8091" 指定 default 集群的 Seata 服务地址为 127.0.0.1:8091(文件注册模式下仅支持单地址);

enableDegrade = false 表示不启用降级功能(当前版本该功能实际未支持);

disableGlobalTransaction = false 表示开启 Seata 全局事务,若设为true则禁用 。

在购买business的事务的事务方法(最大的方法入口)里添加@GlobalTransactional,添加这个标注,seata就会控制一个全局事务的开始,分支事务就会开始汇报给TC,

    @GlobalTransactional
    @Override
    public void purchase(String userId, String commodityCode, int orderCount) {
        //1. 扣减库存
        storageFeignClient.deduct(commodityCode, orderCount);

        //2. 创建订单
        orderFeignClient.create(userId, commodityCode, orderCount);
    }
}

5.6原理:二阶提交协议流程

Seata(Simple Extensible Autonomous Transaction Architecture)的分布式事务处理基于改良的两阶段提交协议,核心角色包括事务协调器(TC)、事务管理器(TM)和资源管理器(RM)。其执行流程分为全局事务注册分支事务执行两阶段提交

  1. 阶段一(准备):TM 向 TC 申请开启全局事务,获取 XID(全局事务 ID);RM 执行本地事务并将 undo/redo 日志写入本地事务资源表,然后向 TC 报告分支事务状态(成功或失败);TC 收集所有分支事务的状态,若全部成功则标记全局事务可提交,否则标记需回滚。
  2. 阶段二(提交 / 回滚):TC 根据全局事务状态向所有 RM 发送提交或回滚指令;RM 执行本地事务的提交或回滚操作(基于阶段一的日志),并释放事务资源;TC 最终确认所有分支事务的执行结果,完成全局事务。

Seata 的优化在于:

  • 异步提交:若阶段一所有分支均成功,阶段二可异步提交,提升性能。
  • 非阻塞:本地事务执行时无需等待其他参与者,仅通过日志保证可回滚。
  • 故障恢复:TC 记录全局事务状态,故障后可根据日志恢复执行。

5.7Seata的四种事务模式

5.7.1Seata AT 模式

AT 模式是 Seata 创新的一种非侵入式的分布式事务解决方案,Seata 在内部做了对数据库操作的代理层,我们使用 Seata AT 模式时,实际上用的是 Seata 自带的数据源代理 DataSourceProxy,Seata 在这层代理中加入了很多逻辑,比如插入回滚 undo_log 日志,检查全局锁等。

整体机制

两阶段提交协议的演变:

  • 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
  • 二阶段:
    • 提交异步化,非常快速地完成。
    • 回滚通过一阶段的回滚日志进行反向补偿。
基本使用

首先抽象一个使用场景,在用户购买行为的时候需要减少库存并减少账户余额,当库存表 stock_tblaccount_tbl 在同一个数据库时,我们可以使用关系数据库自身提供的能力非常容易实现事务。但如果这两个表分属于不同的数据源,我们就要使用 Seata 提供的分布式事务能力了。

观察下方的示例代码,

@GlobalTransactional
public void purchase(String userId, String commodityCode, int count, int money) {
    jdbcTemplateA.update("update stock_tbl set count = count - ? where commodity_code = ?", new Object[] {count, commodityCode});
    jdbcTemplateB.update("update account_tbl set money = money - ? where user_id = ?", new Object[] {money, userId});
}

如果您曾使用过 Spring 框架 @Transactional 注解的话,也可以根据命名类比理解 @GlobalTransactional 的功能。是的,这里只是引入了一个注解就轻松实现了分布式事务能力,使用 AT 模式可以最小程度减少业务改造成本。

同时,需要注意的是,jdbcTemplateAjdbcTemplateB 使用了不同的数据源进行构造,而这两个不同的数据源都需要使用 Seata 提供的 AT 数据源代理类 DataSourceProxy 进行包装。

5.7.2Seata TCC 模式

TCC 模式是 Seata 支持的一种由业务方细粒度控制的侵入式分布式事务解决方案,是继 AT 模式后第二种支持的事务模式,最早由蚂蚁金服贡献。其分布式事务模型直接作用于服务层,不依赖底层数据库,可以灵活选择业务资源的锁定粒度,减少资源锁持有时间,可扩展性好,可以说是为独立部署的 SOA 服务而设计的。

在这里插入图片描述

5.7.3Seata Saga 模式

Saga 模式是 SEATA 提供的长事务解决方案,在 Saga 模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。

在这里插入图片描述

适用场景:
  • 业务流程长、业务流程多
  • 参与者包含其它公司或遗留系统服务,无法提供 TCC 模式要求的三个接口
优势:
  • 一阶段提交本地事务,无锁,高性能
  • 事件驱动架构,参与者可异步执行,高吞吐
  • 补偿服务易于实现
缺点:
  • 不保证隔离性(应对方案见后面文档)
Saga 的实现:
基于状态机引擎的 Saga 实现:

目前 SEATA 提供的 Saga 模式是基于状态机引擎来实现的,机制是:

  1. 通过状态图来定义服务调用的流程并生成 json 状态语言定义文件

  2. 状态图中一个节点可以是调用一个服务,节点可以配置它的补偿节点

  3. 状态图 json 由状态机引擎驱动执行,当出现异常时状态引擎反向执行已成功节点对应的补偿节点将事务回滚

    注意: 异常发生时是否进行补偿也可由用户自定义决定

  4. 可以实现服务编排需求,支持单项选择、并发、子流程、参数转换、参数映射、服务执行状态判断、异常捕获等功能

5.7.4Seata XA 模式

XA 规范 是 X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准。Seata XA 模式是利用事务资源(数据库、消息服务等)对 XA 协议的支持,以 XA 协议的机制来管理分支事务的一种事务模式。

在这里插入图片描述

优势

与 Seata 支持的其它事务模式不同,XA 协议要求事务资源本身提供对规范和协议的支持,所以事务资源(如数据库)可以保障从任意视角对数据的访问有效隔离,满足全局数据一致性。此外的一些优势还包括:

  1. 业务无侵入:和 AT 一样,XA 模式将是业务无侵入的,不给应用设计和开发带来额外负担。
  2. 数据库的支持广泛:XA 协议被主流关系型数据库广泛支持,不需要额外的适配即可使用。
缺点

XA prepare 后,分支事务进入阻塞阶段,收到 XA commit 或 XA rollback 前必须阻塞等待。事务资源长时间得不到释放,锁定周期长,而且在应用层上面无法干预,性能差。

适用场景

适用于想要迁移到 Seata 平台基于 XA 协议的老应用,使用 XA 模式将更平滑,还有 AT 模式未适配的数据库应用。

整体机制
  • 执行阶段:
    • 可回滚:业务 SQL 操作放在 XA 分支中进行,由资源对 XA 协议的支持来保证 可回滚
    • 持久化:XA 分支完成后,执行 XA prepare,同样,由资源对 XA 协议的支持来保证 持久化(即,之后任何意外都不会造成无法回滚的情况)
  • 完成阶段:
    • 分支提交:执行 XA 分支的 commit
    • 分支回滚:执行 XA 分支的 rollback
基本使用

XA 模式使用起来与 AT 模式基本一致,用法上的唯一区别在于数据源代理的替换:使用 DataSourceProxyXA 来替代 DataSourceProxy

public class DataSourceProxy {
    @Bean("dataSourceProxy")
    public DataSource dataSource(DruidDataSource druidDataSource) {
        // DataSourceProxyXA for XA mode
        return new DataSourceProxyXA(druidDataSource);
        // DataSourceProxy for AT mode
        // return new DataSourceProxy(druidDataSource);
    }
}

为独立部署的 SOA 服务而设计的。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值