Seata如何实现两阶段提交(2PC)分布式事务
创始人
2025-07-11 21:01:07
0

介绍

2PC,全称为两阶段提交(Two-Phase Commit),是一种在分布式系统中用来保证事务原子性和一致性的协议。它主要用于协调分布式数据库或分布式事务环境中的多个参与者,确保所有参与者要么一起成功提交事务,要么一起回滚事务,以保持数据的一致性。

图片图片

在2PC协议中有两个主要阶段:

  1. 准备阶段(Prepare Phase):

事务协调器接收到发起事务的客户端请求后,向所有参与该事务的资源管理器(例如数据库、服务节点等)发送“准备提交”请求。

每个资源管理器执行事务操作,并将事务相关的更改锁定但不提交,然后回复事务协调器它们是否准备好提交事务(根据各自是否能够成功完成事务而定)。

  1. 提交阶段(Commit Phase):
  • 如果事务协调器收到了所有资源管理器的肯定答复,即所有参与者都准备好提交事务,则向所有参与者发出“正式提交”指令。

  • 若协调器收到任何一个参与者的否定响应,或者在等待超时后仍有参与者未响应,则向所有参与者发出“回滚事务”的指令。

通过这种方式,2PC确保了所有节点要么全部完成事务,要么全部撤销事务,从而维护了分布式环境下的事务原子性。然而,2PC也存在一些缺点,比如单点故障问题(即事务协调器宕机可能导致事务长期阻塞)、网络分区情况下的不确定性以及性能上的潜在瓶颈。

Seata把一个分布式事务理解成一个包含了若干分支事务的全局事务。全局事务的职责是协调其下管辖的分支事务 达成一致,要么一起成功提交,要么一起失败回滚。此外,通常分支事务本身就是一个关系数据库的本地事务,下图是全局事务与分支事务的关系图:

图片图片

与 传统2PC 的模型类似,Seata定义了3个组件来协议分布式事务的处理过程

图片图片

  • Transaction Coordinator (TC):事务协调器,它是独立的中间件,需要独立部署运行,它维护全局事务的运行状态,接收TM指令发起全局事务的提交与回滚,负责与RM通信协调各各分支事务的提交或回滚。
  • Transaction Manager (TM):事务管理器,TM需要嵌入应用程序中工作,它负责开启一个全局事务,并最终向TC发起全局提交或全局回滚的指令。
  • Resource Manager (RM):控制分支事务,负责分支注册、状态汇报,并接收事务协调器TC的指令,驱动分支(本地)事务的提交和回滚。

具体实现

案例分析:两个账户在不同的银行(张三在bank1、李四在bank2),bank1和bank2是两个微服务。交易过程是,张三给李四转账指定金额。

上述交易步骤,要么一起成功,要么一起失败,必须是一个整体性的事务。

图片图片

为了简化环境搭建,小编这里采用file启动seata,项目搭建也只是两个普通的SpringBoot项目,未使用微服务。

下载seata服务器

官方下载地址:https://github.com/seata/seata/releases

  1. registry.type=file:

registry.type=file 其类型设置为 file 时,意味着 Seata 的服务注册中心不依赖于外部的如 Nacos、Eureka、Zookeeper 等第三方注册中心,而是使用本地文件的方式来存储和管理服务节点信息。这种模式主要用于快速测试或简单的单机部署场景,因为在这种模式下无法自动发现和管理集群环境中的其他 Seata Server 节点,不具备高可用性。

  1. config.type=file:
  • config.type=file 表示 Seata 使用本地文件作为配置源。这意味着 Seata 会从指定的本地文件中读取全局事务协调器(TC)、事务管理器(TM)和资源管理器(RM)等组件所需的配置信息,而不是通过Nacos、Apollo或其他远程配置中心获取配置。这种方式同样适用于快速验证和简单部署情况,实际生产环境中可能需要结合分布式配置中心来动态更新和管理配置。

  • seata安装初始化参考《SpringCloud Alibaba微服务实战之环境准备》,注意本次启动是采用file方式启动
  • seata启动:/bin/seata-server.bat -m file

图片图片

  • bank-1 和 bank-2启动:

图片图片

bank-1 和 bank-2服务搭建

库表建立

CREATE DATABASE `bank1` CHARACTER SET 'utf8' COLLATE 'utf8_general_ci';

CREATE TABLE `account_info` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`account_name` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '户主姓名',
`account_no` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '银行卡号',
`account_password` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '帐户密码',
`account_balance` double NULL DEFAULT NULL COMMENT '帐户余额',
PRIMARY KEY (`id`) USING BTREE ) 
ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;
INSERT INTO `account_info` VALUES (2, '张三的账户', '1', '', 10000);
CREATE DATABASE `bank2` CHARACTER SET 'utf8' COLLATE 'utf8_general_ci';

CREATE TABLE `account_info` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`account_name` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '户主姓名',
`account_no` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '银行卡号',
`account_password` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '帐户密码',
`account_balance` double NULL DEFAULT NULL COMMENT '帐户余额',
PRIMARY KEY (`id`) USING BTREE) 
ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;
INSERT INTO `account_info` VALUES (3, '李四的账户', '2', NULL, 0);

备注:分别在bank1、bank2库中创建undo_log表,此表为seata框架使用

依赖引入


        
            org.springframework.boot
            spring-boot-starter-web
        
        
            io.seata
            seata-spring-boot-starter
            1.4.2
        

        
            com.baomidou
            mybatis-plus-boot-starter
            3.4.1
        
        
        
            mysql
            mysql-connector-java
            5.1.47
        
        
            org.projectlombok
            lombok
        
        
        
            org.apache.httpcomponents
            httpclient
        
    

定义配置

server:
  port: 8081
  #port: 8082

spring:
  application:
    name: bank-1
    #name: bank-2
  datasource:
    url: jdbc:mysql://localhost:3306/bank1?characterEncoding=utf8&useSSL=false
    #url: jdbc:mysql://localhost:3306/bank2?characterEncoding=utf8&useSSL=false
    driver-class-name: com.mysql.jdbc.Driver
    username: root
    password: root

seata:
  tx-service-group: order_tx_group #自定义事务组名称需要与seata-server中的对应
  service:
    vgroup-mapping:
      order_tx_group: default # TC 集群(必须与seata-server保持一致)

定义mapper

# bank-1
@Update("update account_info set account_balance = account_balance + #{amount} where account_no = #{accountNo}")
int updateAccountBalance(@Param("accountNo") String accountNo, @Param("amount") Double amount);

# bank-2
@Update("UPDATE account_info SET account_balance = account_balance + #{amount} WHERE account_no = #{accountNo}")
int updateAccountBalance(@Param("accountNo") String accountNo, @Param("amount") Double amount);

服务调用

bank-1:

@GlobalTransactional
    @Override
    public void updateAccountBalance(String accountNo, Double amount) {
        log.info("******** Bank1 Service Begin ... xid: {}" , RootContext.getXID());
        //张三扣减金额
        baseMapper.updateAccountBalance(accountNo,amount * -1);
        //向李四转账
        CloseableHttpClient httpclient = HttpClients.createDefault();
        HttpGet httpget = new HttpGet("http://localhost:8082/bank2/transfer?amount="+amount);
        httpget.addHeader(RootContext.KEY_XID,RootContext.getXID());
        try{
            CloseableHttpResponse response = httpclient.execute(httpget);
            HttpEntity entity = response.getEntity();
            String result = EntityUtils.toString(entity);
            log.info("bank2 服务返回结果:"+result);
        }catch (Exception e){
            throw new RuntimeException("bank2 服务异常");
        }
        //人为制造错误
        if(amount > 100){
            throw new RuntimeException("bank1 make exception amount > 100");
        }
    }

当业务方法开启全局异常处理器后,TM注册到TC获取到一个XID,此时在业务中,服务远程访问时,此XID会被下面分支业务方法RM接收到,当各个方法处理完成后RM会向TC直接交互把结果通过XID通知给TC,最后业务方法结束后,TM会通知TC业务已经完成,TC会根据RM通知的结果来通知各个RM提交或者回滚。但是在分布式事务中,入口TM传出时不会将XID放入请求头中向其他服务传递,这样就导致全局异常捕获失效,因此需要手动将XID设置到请求头中,携带给各分支业务来避免事务失效问题。

bank-2:

@Transactional
    @Override
    public void updateAccountBalance(String accountNo, Double amount) {
        log.info("******** Bank2 Service Begin ... xid: {}" , RootContext.getXID());
        //李四增加金额
        baseMapper.updateAccountBalance(accountNo,amount);
        //制造异常
        if(amount < 100){
            throw new RuntimeException("bank1 make exception amount < 100");
        }
    }

服务配置seata

file.conf:

图片图片

registry.conf:

图片图片

执行流程

正常流程:

图片图片

回滚流程:

图片图片

相关内容

热门资讯

如何允许远程连接到MySQL数... [[277004]]【51CTO.com快译】默认情况下,MySQL服务器仅侦听来自localhos...
如何利用交换机和端口设置来管理... 在网络管理中,总是有些人让管理员头疼。下面我们就将介绍一下一个网管员利用交换机以及端口设置等来进行D...
施耐德电气数据中心整体解决方案... 近日,全球能效管理专家施耐德电气正式启动大型体验活动“能效中国行——2012卡车巡展”,作为该活动的...
Windows恶意软件20年“... 在Windows的早期年代,病毒游走于系统之间,偶尔删除文件(但被删除的文件几乎都是可恢复的),并弹...
20个非常棒的扁平设计免费资源 Apple设备的平面图标PSD免费平板UI 平板UI套件24平图标Freen平板UI套件PSD径向平...
德国电信门户网站可实时显示全球... 德国电信周三推出一个门户网站,直观地实时提供其安装在全球各地的传感器网络检测到的网络攻击状况。该网站...
着眼MAC地址,解救无法享受D... 在安装了DHCP服务器的局域网环境中,每一台工作站在上网之前,都要先从DHCP服务器那里享受到地址动...
为啥国人偏爱 Mybatis,... 关于 SQL 和 ORM 的争论,永远都不会终止,我也一直在思考这个问题。昨天又跟群里的小伙伴进行...