分布式事务是在分布式环境在由多个本地事务组成,不同的事务操作分布在不同的节点或微服务上。分布式事务用于在分布式的环境中保证不同节点之间的数据一致性。

2PC

2PC(Two-phase commit protocol) 二阶段提交协议,属于数据强一致性的解决方案,引入了事务管理器Transaction Manager、事务参与者的概念,分为准备阶段prepare phase,提交阶段commit phase

准备阶段: 事务协调者向每个参与者发送prepare请求,每个事务参与者执行本地事务,写入undo/redo日志,不执行提交操作

提交阶段: 事务管理器收到任意一个事务参与者的失败或者超时响应时,向每个事务参与者发送回滚请求;反之向所有参与者发送提交请求。参与者收到事务管理器的指令执行提交或者回滚操作,并释放事务处理中使用到的锁资源。必须在最后阶段释放锁资源。

image-20220424104740011

常见2PC的解决方案

XA方案

2PC传统方案在数据库层面被已经常用的数据库厂商进行了实现,Oracle、MySQL都支持2PC协议,为了统一行业标准,国际开放标准组织对分布式事务处理模型进行了定义DTP Distributed Transaction Processing Reference Model

DTP模型定义如下角色

AP Application Program: 应用程序

RM Resource Manger: 资源管理器类似事务参与者,一般代指一个数据库实例,通过RM对该数据库进行控制,管理着分支事务

TM Transaction Manager: 事务管理器 负责协调和管理事务,控制全局事务,管理事务生命周期协调每一个RM

TM与RM之间的通讯接口规范叫XA,基于数据库XA协议来实现的2PC又称为XA方案

  1. TM向AP提供编程接口,AP通过TM提交回滚事务
  2. TM中间件通过XA接口来通知RM事务的开始、结束、提交、回滚等
  • 准备阶段RM执行实际业务操作,不提交事务,资源锁定
  • 提交阶段 TM接受在RM准备阶段的执行回复,如果有一个RM失败,TM通知所有RM执行回滚操作,否则TM通知所有RM执行事务提交,提交阶段结束之后释放资源

存在的问题

  1. 需要本地数据库支持XA协议
  2. 锁资源需要等到两个阶段结束才释放,性能差

Seata方案

Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。Seata目标在于对业务无侵入,在传统2PC基础上演进。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。将事务拆分为若干分支事务和全局事务。全局事务协调管辖分支事务达成一致,要么都成功提交,要么都失败回滚。

Seata的AT模式

AT模式需要本地数据库支持ACID;java应用通过JDBC访问数据库

两阶段提交协议的演变

  1. 一阶段: 业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源
  2. 二阶段:提交异步化快速响应,回滚通过一阶段回滚日志进行反向补偿

角色

TC (Transaction Coordinator) - 事务协调者

维护全局和分支事务的状态,驱动全局事务提交或回滚。

TM (Transaction Manager) - 事务管理器

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

RM (Resource Manager) - 资源管理器

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

执行流程

image-20220424113257080

以用户注册送积分为例,用户系统与积分系统分别是两个不同微服务具体流程如下

  1. 用户服务中的TM向TC申请开启一个全局事务,全局事务创建成功返回一个全局唯一的XID
  2. 用户服务中的RM向TC注册分支事务,该分支事务在用户服务中执行新增用户逻辑,并将其纳入XID对应全局事务管辖
  3. 用户执行分支事务,向用户表插入一条数据
  4. 执行调用远程积分服务(XID在微服务调用链路中上下文传递),积分服务RM向TC注册分支事务,该分支事务执行增加积分逻辑,并将其纳入XID对应全局事务的管辖
  5. 积分事务执行分支事务,向积分表插入记录,执行完毕返回用户服务
  6. 用户服务分支事务执行完毕
  7. TM向TC发送针对XID的全局提交或回滚的决策
  8. TC调度XID下管辖的所有分支事务完成提交或者回滚操作

与传统2PC对比

  1. 传统2PC的RM是在数据库层的,RM就是数据库本身通过XA协议实现,Seata的RM是以jar形式作为中间件层部署在应用程序一侧
  2. 传统2PC事务资源锁都要保持到二阶段完成才会释放。Seata做法实在一阶段就将本地事务提交,省去二阶段持锁时间,提高效率,如果二阶段需要回滚通过undo日志中的回滚方法,对事务进行补偿

实操

已常见的nacos注册中心为例整合Spring Cloud Alibaba

Server端

  1. 从v1.4.2版本开始,已支持从一个Nacos dataId中获取所有配置信息,你只需要额外添加一个dataId配置项。

  2. registry.conf 中加入对应配置中心和注册中心

    registry {
      # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
      type = "nacos"
    
      nacos {
        application = "seata-server"
        serverAddr = "127.0.0.1:8848"
        group = "SEATA_GROUP"
        namespace = "dev"
        cluster = "default"
        username = "nacos"
        password = "nacos"
      }
    }
    
    config {
      # file、nacos 、apollo、zk、consul、etcd3
      type = "nacos"
      nacos {
        serverAddr = "127.0.0.1:8848"
        namespace = "dev"
        group = "SEATA_GROUP"
        username = "nacos"
        password = "nacos"
        dataId = "seataServer.properties"
      }
    
    }
    
  3. 初始化nacos配置

    1. 从v1.4.2版本开始,已支持从一个Nacos dataId中获取所有配置信息,你只需要额外添加一个dataId配置项。
    2. 首先你需要在nacos新建配置,此处dataId为seataServer.properties,配置内容参考https://github.com/seata/seata/tree/develop/script/config-center 的config.txt并按需修改保存
    3. 在client参考如下配置进行修改
    seata:
      config:
        type: nacos
        nacos:
          server-addr: 127.0.0.1:8848
          group : "SEATA_GROUP"
          namespace: ""
          dataId: "seataServer.properties"
          username: "nacos"
          password: "nacos"
    

    image-20220506095155852

  4. seataServer.properties中核心的配置 service.vgroupMapping.XXX_TX_GROUP 表示事务分组名成

    image-20220506095817165

  5. 启动Server 启动成功之后会在nacos中看到注册了一个服务

    sh seata-server.sh
    

    image-20220506100004205

image-20220506100139191

Client端

  1. 整合Spring Cloud Alibaba 引入依赖

            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
            </dependency>
    
  2. 修改配置

    # 对应nacos中seataServer.properties中的事务分组service.vgroupMapping.XXX_TX_GROUP
    spring.cloud.alibaba.seata.tx-service-group=default_tx_group
    
    seata:
      registry:
        type: nacos
        nacos:
          application: seata-server
          server-addr: 127.0.0.1:8848
          namespace: "dev"
          seata:
      config:
        type: nacos
        nacos:
          server-addr: 127.0.0.1:8848
          group : "SEATA_GROUP"
          namespace: "dev"
          dataId: "seataServer.properties"
          username: "nacos"
          password: "nacos"
    
  3. 添加file.conf和registry.conf文件到resource目录下

    image-20220506101601269

    file.conf

    service {
      #vgroup->rgroup
      vgroupMapping.default_tx_group = "default" #修改自定义事务组名称
      #only support single node
      default.grouplist = "127.0.0.1:8091"
      #degrade current not support
      enableDegrade = false
      #disable
      disable = false
      #unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent
      max.commit.retry.timeout = "-1"
      max.rollback.retry.timeout = "-1"
      disableGlobalTransaction = false
    }
    

    registry.conf

    registry {
      # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
      type = "nacos"
    
      nacos {
        application = "seata-server"
        serverAddr = "127.0.0.1:8848"
        group = "SEATA_GROUP"
        namespace = "dev"
        cluster = "default"
        username = "nacos"
        password = "nacos"
      }
    }
    
    config {
      # file、nacos 、apollo、zk、consul、etcd3
      type = "nacos"
    
      nacos {
        serverAddr = "127.0.0.1:8848"
        namespace = "dev"
        group = "SEATA_GROUP"
        username = "nacos"
        password = "nacos"
        dataId = "seataServer.properties"
      }
    }
    
  4. 在分布式事务的发起方添加注解@GlobalTransactional 省略实体和mapper的代码

    @FeignClient(contextId = "spring-consumer", name = "spring-consumer")
    @Service
    public interface PointService {
    
        @PostMapping("createPoint")
        Map<String,Object> createPoint(@RequestBody Point point);
    }
    
    @RestController
    @Slf4j
    public class PointController implements PointService {
        @Autowired
        private PointMapper pointMapper;
    
        @Override
        @Transactional
        public Map<String, Object> createPoint(Point point) {
            String txId = RootContext.getXID();
            log.info("Seata全局事务id=================>{}", txId);
            pointMapper.insert(point);
            Map<String, Object> result = new HashMap<>();
            result.put("message", "success");
            // 模拟异常
            int i = 1/0;
            return result;
        }
    }
    
        @GlobalTransactional
        public void create(String username) {
            log.info("Seata全局事务id=================>{}",RootContext.getXID());
            User user = new User();
            user.setName(username);
            userMapper.insert(user);
            // 增加积分
            Point point = new Point();
            point.setUserId(user.getId());
            point.setPoint(atomicInteger.incrementAndGet());
            pointService.createPoint(point);
    //        int i = 1/0;
        }
    
  5. 测试分布式事务中出现异常 查看日志全局事务id下的二阶段状态为回滚 查看数据库没有提交数据

    image-20220506102050939

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐