微服务保护(Sentinel、Seata)

462 阅读22分钟

Sentinel

雪崩问题

服务D出现了故障,服务A一直请求服务D,当tomcat资源耗尽,服务A也出现了问题

雪崩就是某个服务出现故障,导致整个微服务都不可用

image.png 解决方案1:超时处理

设定超时时间,如果超过一定的时间就返回错误信息,不会无休止等待

解决方案2:舱壁模式

限定每个业务能使用的线程数,避免消耗整个tomcat的资源, 也叫线程隔离

解决方案3:熔断降级

由断路器统计业务的执行异常比例
如果超出阈值则会熔断该业务,拦截访问该业务的一切请求(比较好)

解决方案4:流量控制

限制每秒钟处理请求的数量,限制访问的OPS,避免服务因为流量徒增而故障

  1. 如何避免因瞬间高并发流量导致的服务故障
    • 流量控制
  2. 如何避免因服务故障引起的雪崩问题?
    • 超时处理
    • 线程隔离
    • 降级熔断

image.png

特点:完备的实时监控,广泛的开源生态

启动Sentinel

下载Jar包,在此处打开cmd, 输入指令

java -jar sentinel-dashboard-1.8.1.jar

默认的账户和密码都是sentinel

更换端口举例:

java -jar sentinel-dashboard-1.8.1.jar -Dserver.port=8090

微服务整合sentinel

注册中心和网关启动后

步骤1:引入依赖(服务加)

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

配置Sentinel信息

spring:
  cloud:
    sentinel:
      transport:
        dashboard: localhost:8080 # sentinel 地址

如果报错: Relying upon circular references is discouraged and they are prohibited by default.

原因:因为 sentinel 循环依赖 springboot2.6.2的冲突,依赖循环引用

spring:
    main:
      allow-circular-references: true

image.png

限流规则(簇点链路)

簇点链路:就是项目内的调用链路(就是先访问Controller,然后交给Service,然后Mapper,形成的链路)

Sentinel 默认会监控 springMVC 的每个断点(EndPoint)(资源)

下面就是给每个资源进行资源流控 image.png

新增资源流控

image.png

  • QPS是并发量,后面点击阈值设置为1,表示每秒只能允许一个请求

关联限流

就是B触发了限流,对A进行限流

当write请求到达阈值时,对update限流

image.png

使用条件:

  • 两个竞争关系的资源
  • 一个优先级高,一个优先级低的

链路限流

只计算从 test进入update的请求

image.png

200 / 50 = 4 每秒4个 image.png

下面两个方法都会调用 queryOrder 方法

@RestController
@RequestMapping("/order")
public class OrderController {

    @Resource
    private OrderServiceImpl orderService;


    @GetMapping("/update")
    public String update(){
        orderService.queryOrder();
        return "更新成功";
    }

    @GetMapping("/query")
    public String query(){
        orderService.queryOrder();
        return "查询成功";
    }
}

想想,service层的方法被监控了吗?没有吧!那显然不可以配置规则

Sentinel 默认只标记Controller中的方法作为资源,要标记其他方法需要

@Service
public class OrderServiceImpl {
    @SentinelResource("goods")
    public void queryOrder(){
        System.err.println("查询成功!");
    }
}

但是 Sentinel 默认会将Controller做Context整合,导致链路流控失效,需要修改配置

spring:
  cloud:
    sentinel:
      transport:
        dashboard: localhost:8080 # sentinel 地址
      web-context-unify: false # 关闭context整合

流控效果

就是当达到阈值时,采取的措施

快速失败:直接抛出异常

Warm up:预热模式,当抛出异常,阈值逐渐增大

排队等待:让所有请求依次等待,依次执行,两个请求不能小于指定等待时长。

预热模式

预热5秒,最大数量十个,逐渐每秒最大数量 image.png

细节:请求阈值初始值 = OPS(阈值) / coldFactor

coldFactor 默认是3

排队等候

后面请求会等待前面请求结束后执行,如果超时会抛出异常

预期等待:200ms,最大2000ms,如果超出直接抛出异常 image.png

热点参数限流

是更细粒度的限流

注意:热点参数限流对默认的SpringMVC资源无效,需要加上@SentinelResource("hot")

@SentinelResource("hot")
@GetMapping("/{orderId}")
public Order queryOrderById(@PathVariable Integer orderId, @RequestHeader(value = "Truth",required = false) String truth) {
    return order;
}

给/order/{orderId}这个资源添加热点参数限流,规则如下:
默认的热点参数规则是每1秒请求量不超过2
给102这个参数设置例外:每1秒请求量不超过4
给103这个参数设置例外:每1秒请求量不超过10

image.png

注意:类型要一致,long与int不一样

总结:根据线程数量 / 总秒数
单机阈值 和 1

隔离和降级

FeignClient整合Sentinel

限流可以尽量避免高并发,但是还会有问题,要靠线程隔离或者熔断降级

开启feign的Sentinel功能

feign:
  sentinel:
    enabled: true # 开启feign的sentinel功能

给feignClient编写失败后的逻辑

  • 方式一:FallbackClass,无法对远程调用的异常做处理
  • 方式二:FallbackFactory,可以对远程调用的异常做处理,我们选择这种

首先创建类实现FallbackFactory(这是公共的)

package com.feign.fallback;

import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.openfeign.FallbackFactory;

@Slf4j
public class UserClientFallbackFactory implements FallbackFactory<UserClient> {
    @Override
    public UserClient create(Throwable cause) {
        return new UserClient() {
            @Override
            public User findById(Integer userId) {
                log.error("查询用户异常!",cause);
                return new User();
            }
        };
    }
}

将上面对象声明成 bean,要在导入的类里面声明

@Bean
public UserClientFallbackFactory userClientFallbackFactory(){
    return new UserClientFallbackFactory();
}

在之前的客户端上加

@FeignClient(value = "user1service1", fallbackFactory = UserClientFallbackFactory.class)
public interface UserClient {

Order表主启动类

@EnableFeignClients(clients = UserClient.class, defaultConfiguration = DefaultFeignConfiguration.class)
public class OrderApplication {

遇到错误: Unexpected exception during bean creation; nested exception is java.lang.IllegalStateException: Incompatible fallbackFactory instance. Fallback/fallbackFactory of type class com.feign.fallback.UserClientFallbackFactory is not assignable to interface feign.hystrix.FallbackFactory for feign client user1service1

原因:因为导包岛错了 FallbackFactory的包应该导入这个

import feign.hystrix.FallbackFactory;

@Slf4j
public class UserClientFallbackFactory implements FallbackFactory<UserClient> {

会发现这里的地址已经是Get开头了 image.png

线程隔离两种方式

信号量隔离(Sentinel默认采用)

  • 服务A 依赖服务B、服务C,当请求来的时候
  • 让从N个池子里取线程,再分别调用Feign的客户端发起远程调用

image.png

信号量隔离 请求的时候,计数器会减1,如果总共信号量是10,超过10会直接拒绝,当然完成一个会减少一个

线程池隔离
优点缺点
支持主动超时线程的额外开销比较大
支持异步调用

场景:低扇出,就是一个服务依赖其他服务比较少,不适合高扇出

信号量隔离
优点缺点
轻量级,无额外开销不支持主动超时
不支持异步调用

场景:高频调用,高扇出

实现线程隔离 这就是配置信号量最大值隔离

image.png

熔断降级

统计异常比例,如果超出阈值,则会拦截服务一切请求。
当服务恢复时会放行该服务。

  • closed:达到失败阈值,自动转Open快速失败
  • Open:有个熔断时间,当熔断时间结束转Half-Open
  • Half-Open:尝试放一次请求,根据返回结果,转Closed或Open image.png

熔断策略

慢调用

业务响应时间过长,超过指定时长,触发熔断

  • 最大RT:超过这个时间都算慢调用
  • 比例阈值:达到设定时间触发熔断
  • 熔断时长:超过这个时间后,进入Half-Open
  • 最小请求数:在统计时长内统计N次达到最大RT的,达到比例阈值,触发熔断,熔断时间开始
  • 统计时长:统计多少时间内的 image.png

异常比例或异常数

  • 指定时间内调用次数超过指定请求数
  • 并且异常比例达到设定的阈值(或者超过指定异常数),触发熔断

授权规则(只允许从网关来的请求)

白名单:来源(origin)在白名单内的调用者允许访问
黑名单:来源(origin)在黑名单内的调用者不允许访问

只允许从网关来的请求

步骤1:判断是否从网关发起的请求

@Component
public class HeaderOriginParser implements RequestOriginParser {
    // 1. 创建 sentinel包下 HeaderOriginParser 实现 RequestOriginParser
    @Override
    public String parseOrigin(HttpServletRequest request) {
        // 1. 获取请求头
        String origin = request.getHeader("origin");
        if(StringUtils.isEmpty(origin)){
            //设置默值
            return origin = "blank";
        }
        return origin;
    }
}

步骤2:给网关过来的请求新增请求头

spring:
  application:
    name: gateway # 服务名称
  cloud:
    nacos:
      server-addr: localhost:8845 # nacos地址
    gateway:
      routes: # 网关路由配置
        - id: user-service # 路由id,自定义,只要唯一即可
          # uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址
          uri: lb://user1service1 # 路由的目标地址 lb就是负载均衡,后面跟服务名称
          predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
            - Path=/user/** # 这个是按照路径匹配,只要以/user/开头就符合要求
        - id: order-service
          uri: lb://order1service
          predicates:
            - Path=/order/**
      # 添加过滤条件,新增请求头
      default-filters:
        - AddRequestHeader=Truth,Itcast is freaking aowsome!
        - AddRequestHeader=origin,gateway

步骤3:添加授权规则

image.png

自定义异常结果

image.png

@Component
public class SentinelExceptionHandler implements BlockExceptionHandler {
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse response, BlockException e) throws Exception {
        String msg = "未知异常";
        int status = 429; //限流都是429
        if(e instanceof FlowException){
            msg = "请求被限流";
        }else if(e instanceof DegradeException){
            msg = "请求被降级";
        }else if(e instanceof ParamFlowException){
            msg = "热点参数限流";
        }else if(e instanceof AuthorityException){
            msg = "请求没有权限";
            status = 401;
        }
        //设置相应类型为JSON和编码设置
        response.setContentType("application/json;charset=utf-8");
        // 状态码
        response.setStatus(status);
        // 拼接JSON
        response.getWriter().println("{"message": "" + msg + "", "status": " + status + "}");
    }
}

规则持久化

重启服务,配置的规则都丢失了,肯定不能容忍,因为保存到了内存里

持久化规则

规则管理模式
原始模式Sentinel的默认模式,将规则保存在内存,重启丢失
pull模式

pull模式

  • 当我们编写规则时,会推送给Sentinel Dashboard 客户端
  • 会更新内存中的规则,并且持久化到数据库中
  • 如果另外的服务,也需要这个规则
    • 微服务就会定时轮询这个数据库
    • 监听到数据库或者文件内容发生变化,就知道规则更新

缺点:时效性太差,会导致数据不一致

push模式(推荐)

  • push模式不会把规则推送给任何一个客户端
  • 而是将配置推送到远程配置中心,例如Nacos
  • 微服务去监听Nacos,发现变化,立即更新

实现Push模式

push模式实现最为复杂,依赖于nacos,并且需要改在Sentinel控制台源码。

步骤1:引入依赖

<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-nacos</artifactId>
</dependency>

步骤2:配置Nacos地址

spring:
 application:
   name: order1service
 profiles:
   active: dev
 cloud:
   nacos:
     discovery:
       server-addr: localhost:8845
     config:
       file-extension: yaml
       enabled: false
   sentinel:
     datasource:
       flow:
         nacos:
           server-addr: localhost:8845 # nacos地址
           dataId: order1service-flow-rules
           groupId: SENTINEL_GROUP
           rule-type: flow # 还可以是:degrade、authority、param-flow
       degrade: #配置降级,这里不加
         nacos:
           server-addr: localhost:8845 # nacos地址
           dataId: order1service-degrade-rules
           groupId: SENTINEL_GROUP
           rule-type: degrade # 还可以是:degrade、authority、param-flow

分布式事务(Seata)

事务必须满足,ACID

一个微服务,可以多个事务同时

每个服务都是独立的,所以并不知道其他服务抛出异常了,不会回滚

分布式事务就是要保证所有分支事务最终状态一致

CAP:一致性,可用性,分区容错性

CAP
Consistency(一致性)
Availability(可用性)
Partition tolerance (分区容错性)

Eric Brewer 说,分布式系统无法同时满足这三个指标。
这个结论就叫做 CAP 定理。

一致性:用户访问分布式的任意节点,得到的数据必须一致

用户修改任意节点的数据,就必须实现节点数据的同步

可用性:用户访问集群中任意健康节点,必须得到回应,不能超时或者拒绝

Partition 分区:因为网络故障或其他原因,导致分布式系统部分节点与其他节点失去联系,形成独立分区
Tolerance(容错):在集群出现分区时,整个系统也要持续对外提供服务

简述CAP理论

分布式系统节点通过网络连接,一定会出现分区问题(P)
当出现分区问题,系统的一致性和可用性就无法同时满足

思考:elasticsearch集群是CP还是AP?
ES集群出现分区时,故障节点会被剔除集群,数据分片会重新分配到其它节点,保证数据一致。

  • 因此是低可用性,高一致性,属于CP

BASE理论

BASE理论是对CAP的一种解决思路,包含三个思想:

Basically Available (基本可用)

  • 分布式系统在出现故障时,允许损失部分可用性,即保证核心可用。

Soft State(软状态)

  • 在一定时间内,允许出现中间状态,比如临时的不一致状态。

Eventually Consistent(最终一致性)

  • 虽然无法保证强一致性,但是在软状态结束后,最终达到数据一致。

AP模式:

各子事务分别执行和提交,允许出现结果不一致,然后采用弥补措施恢复数据即可,实现最终一致。

CP模式:

CP模式:各个子事务执行后互相等待,同时提交,同时回滚,达成强一致。但事务等待过程中,处于弱可用状态。

image.png

初始Seata

TC:维护全局和分支事务的状态,协调全局事务提交或回滚。

TM:定义全局事务的范围、开始全局事务、提交或回滚全局事务。

RM:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

具体:

  • TC:TC检查分支事务的状态,全成功才可以提交。
  • TM:告诉TC,开始全局事务,调用分支事务,执行完毕,提高事务给TC
  • RM:代理分支事务,向TC注册当前分支事务,执行业务。报告分支事务给TC
不同分布式事务的解决方案
XA模式强一致性分阶段事务模式,牺牲了一定的可用性,无业务侵入
TCC模式最终一致的分阶段事务模式,有业务侵入
AT模式最终一致的分阶段事务模式,无业务侵入,也是Seata的默认模式
SAGA模式长事务模式,有业务侵入

部署TC服务(java 17)

配置Seata

打开 模板配置,直接修改面信息

seata:
  config:
    # support: nacos 、 consul 、 apollo 、 zk  、 etcd3
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8845
      namespace:
      group: DEFAULT_GROUP
      username: nacos
      password: nacos
      context-path:
      ##if use MSE Nacos with auth, mutex with username/password attribute
      #access-key:
      #secret-key:
      data-id: seataServer.properties #配置文件
  registry:
    # support: nacos 、 eureka 、 redis 、 zk  、 consul 、 etcd3 、 sofa
    type: file
    preferred-networks: 30.240.*
    nacos:
      application: seata-server
      server-addr: 127.0.0.1:8845
      group: DEFAULT_GROUP
      namespace:
      cluster: default # 集群
      username: nacos
      password: nacos
      context-path:
      ##if use MSE Nacos with auth, mutex with username/password attribute
      #access-key:
      #secret-key:

在Nacos 控制台中新增配置文件 seataServer.properties

信息下方

# 数据存储方式,db代表数据库
store.mode=db # 存储数据库
store.db.datasource=druid 
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true
store.db.user=root
store.db.password=123
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
# 下方都是默认的
# 事务、日志等配置
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.rollbackRetryTimeoutUnlockEnable=false
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000

# 客户端与服务端传输方式
transport.serialization=seata
transport.compressor=none
# 关闭metrics功能,提高性能
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898

在刚刚指定的架构中创建表


SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- 分支事务表
-- ----------------------------
DROP TABLE IF EXISTS `branch_table`;
CREATE TABLE `branch_table`  (
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `transaction_id` bigint(20) NULL DEFAULT NULL,
  `resource_group_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `resource_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `branch_type` varchar(8) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `status` tinyint(4) NULL DEFAULT NULL,
  `client_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `application_data` varchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `gmt_create` datetime(6) NULL DEFAULT NULL,
  `gmt_modified` datetime(6) NULL DEFAULT NULL,
  PRIMARY KEY (`branch_id`) USING BTREE,
  INDEX `idx_xid`(`xid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

-- ----------------------------
-- 全局事务表
-- ----------------------------
DROP TABLE IF EXISTS `global_table`;
CREATE TABLE `global_table`  (
  `xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `transaction_id` bigint(20) NULL DEFAULT NULL,
  `status` tinyint(4) NOT NULL,
  `application_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `transaction_service_group` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `transaction_name` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `timeout` int(11) NULL DEFAULT NULL,
  `begin_time` bigint(20) NULL DEFAULT NULL,
  `application_data` varchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `gmt_create` datetime NULL DEFAULT NULL,
  `gmt_modified` datetime NULL DEFAULT NULL,
  PRIMARY KEY (`xid`) USING BTREE,
  INDEX `idx_gmt_modified_status`(`gmt_modified`, `status`) USING BTREE,
  INDEX `idx_transaction_id`(`transaction_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

SET FOREIGN_KEY_CHECKS = 1;

部署TC服务(java 8)

打开config 里面 registry.conf

registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "nacos"
  # tc服务的注册中心类,这里选择nacos,也可以是eureka、zookeeper等
  nacos {
    application = "seata-server"
    serverAddr = "127.0.0.1:8845"
    group = "DEFAULT_GROUP"
    namespace = ""
    cluster = "default"
    username = "nacos"
    password = "nacos"
  }
}

config {
  # file、nacos 、apollo、zk、consul、etcd3
  type = "nacos"

  nacos {
    serverAddr = "127.0.0.1:8845"
    namespace = ""
    group = "DEFAULT_GROUP"
    username = "nacos"
    password = "nacos"
    dataId = "seataServer.properties"
  }
}

在Nacos 控制台中新增配置文件 seataServer.properties

表示安装成功

image.png

集成Seata

引入依赖(java 8)

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    <exclusions>
        <exclusion>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>1.4.2</version>
</dependency>

配置文件,会检查当前服务,需要设置GROUP

seata:
  registry:
    type: nacos
    nacos:
      server-addr: localhost:8845
#      namespace: ""
      group: DEFAULT_GROUP
      application: seata-server #这个名字要和 Seata 配置文件中一样
      username: nacos
      password: nacos
  tx-service-group: seata-demo # 项目名
  service:
    vgroup-mapping:
      seata-demo: SH

XA模式(强一致性)

DTP标准

image.png

seata的XA模式进行了调整

  • RM一阶段工作
    1. 注册事务到TC
    2. 执行分业务sql不提交
    3. 报告状态到TC
  • TC二阶段工作
    • TC检测分支事务执行状态
    1. 如果都成功,通知RM提交事务
    2. 如果有失败,通知所有RM回滚事务
  • RM二阶段工作
    1. 接收TC指令,提交或回滚事务
XA模式的优点是什么
事务的强一致,满足ACID原则
常用数据库都支持,实现简单,并且没有代码侵入
XA模式缺点是什么
因为一阶段需要锁定数据库,等到第二阶段结束才释放,性能较差
依赖关系性数据库实现事务

1:开启XA:每一个参与事务的微服务都要添加

开启XA模式

seata:
  data-source-proxy-mode: XA #开启数据源代理的XA模式

2:给发起全局事务的入口添加注解,替换以前的 Transactional

// 创建订单
@Override
@GlobalTransactional
public Integer insert(Order order) {
    // 扣除余额
    accountClient.updateAccount(order.getUserId(), order.getMoney().toString());
    // 扣除库存
    storageClient.updateCount(order.getCommodityCode(), order.getCount());
    return orderDao.insert(order);
}

AT模式(最终一致性)

解决XA模式,资源模式锁定过长

阶段一RM:

  • 注册分支事务
  • 记录undolog(数据快照)\color{green}{记录undo-log(数据快照)}
  • 执行业务sql并提交\color{green}{提交}
  • 报告事务状态

阶段二提交时RM的工作:

  • 删除undo-log即可 阶段二回滚时RM的工作:
  • 根据undo-log恢复数据到更新前

image.png

简述AT模式与XA模式最大的区别是什么?
XA模式一阶段不提交事务,锁定资源AT模式一阶段直接提交,不锁定资源
XA模式依赖数据库机制回滚AT模式利用数据快照实现数据回滚
XA模式强一致AT模式最终一致

读写隔离问题?
在事务1释放db锁后,事务2拿到了锁,并对数据库进行了修改。这个时候事务1,发生了错误,产生了回滚。事务2的数据就等于无效更新

全局锁:由TC记录当前正在操作的某行数据的事务,该事务持有全局锁,具备执行权

事务隔离:

  • 事务1,获取db锁,保存快照,执行业务,获取全局锁,提交事务释放db锁
  • 事务2,获取db锁,保存快照,获取全局锁失败,因为事务1没释放。
  • 事务1,获取db锁,但事务2事务没结束。无法获得,等待时间很长
  • 事务2,每隔10毫秒时间会获取全局锁,最多30次300毫秒。
  • 事务2,任务超时回滚业务释放锁
  • 事务1,获取db锁,回滚业务

image.png

AT和XA性能不一样?

因为全局锁锁定的范围只是Seata的,db锁不释放,任何人都无法访问

如果非Seate管理的事务执行了,怎么办

  • seate事务:获取db锁,保存快照,执行业务,获取全局锁,提交事务,释放db锁
  • 非seate事务,获取db锁,执行业务,提交事务,释放db锁

快照有两个,一个是更新前, 一个是更新后

拿着更新后的快照和现在数据库的数据进行对比,会回滚
记录异常,发送警告,人工介入

ATM的优点ATM缺点
一阶段直接提交事务,释放数据库资源性能比较快两阶段之间属于软状态,属于最终一致
利用全局锁实现读写隔离快照功能影响性能,但是比XA模式好
没有代码侵入,框架自动回滚和提交
  1. 创建表

在Seata的数据库中,创建表


-- ----------------------------
-- Records of undo_log
-- ----------------------------



-- ----------------------------
-- Table structure for lock_table
-- ----------------------------
DROP TABLE IF EXISTS `lock_table`;
CREATE TABLE `lock_table`  (
  `row_key` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `xid` varchar(96) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `transaction_id` bigint(20) NULL DEFAULT NULL,
  `branch_id` bigint(20) NOT NULL,
  `resource_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `table_name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `pk` varchar(36) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `gmt_create` datetime NULL DEFAULT NULL,
  `gmt_modified` datetime NULL DEFAULT NULL,
  PRIMARY KEY (`row_key`) USING BTREE,
  INDEX `idx_branch_id`(`branch_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;


SET FOREIGN_KEY_CHECKS = 1;

在微服务项目数据库中创建

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for undo_log
-- ----------------------------
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log`  (
  `branch_id` bigint(20) NOT NULL COMMENT 'branch transaction id',
  `xid` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'global transaction id',
  `context` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'undo_log context,such as serialization',
  `rollback_info` longblob NOT NULL COMMENT 'rollback info',
  `log_status` int(11) NOT NULL COMMENT '0:normal status,1:defense status',
  `log_created` datetime(6) NOT NULL COMMENT 'create datetime',
  `log_modified` datetime(6) NOT NULL COMMENT 'modify datetime',
  UNIQUE INDEX `ux_undo_log`(`xid`, `branch_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = 'AT transaction mode undo table' ROW_FORMAT = Compact;
  1. 实现AT,记得加全局事务哦~
seata:
  data-source-proxy-mode: AT

TCC模式(性能最好)

TCC模式与AT模式非常相似,每阶段都是独立事务,不同的是TCC通过人工编码来实现数据恢复。需要实现三个方法:

  • Try:资源的检测和预留;
  • Confirm:完成资源操作业务;要求 Try 成功 Confirm 一定要能成功。
  • Cancel:预留资源释放,可以理解为try的反向操作。

image.png

TCC模式的每个阶段是做什么的?

  • Try:资源检查和预留
  • Confirm:业务执行和提交
  • Cancel:预留资源的释放

TCC的优点是什么?

  • 一阶段完成直接提交事务,释放数据库资源,性能好
  • 相比AT模型,无需生成快照,无需使用全局锁,性能最强
  • 不依赖数据库事务,而是依赖补偿操作,可以用于非事务型数据库

TCC的缺点是什么?

  • 有代码侵入,需要人为编写try、Confirm和Cancel接口,太麻烦
  • 软状态,事务是最终一致
  • 需要考虑Confirm和Cancel的失败情况,做好幂等处理

幂等性:幂等性就是,无论调用多少次,不会重复调用出现问题,保证是数据一致

空回滚:当某分支事务阶段阻塞时,可能导致全局事务超时而触发二阶段Cancel操作,未执行try操作时先执行了cancel操作,就是空回滚

image.png

业务悬挂:就是已经执行了回滚业务,但是这个时候突然畅通了,执行try去了。但是这个时候没有阶段二了,无法confirm或cancel了,这就是业务悬挂。

TCC 业务分析 / 声明接口

为了实现空回滚、防止业务悬挂,以及幂等性要求。我们必须在数据库记录冻结金额的同时,记录当前事务id和执行状态,为此我们设计了一张表:

image.png

业务表

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

DROP TABLE IF EXISTS `account_freeze_tbl`;
CREATE TABLE `account_freeze_tbl`  (
  `xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `user_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `freeze_money` int(11) UNSIGNED NULL DEFAULT 0,
  `state` int(1) NULL DEFAULT NULL COMMENT '事务状态,0:try,1:confirm,2:cancel',
  PRIMARY KEY (`xid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = COMPACT;

SET FOREIGN_KEY_CHECKS = 1;

需要创建TCC接口和TCC业务层实现类,以及dao层,实体类

image.png

service 层

@LocalTCC // 告诉Seate这是TCC
public interface AccountTCCService {

    // - @TwoPhaseBusinessAction 这个注解加在那个方法上,那个方法就是 try
    // - name 指定当前方法名称
    // - commitMethod 对应 confirm方法、
    // - rollbackMethod 对应 cancel 方法
    // - @BusinessActionContextParameter 用这个注解标注的参数,表示会放到上下文中,会利用 BusinessActionContext拿到参数
    @TwoPhaseBusinessAction(name = "deduct", commitMethod = "confirm", rollbackMethod = "cancel")
    void deduct(@BusinessActionContextParameter(paramName = "userId") String userId,
                @BusinessActionContextParameter(paramName = "money")int money);

    boolean confirm(BusinessActionContext ctx);

    boolean cancel(BusinessActionContext ctx);
}

serviceImpl实现类

@Slf4j
@Service
public class AccountTCCServiceImpl implements AccountTCCService {
    @Resource
    private AccountDao accountDao;

    @Resource
    private AccountFreezeMapper freezeMapper;


    @Override
    @Transactional
    public void deduct(String userId, int money) {
        // 全局获取事务 id
        String xid = RootContext.getXID();
        // 1. 判断是否是业务悬挂
        AccountFreeze freeze1 = freezeMapper.selectById(xid);
        if (freeze1 != null) {
            System.out.println("拒绝try");
            // 说明已经走过了 直接拒绝
            return;
        }
        // 1.扣减可用余额
        accountDao.deduct(userId, money);
        // 2.记录冻结金额,事务状态
        AccountFreeze freeze = new AccountFreeze();
        freeze.setUserId(userId);
        freeze.setFreezeMoney(money);
        freeze.setState(AccountFreeze.TRY);
        freeze.setXid(xid);
        freezeMapper.insert(freeze);
        System.out.println("运行try");
    }

    @Override
    public boolean confirm(BusinessActionContext context) {
        // 1.获取事务id
        String xid = context.getXid();
        // 2.根据id删除冻结记录
        int count = freezeMapper.deleteById(xid);
        System.out.println("运行confirm");
        return count == 1;
    }

    @Override
    public boolean cancel(BusinessActionContext context) {
        // 0.查询冻结记录
        String xid = context.getXid();
        AccountFreeze freeze = freezeMapper.selectById(xid);
        // 判断空回滚
        if (freeze == null) {
            String userId = context.getActionContext("userId").toString();
            freeze = new AccountFreeze();
            freeze.setXid(xid);
            freeze.setUserId(userId);
            freeze.setState(AccountFreeze.CANCEL);
            freeze.setFreezeMoney(0);
            System.out.println("空回滚");
            return true;
        }
        // 判断是否是幂等
        if(freeze.getState() == AccountFreeze.CANCEL){
            System.out.println("幂等");
            return true;
        }

        // 1.恢复可用余额
        accountDao.refund(freeze.getUserId(), freeze.getFreezeMoney());
        // 2.将冻结金额清零,状态改为CANCEL
        freeze.setFreezeMoney(0);
        freeze.setState(AccountFreeze.CANCEL);
        int count = freezeMapper.updateById(freeze);
        System.out.println("运行回滚");
        return count == 1;
    }
}

Saga 模式

Saga模式是SEATA提供的长事务解决方案。也分为两个阶段:

  • 一阶段:直接提交本地事务
  • 二阶段:成功则什么都不做;失败则通过编写补偿业务来回滚

没有隔离性,会有脏写的可能,没有全局锁,不冻结资源

一阶段:依次执行事务 如果有任何一个事务存在问题,会启动补偿逻辑。

Saga模式优点Saga模式优点
事务参与者基于事件驱动实现异步调用,吞吐高软状态持续时间不确定,时效性差
不用编写TCC中的三个阶段,实现简单没有锁,没有事务隔离,会有脏写

image.png

高可用(搭建)

TC的异地多机房容灾架构

如果有服务挂了。咋整

异地,放在多个机房。上海!北京!杭州

微服务通过Nacos,namespace,和 group,服务名,找到服务

可是事务组,写在代码里,这怎么行。需要(热更新)

实现高可用,异地容灾

  1. 首先再打开一个seata,里面的 cluster = "HZ"
  2. 启动,可以看到seata.server有两个集群了

image.png

  1. 更改热更新,动态修改配置

新增配置组

image.png 配置内容如下

# 事务组映射关系
service.vgroupMapping.seata-demo=SH

service.enableDegrade=false
service.disableGlobalTransaction=false
# 与TC服务的通信配置
transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.enableClientBatchSendRequest=false
transport.threadFactory.bossThreadPrefix=NettyBoss
transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker
transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler
transport.threadFactory.shareBossWorker=false
transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector
transport.threadFactory.clientSelectorThreadSize=1
transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread
transport.threadFactory.bossThreadSize=1
transport.threadFactory.workerThreadSize=default
transport.shutdown.wait=3
# RM配置
client.rm.asyncCommitBufferLimit=10000
client.rm.lock.retryInterval=10
client.rm.lock.retryTimes=30
client.rm.lock.retryPolicyBranchRollbackOnConflict=true
client.rm.reportRetryCount=5
client.rm.tableMetaCheckEnable=false
client.rm.tableMetaCheckerInterval=60000
client.rm.sqlParserType=druid
client.rm.reportSuccessEnable=false
client.rm.sagaBranchRegisterEnable=false
# TM配置
client.tm.commitRetryCount=5
client.tm.rollbackRetryCount=5
client.tm.defaultGlobalTransactionTimeout=60000
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000

# undo日志配置
client.undo.dataValidation=true
client.undo.logSerialization=jackson
client.undo.onlyCareUpdateColumns=true
client.undo.logTable=undo_log
client.undo.compress.enable=true
client.undo.compress.type=zip
client.undo.compress.threshold=64k
client.log.exceptionRate=100

那怎么读取呢,新增配置,事务组可以不要

seata:
  config:
    type: nacos
    nacos:
      server-addr: localhost:8845 # 读取地址
      username: nacos # 用户名和密码
      password: nacos
      group: SEATA_GROUP # 配置所在群组
      data-id: client.properties # 配置文件名称

注意:seate配置文件中不要用默认的服务名

  • 下图配置管理中要在一个分组中
  • 服务也在一块

image.png

image.png