分布式事务解决方案:XA规范

请添加图片描述

XA规范

二阶段提交协议是一个协议,而XA规范是X/Open 组织针对二阶段提交协议的实现做的规范。目前几乎所有的主流数据库都对XA规范提供了支持。

这样做的好处是方便多个资源(如数据库,应用服务器,消息队列等)在同一个事务中访问。你可以类比JDBC

我们这篇文章就以MySQL XA为例演示一下XA怎么玩?

MySQL XA常用的命令如下

命令解释
XA START xid开启一个事务,并将事务置于ACTIVE状态,此后执行的SQL语句都将置于该事务中
XA END xid将事务置于IDLE状态,表示事务内的SQL操作完成
XA PREPARE xid实现事务提交的准备工作,事务状态置于PREPARED状态。事务如果无法完成提交前的准备操作,该语句会执行失败
XA COMMIT xid事务最终提交,完成持久化
XA ROLLBACK xid事务回滚终止
XA RECOVER查看MySQL中存在的PREPARED状态的xa事务

我们在db_account_1和db_account_2都建一个account_info表并初始化2条记录

CREATE TABLE `account_info`
(
    `id`      INT(11)      NOT NULL AUTO_INCREMENT COMMENT '自增主键',
    `user_id` VARCHAR(255) NOT NULL COMMENT '用户id',
    `balance` INT(11)      NOT NULL DEFAULT 0 COMMENT '用户余额',
    PRIMARY KEY (`id`)
) ENGINE = InnoDB
  AUTO_INCREMENT = 1
  DEFAULT CHARSET = utf8;

INSERT INTO account_info (id, user_id, balance)
VALUES (1, '1001', 10000);
INSERT INTO account_info (id, user_id, balance)
VALUES (2, '1002', 10000);

我们以用户1001向1002转账200元为例

mysql> XA START "transfer_money";
Query OK, 0 rows affected (0.01 sec)
 
mysql> update account_info set balance = balance - 200 where user_id = '1001';
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0
mysql> XA END "transfer_money";
Query OK, 0 rows affected (0.01 sec)
 
mysql> XA PREPARE "transfer_money";
Query OK, 0 rows affected (0.01 sec)
 
mysql> XA COMMIT "transfer_money";
Query OK, 0 rows affected (0.01 sec)

在XA START执行后所有资源将会被锁定,直到执行了XA PREPARE或者XA COMMIT才会释放。

如果在这个时间段内另外一个事务执行如下语句则会一直被阻塞

update account_info set balance = balance - 200 where user_id = '1001';

这就是XA规范这种解决方案很少被使用的原因,因为中间过程会锁定资源,很难支持高并发

我们也可以将一个 IDLE 状态的 XA 事务可以直接提交或者回滚

mysql> XA COMMIT "transfer_money";
1399 - XAER_RMFAIL: The command cannot be executed when global transaction is in the  IDLE state
mysql> XA COMMIT "transfer_money" ONE PHASE;
Query OK, 0 rows affected (0.01 sec)
 
mysql> XA START "transfer_money";
Query OK, 0 rows affected (0.01 sec)
 
mysql> update account_info set balance = balance - 200 where user_id = '1001';
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0
mysql> XA END "transfer_money";
Query OK, 0 rows affected (0.01 sec)
 
mysql> XA COMMIT "transfer_money" ONE PHASE;
Query OK, 0 rows affected (0.01 sec)

XA事务变化图
在这里插入图片描述

JTA

JTA(Java Transaction API),是J2EE的编程接口规范,它是XA规范的Java实现相关的接口有如下2个

javax.transaction.TransactionManager(事务管理器的接口):定义了有关事务的开始、提交、撤回等操作。
javax.transaction.xa.XAResource(满足XA规范的资源定义接口):一种资源如果要支持JTA事务,就需要让它的资源实现该XAResource接口,并实现该接口定义的两阶段提交相关的接口

在Java中有很多框架都对XA规范进行了实现,我就演示一下最常用的实现atomikos和seata

atomikos只能用在单个应用对多个库进行操作的场景。而seata所有的分布式事务场景都能用
是什么造成这种差异呢?看Demo

Atomikos实现XA规范

先加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jta-atomikos</artifactId>
    <version>2.1.14.RELEASE</version>
</dependency>

配置2个数据源

spring:
  jta:
    atomikos:
      datasource:
        primary:
          borrow-connection-timeout: 10000.0
          max-lifetime: 20000.0
          max-pool-size: 25.0
          min-pool-size: 3.0
          unique-resource-name: test1
          xa-data-source-class-name: com.mysql.jdbc.jdbc2.optional.MysqlXADataSource
          xa-properties:
            password: test
            url: jdbc:mysql://myhost:3306/db_account_1
            user: test
        secondary:
          borrow-connection-timeout: 10000.0
          max-lifetime: 20000.0
          max-pool-size: 25.0
          min-pool-size: 3.0
          unique-resource-name: test2
          xa-data-source-class-name: com.mysql.jdbc.jdbc2.optional.MysqlXADataSource
          xa-properties:
            password: test
            url: jdbc:mysql://myhost:3306/db_account_2
            user: test
    enabled: true
@Configuration
public class DataSourceConfig {

    @Bean
    @ConfigurationProperties(prefix = "spring.jta.atomikos.datasource.primary")
    public DataSource primaryDataSource() {
        return new AtomikosDataSourceBean();
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.jta.atomikos.datasource.secondary")
    public DataSource secondaryDataSource() {
        AtomikosDataSourceBean ds = new AtomikosDataSourceBean();
        return ds;
    }

    @Bean
    public JdbcTemplate primaryJdbcTemplate(
            @Qualifier("primaryDataSource") DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }

    @Bean
    public JdbcTemplate secondaryJdbcTemplate(
            @Qualifier("secondaryDataSource") DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }
}
@Service
public class AccountService {

    @Resource
    @Qualifier("primaryJdbcTemplate")
    private JdbcTemplate primaryJdbcTemplate;

    @Resource
    @Qualifier("secondaryJdbcTemplate")
    private JdbcTemplate secondaryJdbcTemplate;

    @Transactional(rollbackFor = Exception.class)
    public void tx1() {
        Integer money = 100;
        String sql = "update account_info set balance = balance + ? where user_id = ?";
        primaryJdbcTemplate.update(sql, new Object[]{-money, 1001});
        secondaryJdbcTemplate.update(sql, new Object[]{money, 1002});
    }

    @Transactional(rollbackFor = Exception.class)
    public void tx2() {
        Integer money = 100;
        String sql = "update account_info set balance = balance + ? where user_id = ?";
        primaryJdbcTemplate.update(sql, new Object[]{-money, 1001});
        secondaryJdbcTemplate.update(sql, new Object[]{money, 1002});
        throw new RuntimeException();
    }
}
@RunWith(SpringRunner.class)
@SpringBootTest
public class AtomikosAtApplicationTests {

    @Resource
    private AccountService accountService;

	// 正常执行
    @Test
    public void test1() {
        accountService.tx1();
    }
    
    // 异常回滚
    @Test
    public void test2() {
        accountService.tx2();
    }
}

SEATA实现XA规范

以seata-xa-tm向seata-xa-rm转账演示一下seata xa模式的用法
github地址:https://github.com/erlieStar/seata-learning

seata-xa-tm

我们只需要配置一下application.yaml即可。你可能看到很多文章还需要配置file.conf(设置配置中心)和registry.conf(设置注册中心),用了spring starter后直接在application.yaml配置即可

application.yaml

server:
  port: 30002

spring:
  application:
    name: seata-xa-tm
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url : jdbc:mysql://myhost:3306/db_account_1?useUnicode=true&characterEncoding=utf8
    username: test
    password: test

seata:
  data-source-proxy-mode: XA
  enabled: true
  application-id: ${spring.application.name}
  tx-service-group: my_test_tx_group
  service:
    vgroup-mapping:
      my_test_tx_group: default
    grouplist:
      default: myhost:18091
    disable-global-transaction: false
  config:
    type: file
    file:
      name: file.conf
  registry:
    type: file
    file:
      name: file.conf

tx-service-group配置了事务分组,事务分组容易配错,以下面配置为例,演示一下事务分组的工作流程

tx-service-group: my_test_tx_group
service:
  vgroup-mapping:
    my_test_tx_group: default
  grouplist:
    default: myhost:18091
  disable-global-transaction: false
  1. 通过tx-service-group找到对应的事务分组名my_test_tx_group
  2. 将事务分组名拼接成如下形式service.vgroup-mapping.my_test_tx_group,查找到的tc集群名为default
  3. 将集群名拼接为如下形式service.grouplist.default,找到真实的TC服务地址为myhost:18091

事务分组的作用你可以参考一下官网:http://seata.io/zh-cn/docs/user/txgroup/transaction-group.html

config(配置中心)和 registry(注册中心)的相关配置,我就直接设置成file类型了(读取本地文件),方便我们调试

用@EnableAutoDataSourceProxy注解开启数据源代理,只需指定为XA模式,因为默认是AT模式

@EnableFeignClients
@SpringBootApplication
@EnableAutoDataSourceProxy(dataSourceProxyMode = "XA")
public class SeataXATm {

    public static void main(String[] args) {
        SpringApplication.run(SeataXATm.class, args);
    }

}

开发转账接口

@RestController
@RequestMapping("account")
public class AccountController {

    @Resource
    private JdbcTemplate jdbcTemplate;
    @Resource
    private RmAccountClient rmAccountClient;

    @GlobalTransactional
    @RequestMapping("transfer")
    public String transfer(@RequestParam("fromUserId") String fromUserId,
                           @RequestParam("toUserId") String toUserId,
                           @RequestParam("money") Integer money,
                           @RequestParam(value = "flag", required = false) Boolean flag) {
        String sql = "update account_info set balance = balance + ? where user_id = ?";
        jdbcTemplate.update(sql, new Object[]{-money, fromUserId});
        String result = rmAccountClient.transfer(fromUserId, toUserId, money);
        if ("fail".equals(result)) {
            throw new RuntimeException("转账失败");
        }
        if (flag != null && flag) {
            throw new RuntimeException("测试同时回滚");
        }
        return "success";
    }
}

调用另外一个账户服务,为了方便我就不用注册中心了,直接指定了服务的地址

@FeignClient(value = "seata-xa-rm", url = "http://127.0.0.1:30001")
public interface RmAccountClient {

    @RequestMapping("account/transfer")
    String transfer(@RequestParam("fromUserId") String fromUserId,
                    @RequestParam("toUserId") String toUserId,
                    @RequestParam("money") Integer money);

}

seata-xa-rm

这是我们开发的另一个账户服务

application.yaml

server:
  port: 30001

spring:
  application:
    name: seata-xa-rm
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url : jdbc:mysql://myhost:3306/db_account_2?useUnicode=true&characterEncoding=utf8
    username: test
    password: test

seata:
  data-source-proxy-mode: XA
  enabled: true
  application-id: ${spring.application.name}
  tx-service-group: my_test_tx_group
  service:
    vgroup-mapping:
      my_test_tx_group: default
    grouplist:
      default: myhost:18091
    disable-global-transaction: false
  config:
    type: file
    file:
      name: file.conf
  registry:
    type: file
    file:
      name: file.conf

启动类

@SpringBootApplication
@EnableAutoDataSourceProxy(dataSourceProxyMode = "XA")
public class SeataXARm {

    public static void main(String[] args) {
        SpringApplication.run(SeataXARm.class, args);
    }
}

转账接口

@RestController
@RequestMapping("account")
public class AccountController {

    @Resource
    private JdbcTemplate jdbcTemplate;

    @RequestMapping("transfer")
    public String transfer(@RequestParam("fromUserId") String fromUserId,
                           @RequestParam("toUserId") String toUserId,
                           @RequestParam("money") Integer money) {
        String sql = "update account_info set balance = balance + ? where user_id = ?";
        int result = jdbcTemplate.update(sql, new Object[]{money, toUserId});
        if (result == 0) {
            return "fail";
        }
        return "success";
    }
}

测试

启动这2个服务

测试正常转账

curl http:127.0.0.1:30002/account?fromUserId=1001&toUserId=1002&money=100

测试seata-xa-tm项目失败回滚

curl http:127.0.0.1:30002/account?fromUserId=1001&toUserId=1002&money=100&flag=truee

用flag=true来让seata-xa-tm项目失败回滚

测试seata-xa-rm项目失败回滚

curl http:127.0.0.1:30002/account?fromUserId=1001&toUserId=1003&money=100&flag=truee

toUserId=1003,用户不存在,seata-xa-rm返回fail回滚

参考博客

汇总
[1]https://segmentfault.com/a/1190000040321750
[2]https://zhuanlan.zhihu.com/p/183753774
xa事务
[3]https://www.jianshu.com/p/a59c79186b6d
[4]https://www.jianshu.com/p/7003d58ea182
jta
[5]https://www.jianshu.com/p/86b4ab4f2d18

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Java识堂

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值