SpringCloud企业级常用框架整合--下篇

发布于:2025-04-18 ⋅ 阅读:(22) ⋅ 点赞:(0)

Sentinel

在微服务板块中间,服务与服务连接紧密,可能某一条链路上的一个服务出现故障,就会导致整个大型服务出现故障,产生雪崩式效应。所以Sentinel就是用来进行服务保护的框架。维持服务之间的稳定性。

服务保护

要想使用好Sentinel这个服务保护框架,就要掌握如何定义资源和如何定义规则(对资源的保护规则)

===================pom依赖================================================================
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
#=================配置连接Sentinel可视化界面================================================
spring:
  cloud:
    sentinel:
      transport:
        dashboard: localhost:8080

其具体的工作流程如下所示:

异常处理

Web接口底层:

底层使用SentinelWebInterceptor拦截器机制(SpringMVC中的拦截器间接的实现HandlerInterceptor),以下为底层源码分析,从上到下依次是方法层层剥离。总而言之,SentinelWebInterceptor拦截器拦截了指定的请求,并在执行目标方法前开启了资源保护,经过规则校验,如果违背了规则那么,就会抛出BlockException的异常,此时就会找在容器里面是否有处理阻塞异常(BlockException)的方法BlockExceptionHandler(这是一个接口,可以自定义实现),如果没有就会取出(默认阻塞异常处理方法)DefaultBlockExceptionHandler,实现其中的代码逻辑。

//==============WEB接口自定义异常处理======================================================
@Component
public class MyBlockException implements BlockExceptionHandler {
​
    private static final ObjectMapper objectMapper = new ObjectMapper();
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, String s, BlockException e) throws Exception {
        PrintWriter writer = httpServletResponse.getWriter();
        //设置编码格式
        httpServletResponse.setContentType("application/json;charset=utf-8");
        //设置错误信息
        R error = R.error(s + "被Sentinel限制了,原因是:" + e.getMessage(), 500);
        //将错误信息转换为json字符串,并写出
        writer.write(objectMapper.writeValueAsString(error));
        //刷新缓冲区,关闭流
        writer.flush();
        writer.close();
    }
}

@SentinelResource底层原理:

底层使用SentinelResourceAspect切面进行,进行资源保护。以下为底层源码分析,从上到下依次是方法层层剥离。总而言之,在执行目标方法之前,通过环绕通知开启资源保护,经过规则校验,如果违背了规则那么,就会抛出BlockException的异常,此时就要看@SentinelResource注解上是否声明有blockHandler方法,有就会执行其中的异常处理代码,没有就会去寻找fallback方法。相同的,依旧可以从注解中查看是否声明有fallback方法,没有fallback方法就会使用系统默认的fallback方法,如果系统默认的fallback方法都没有就会向上抛出异常给SpringBoot自己处理

//===================@SentinelResource声明资源的自定义异常处理===============================
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
​
    @Resource
    private ProductFeigin productFeigin;
​
​
    //这个注解一般不在controller上使用,而是在业务层使用。保护业务层远程调用成功时有数据返回,失败时有兜底方法。
    @SentinelResource(value = "创建订单", blockHandler = "OrderBlockHandler")
    @Override
    public Order createOrder(Long userId, Long productId) {
        Order order = new Order();
        //Product product = getProductByIdWithLoadBalancerAnnotation(productId);
        //使用Feign客户端远程调用商品服务查询商品详情
        Product product = productFeigin.queryProductById(productId);
        order.setId(1l);
        //TODO:查询商品单价,数量来计算总价
        BigDecimal price = product.getPrice();
        int num = product.getNum();
        order.setTotalAmount(new BigDecimal(num).multiply(price));
        order.setUserId(userId);
        order.setNickName("胡尔摩斯");
        order.setAddress("贝克街");
        //TODO:跟据商品id查询商品详情列表
        order.setProductList(Arrays.asList(product));
        return order;
    }
​
​
    //兜底方法
    public Order OrderBlockHandler(Long userId, Long productId, BlockException e) {
        Order order = new Order();
        order.setId(1l);
        order.setTotalAmount(new BigDecimal(0));
        order.setUserId(userId);
        order.setNickName("未知用户");
        order.setAddress("未知地址"+e.getClass());
        order.setProductList(null);
        return order;
    }
​
}
​

openFeigin调用底层原理:

底层使用了feignSentinelBuilder构造器,整合了Sentinel与Feigin。以下为底层源码分析,从上到下依次是方法层层剥离。总而言之,在创建Feigin接口的实例时,会先获取到FeiginClient上标注的fallback方法,将其进行封装后开启资源保护,如果没有违反规则则利用反射执行目标方法,违反规则后,校验fallback是否存在,存在就会执行,不存在就继续向上抛出。

//====================Feigin远程调用自定义异常处理===========================================
@FeignClient(value = "service-product" ,fallback = ProductFeiginFallBack.class)
public interface ProductFeigin {
​
    //声明远程调用的方法,
    // 技巧:可以直接从需要远程调用的微服务的controller中拷贝其方法签名
​
​
    //MVC中的Mapping注解有两种用法
    //1在MVC框架里面,表示的是接收一个对应的请求路径
    //2.在Feign框架里面,表示的是远程调用的请求路径(发送请求的路径)
    //注意:Feigin是自带负载均衡的。不需要再进行负载均衡的配置了。
​
    @GetMapping("/product/{id}")
    public Product queryProductById(@PathVariable Long id);
​
}
​
​
//=================自定义兜底方法,放入IOC容器才能生效=========================================
@Component
public class ProductFeiginFallBack implements ProductFeigin {
    @Override
    public Product queryProductById(Long id) {
        Product product = new Product();
        product.setId(id);
        product.setPrice(BigDecimal.valueOf(0.0));
        product.setProductName("未知商品");
        product.setNum(0);
        return product;
    }
}

原生Sphu硬编码:

流控规则

  • 阈值类型

  • 流控模式

#取消web上下文统一
spring.cloud.sentinel.web-context-unify=false
@RefreshScope//刷新配置
@RestController
public class OrderController {
​
    @Autowired
    private OrderService orderService;
​
    //创建订单
    @GetMapping("/create")
    public Order createOrder(@RequestParam("userId") Long userId, @RequestParam("productId") Long productId) {
        Order order = orderService.createOrder(userId, productId);
        return order;
    }
​
    @GetMapping("/secdkill")
    public Order secdkillOrder(@RequestParam("userId") Long userId, @RequestParam("productId") Long productId) {
        Order order = orderService.createOrder(userId, productId);
        order.setId(Long.MAX_VALUE);
        return order;
    }
​
}
//=========================设置流控资源A,作用在流控资源B上====================================
@RefreshScope//刷新配置
@RestController
public class OrderController {
​
    @GetMapping("/readDB")
    public String readDB(){
        return "readDB,成功从数据库中读取数据";
    }
​
    @GetMapping("/writeDB")
    public String writeDB(){
        return "writeDB,成功向数据库中写入数据";
    }
​
}
  • 流控效果

注意:只有快速失败支持流控模式(直接,关联,链路)的设置。流控规则可以设置在请求的各个路径上,按需配置,选择多样。

熔断规则

熔断降级作为保护自身的手段,通常在客户端(调用端)进行配置。配置在需要进行远程调用的类上。

作用:

  1. 切断不稳定的调用

  2. 快速返回不积压

  3. 避免服务雪崩出现

有熔断VS无熔断

在无熔断规则时,每一次的远程调用。都会发送出去,经过判断超时或者异常错误时,才会执行兜底fallback方法。而有熔断规则时,触发熔断后,在一定的熔断时间内就不会再进行远程调用,直接使用兜底fallback方法,减少调用资源的消耗,快速返回结果,避免服务雪崩发生。还有一点需要注意的是,在配置熔断时,是在远程调用处设置熔断以及相关规则。这一点要和流控设置区分开来。

热点规则

对于经常访问的数据我们称之为热点,热点参数限流会统计出传入参数中的热点参数,对包含热点参数的资源调用进行限流操作。热点参数限流,可以看作是一种特殊的流控规则,仅对包含热点参数的资源生效。

@RefreshScope//刷新配置
@RestController
public class OrderController {
​
​
    @SentinelResource(value = "秒杀订单",fallback = "secdKillHandleException")
    @GetMapping("/secdkill")
    public Order secdkillOrder(@RequestParam("userId") Long userId, @RequestParam("productId") Long productId) {
        Order order = orderService.createOrder(userId, productId);
        order.setId(Long.MAX_VALUE);
        return order;
    }
​
    public Order secdKillHandleException(Long userId, Long productId,Throwable e){
        System.out.println("secdkill...兜底方法执行了");
        Order order = new Order();
        order.setUserId(userId);
        order.setId((productId));
        order.setAddress("异常信息:"+e.getMessage());
        return order;
    }
}

blockHandler与fallback的区别

  • fallback属性指定了一个降级方法,当资源发生异常(包括Sentinel定义的异常和业务代码抛出的异常)时,会调用该方法。任何类型的异常,无论是Sentinel定义的异常(如限流异常、降级异常)还是业务代码中的异常,都会触发fallback方法。

  • blockHandler属性指定了一个限流处理逻辑,当资源被限流时会调用该方法。仅当资源访问被Sentinel限流时会触发blockHandler方法。

Gateway-网关

网关可以作为所有请求的入口,这样就可以很好的隐藏微服务所在的服务器,不需要向外暴露其IP地址和端口信息,具有一定的安全性。

路由

=========================导入相关依赖======================================================
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
server:
  port: 80
​
spring:
  application:
    name: gateway
  cloud:
    nacos:
      server-addr: 127.0.0.1:8848
    gateway:
      routes:
        - id: service-order            #路由的id,自定义,只要唯一即可
          uri: lb://service-order      # 路由的目标地址,lb表示启用负载均衡,后面跟上服务名称
          predicates:                  # 路由断言,判断请求是否符合路由规则的条件
            - Path=api/order/**        # 这个是按照路径匹配,要求请求的路径符合该模式才可以路由
        - id: service-product
          uri: lb://service-product
          predicates:
            - Path=api/product/**

配置的路由规则都是有序的,如果没有声明order参数值(数值越小优先路由的优先级越大),就会按照路由的声明顺序来确定优先级的。当请求满足某一个路由之后就不会再使用后面的路由了。

断言

#===============================长/短断言写法==============================================
spring:
  application:
    name: gateway
  cloud:
    nacos:
      server-addr: 127.0.0.1:8848
    gateway:
      routes:
        - id: service-user
          uri: lb://service-user
          predicates:
            - Path=api/user/**
        - id: service-user
          uri: lb://service-user
          predicates:
            - name: path
              args:
                pattern: /api/user/**
                matchTrailiingSlash: true # 匹配尾部的斜杠, 比如 /api/user/ 和 /api/user 都能匹配

自定义断言工厂

首先要继承于AbstractRoutePredicateFactory(抽象的断言工厂),再进行底层逻辑的实现。关键在于有类部类的属性,就是断言的配置属性类。如果有短形式的配置要求,就需要定义参数的对应顺序,实现shortcutFieldOrder方法。最后断言逻辑再apply方法中实现即可。切记要将自定义的断言工厂放入容器才能生效。

@Component
public class VipRoutePredicateFactory extends AbstractRoutePredicateFactory<VipRoutePredicateFactory.Config> {
​
​
    // 空参构造方法,调用父类的空参构造 传入配置类的class对象
    public VipRoutePredicateFactory() {
        super(Config.class);
    }
​
​
    //指定配置字段的顺序或优先级。短形式的属性顺序,对应者配置文件中的声明顺序
    public List<String> shortcutFieldOrder() {
        return Arrays.asList("param", "value");
    }
​
​
    /*
    自定义断言逻辑的实现
     */
    @Override
    public Predicate<ServerWebExchange> apply(Config config) {
​
        return new GatewayPredicate() {
            public boolean test(ServerWebExchange exchange) {
                //exchange里面封装了请求和响应对象
                ServerHttpRequest request = exchange.getRequest();
                String first = request.getQueryParams().getFirst(config.param);
​
                if (StringUtils.hasText(first) && first.equals(config.value)) {
                    return true;
                }
                
                return false;
            }
​
        };
    }
​
    /*
    * @description: 配置类,可以进行配置的参数
     */
    public static class Config {
​
        @NotEmpty
        private String param;
        @NotEmpty
        private String value;
​
        public @NotEmpty String getValue() {
            return value;
        }
​
        public void setValue(@NotEmpty String value) {
            this.value = value;
        }
​
        public @NotEmpty String getParam() {
            return param;
        }
​
        public void setParam(@NotEmpty String param) {
            this.param = param;
        }
    }
}

server:
  port: 80
​
spring:
  application:
    name: gateway
  cloud:
    nacos:
      server-addr: 127.0.0.1:8848
    gateway:
      routes:
        - id: order-router             
          uri: lb://service-order             
          predicates:
            - Vip=name, hulmos

过滤器

RewritePath过滤器:

进行请求路径的重写,动态的改写请求路径的相关信息,使用正则表达式,动态的取出想要复用的路径。

AddRequestHeader过滤器:

给请求头添加一些信息,在请求发出之前可以添加一些信息,携带在请求头里面。

default-filters默认过滤器:

可以给路由都设置一个默认的过滤器,这个过滤器会在所有的路由规则上生效。

server:
  port: 80
​
spring:
  application:
    name: gateway
  cloud:
    nacos:
      server-addr: 127.0.0.1:8848
    gateway:
      default-filters:
        - AddRequestHeader=X-Request-name, hulmos  # 默认过滤器,给所有路由添加请求头
      routes:
        - id: order-router             #路由的id,自定义,只要唯一即可
          uri: lb://service-order      # 路由的目标地址,lb表示启用负载均衡,后面跟上服务名称
          predicates:                  # 路由断言,判断请求是否符合路由规则的条件
            - Path=api/order/**        # 这个是按照路径匹配,要求请求的路径符合该模式才可以路由
          filters:                      # 路由过滤器,对请求或响应进行一些额外的处理
            - RewritePath=/api/order/(?<segment>.*), /$\{segment}  # 请求路径重写,去掉前面的api/order
            - AddRequestHeader=X-Request-abc, 123  # 给请求头添加一个名为X-Request-abc的header,值为123

全局Filiter定义,拦截所有的请求。

自定义全局过滤器可以自命名为xxxGlobalFilter,要进行代码实现就要完成以下的操作。首先要实现GlobalFilter接口在重写的方法中完成,自己过滤逻辑(定义前置后置的操作)。实现Ordered接口,设置自定义全局过滤器的优先级别。以下代码为跟家详细的实现过程。

@Slf4j
@Component
public class RTGlobalFilter implements GlobalFilter, Ordered {
    @Override
    public int getOrder() {
        return 0;
    }
​
    /**
     * 过滤器方法
     * @param exchange 交换对象,里面封装了请求和响应信息
     * @param chain 过滤器链,用于传递请求和响应
     * @return      Mono<Void> 响应结果
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
​
        // 获取请求和响应信息
        log.info("获取请求和响应信息");
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
        // 获取请求的URI,并将其转换为字符串
        String uri = request.getURI().toString();
        // 记录请求的开始时间
        long start = System.currentTimeMillis();
        log.info("请求【{}】开始,时间:{}",uri,start );
​
        //==========================以上是过滤器的前置处理==========================
​
        // 执行过滤器链,并将请求和响应信息传递给下一个过滤器
        //这是一个异步放行操作,所以后置处理需要使用Mono<Void>的返回值,如果直接在下面写后置处理,就不会达到预期效果
        Mono<Void> mono = chain.filter(exchange)
                .doFinally(
                        (result) -> {
                            // 记录请求的结束时间
                            long end = System.currentTimeMillis();
                            log.info("请求【{}】结束,时间:{}",uri,end);
                            // 计算并记录请求的耗时
                            Long duration = (end - start);
                            log.info("请求【{}】耗时:{}ms", uri,duration);
                        }
                );
​
        return mono;
    }
}

自定义过滤器工厂

由于系统中给我们提供了许多过滤器,但是这些过滤器还是满足不了我们个性化的需求,所以我们就可以进行自定义过滤器工厂的编写。

@Component
public class OnceTokenGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {
    @Override
    public GatewayFilter apply(NameValueConfig config) {
        return new GatewayFilter() {
            @Override
            public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                //在每次响应之前,都在响应头里面添加一个令牌。支持UUID,JWT等格式
                return chain.filter(exchange)
                        .then(Mono.fromRunnable(() -> {
                            //链式调用,获取响应信息,获取响应头信息
                            HttpHeaders headers = exchange.getResponse().getHeaders();
​
                            //在响应头里面添加令牌
                            //①当前配置的令牌是UUID,则在响应头里面添加一个随机的UUID
                            String value = config.getValue();
                            if(value.equals("UUID")){
                                value = UUID.randomUUID().toString();
                            }
​
                            //②当前配置的令牌是JWT,则在响应头里面添加一个固定的字符串"JWT令牌"
                            if(value.equals("JWT")){
                                value = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ";
                            }
                            headers.add(config.getName(),value);
                        }));
            }
        };
    }
}
​
server:
  port: 80
​
spring:
  application:
    name: gateway
  cloud:
    nacos:
      server-addr: 127.0.0.1:8848
    gateway:
      routes:
        - id: order-router             
          uri: lb://service-order             
          filters:                     
            - OnceToken=X-Response-Token, JWT

全局跨域

以下配置表达的是:允许所有的跨域,允许所有的头,允许所有的请求方式

经典面试题:微服务之间的调用经过网关吗?

答:可以使用Openfein远程调用网关的微服务,再路由到指定的微服务,达到微服务调用经过网关的效果。但是这样做性能不好,微服务调用过于繁琐,所以在实际的代码开发的过程中,一般微服务调用不会经过网关。

Seata

分布式事务,在为微服务的生态架构里面。每一个微服务可能都会有其专门操作的数据库表,在本地事务里面,对某一个数据库操作的报错问题有回滚策略的实现。但是微服务之间相互调用,分别完成对不同数据库的操作,本地事务无法对着一连串的数据库操作具有回滚约束性了。此时就需要有专门的框架来把分布式的事务统一管理起来,可以理解为将这些事务串成一个大事务,来满足对事务回滚的需要。

原理:

Seata 的核心组件

  • TC (Transaction Coordinator): 事务协调者,维护全局和分支事务的状态,驱动全局事务提交或回滚。--Seata服务器

  • TM (Transaction Manager): 事务管理器,定义全局事务的边界,申请开启、提交或回滚全局事务。--Seata客户端

  • RM (Resource Manager): 资源管理器,管理分支事务处理的资源,与 TC 通讯以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。--Seata客户端

//=================================导入pom依赖=====================================
<dependency>
  <groupId>com.alibaba.cloud</groupId>
  <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>

每个微服务创建 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 服务端的基本设置,包括事务服务组的映射、服务器列表、服务降级和全局事务功能的启用状态。在实际应用中,您可能需要根据具体的业务需求和网络环境对这些设置进行调整。同时,请注意,Seata 的配置可能随着版本的更新而发生变化,因此建议参考官方文档或最新版本的配置说明进行配置。

二阶提交协议

第一阶段:

分支事务首先会解析业务SQL语句,查询前镜像(即还未执行业务SQL的数据),然后执行业务SQL对数据进行操作后,跟据第一次查询前镜像拿到的id值精确查询后镜像(即执行业务SQL后的数据)。给undo_log表中插入前后镜像组成的回滚日志。注册分支事务,为了防止其它人对目标数据库的操作,还会在申请目标表的全局锁后进行本地事务的提交(业务数据+undo_log一起保存)。最后就可以向Seata服务器汇报自己提交成功与否。其它分支事务也都会按照第一阶段来进行操作。

第二阶段:

  • 分支提交:如果所有的分支事务都成功了,TC感知到了所有分支事务的状态。TC就会通知所有的微服务告诉它们可以提交分支事务了,完成后会立即响应OK。然后TC会给异步任务队列中添加异步任务,异步和批量的删除相应的UNDO_LOG记录。

  • 分支回滚:如果某一个分支事务炸了,TC感知到后就会通知所有的分支事务,让他们进行数据的回滚操作。收到TC回滚请求后会开启一个本地事务,此事务任务如下:①找到对应的undo_log记录(通过全局事务ID“XID”,分支事务ID“BranchID”)②进行数据校验(检查后镜像与当前数据是否一致),如果校验不通过,说明数据被通过其它渠道进行了修改,需要配置对应的策略来处理。如果校验通过,说明一切都正常,只差数据回滚了。③回滚数据,获取undo_log前镜像内容,执行修改,完成后删除undo_log。

四种事务模式:

Seata 提供了 AT、TCC、SAGA 和 XA 四种事务模式,以适应不同的业务场景和需求。

  • AT 模式(Automatic Transaction):基于支持本地 ACID 事务的关系型数据库,通过代理数据源的方式,拦截 SQL 操作,自动补偿回滚,实现分布式事务。

  • TCC 模式(Try-Confirm-Cancel):应用层侵入业务代码,通过定义 Try、Confirm 和 Cancel 三个操作,由框架调用以完成分支事务的提交或回滚。

  • SAGA 模式:通过事件驱动的方式,补偿服务在接收到事件后进行相应的业务处理,实现长事务的最终一致性。

  • XA 模式:基于 XA 协议的两阶段提交,通过数据库支持的 XA 事务来实现分布式事务。


网站公告

今日签到

点亮在社区的每一天
去签到