Análise de Transação Distribuída de Consistência Forte XA

I. Introdução

Transação distribuída: Em condições distribuídas, a consistência geral da transação de várias operações de nó.

Especialmente no cenário de microsserviço, os negócios A e B estão associados, a transação A é bem-sucedida e a transação B falha. Por causa do sistema cruzado, ela não será percebida. Neste ponto, os dados são inconsistentes como um todo.

Duas teorias básicas em transações distribuídas: CAPteoria e Baseteoria .

As soluções de transações distribuídas podem ser divididas em:

  • Soluções de transações distribuídas fortemente consistentes: baseadas na CAPteoria
  • Eventuais soluções de transações distribuídas consistentes: baseadas na Baseteoria

2022-07-0615-22-00.png

Solução distribuída consistente e forte

Em uma solução de transação fortemente consistente, os cenários típicos incluem:

  • DTPModelo (Global Transaction Model): Baseado no DTPmodelo , a solução típica é a XAespecificação de um protocolo de comunicação distribuído
  • 2PCModelo (modelo de confirmação de duas fases)
  • 3PCModelo (modelo de confirmação de três fases)

(1) DTPModelo

DTPUm modelo X/Opené um conjunto de padrões de transações distribuídas definido por uma organização.Esse conjunto de padrões define principalmente a especificação e implementação de transações distribuídas API.

DTPConceitos importantes do modelo:

  1. Transação: Uma transação é uma unidade completa de trabalho, com ACIDcaracterísticas .
  2. Transação global: Uma transação gerenciada por um gerenciador de transações, capaz de operar vários gerenciadores de recursos ao mesmo tempo.
  3. Transação de filial: Na transação global gerenciada pelo gerenciador de transações, a transação executada independentemente em cada gerenciador de recursos.
  4. Thread de controle: a thread que executa a transação global, esta thread é usada para associar o relacionamento entre a aplicação, o gerenciador de transações e o gerenciador de recursos,

No DTPmodelo , 3 componentes principais são definidos:

XA.png

  1. application( AP) : um aplicativo que participa do modelo de transação DTPdistribuído .
  2. 事务管理器(TM:负责协调和管理 DTP 模型中的事务,为应用程序提供编程接口,同时管理资源管理器。
  3. 资源管理器(RM:数据库管理系统或消息服务管理器。

(2)2PC 模型

两阶段提交(Two-phase Commit, 2PC)算法,经常用来实现分布式事务。

2PC 模型两阶段执行流程:

2022-07-0616-50-20.png

  1. Prepare 准备阶段:在本地执行相应的事务,但事务并没有提交
  2. Commit 提交阶段:发送 回滚消息 或者 提交消息

2PC 模型存在的问题:

  1. 同步阻塞问题:事务的执行过程中,所有参与事务的节点都会对其占用的公共资源加锁,导致其他访问公共资源的进程或者线程阻塞。
  2. 单点故障问题:如果事务管理器发生故障,则资源管理器会一直阻塞。
  3. 数据不一致问题:如果在 Commit 阶段,由于网络或者部分资源管理器发生故障,导致部分资源管理器没有接收到事务管理器发送过来的 Commit 消息,会引起数据不一致的问题。
  4. 无法解决的问题:如果在 Commit 阶段,事务管理器发出 Commit 消息后宕机,并且唯一接收到这条 Commit 消息的资源管理器也宕机了,则无法确认事务是否已经提交。

(3)3PC 模型

3PC 模型是指三阶段提交模型,是在 2PC 模型的基础上改进的版本。

3PC 模型把 2PC 模型中的 Prepare 阶段一分为二,形成 3个阶段:

2022-07-0617-17-29.png

  1. CanCommit 阶段:询问是否能够执行事务。
  2. PreCommit 阶段:执行事务操作。
  3. doCommit / doRollback 阶段:正式提交事务。

3PC 模型主要解决了 单点故障问题,并减少了事务执行过程中产生的阻塞现象。



二、XA 强一致性分布式事务原理

XA 规范:

  • xa_start: 负责开启或者恢复一个事务分支,并且管理 XID 到调用线程。

  • xa_end: 负责取消当前线程与事务分支的关联。

  • xa_prepare: 询问 RM 是否准备好提交事务分支。

  • —————— 第一阶段提交 —————————

    如果是单机,可以直接跳过 prepare 和第二阶段,输入 one phase commit 事务id 直接进行提交即可。

  • xa_commit: 通知 RM 提交事务分支。

  • xa_rollback: 通知 RM 回滚事务分支。

  • xa_recover: 需要恢复的 XA 事务。

  • —————— 第二阶段提交 —————————

XA 二阶段提交:

  • 一阶段:执行 XA PREPARE 语句。
  • 二阶段:执行 XA COMMIT/ROLLBACK 语句。

XA 协议存在的问题

  1. 同步阻塞问题:一般情况下,不需要调高隔离级别,XA 默认不会改变隔离级别

    全局事务内部包含了多个独立的事务分支,这一组事务分支要不都成功,要不都失败。各个事务分支的 ACID 特性共同构成了全局事务的 ACID 特性。也就是将单个事务分支的支持的 ACID 特性提升一个层次(up a level)到分布式事务的范畴。即使在非分布事务中(即本地事务),如果对操作读很敏感,我们也需要将事务隔离级别设置为 SERIALIZABLE,而对于分布式事务来说,更是如此,可重复读隔离级别不足以保证分布式事务一致性。也就是说,如果我们使用 MySQL 来支持 XA 分布式事务的话,那么最好将事务隔离级别设置为 SERIALIZABLE,地球人都知道 SERIALIZABLE(串行化)是四个事务隔离级别中最高的一个级别,也是执行效率最低的一个级别

  2. 单点故障成熟的 XA 框架需要考虑 TM 的高可用性

    由于协调者的重要性,一旦协调者 TM 发生故障,参与者 RM 会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)

  3. 数据不一致极端情况下,一定有事务失败问题,需要监控和人工处理

    在二阶段提交的阶段二中,当协调者向参与者发送 commit 请求之后,发生了局部网络异常或者在发送 commit 请求过程中协调者发生了故障,这回导致只有一部分参与者接受到了 commit 请求。而在这部分参与者接到 commit 请求之后就会执行 commit 操作。但是其他部分未接到 commit 请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据不一致性的现象。

解决 XA 存在的问题

解决 XA 数据不一致方案:

  1. 日志存储:记录 XA 事务在每个流程中的执行状态。
  2. 自定义事务恢复:通过 XA recovery 命令从资源管理器中获取需要被恢复的事务记录,然后根据 XID 匹配应用程序中存储的日志,根据事务状态进行提交或回滚。

解决事务管理器的单点故障方案:

  1. 去中心化部署:事务管理器嵌套在应用程序里面,不再单独部署。

2022-07-0712-10-31.png

  1. 中心化部署:事务管理器单独部署,然后与应用程序进行远程通信。

2022-07-0712-12-56.png

(1)MySQLXA 规范的支持

MySQL5.0.3 开始支持 InnoDB 引擎的 XA 分布式事务, MySQL Connector/J5.0.0 版本开始支持 XA

2022-07-0623-13-00.png

MySQL XA 事务状态是正确执行 XA 事务的关键:

每次执行 MySQLXA 事务语句都会修改 XA 事务的状态,进而执行不同的 XA 语句。

2022-07-0623-34-05.png

完整的 XA 事务处理过程: 2022-07-0623-46-45.png

单个 MySQL 的内部操作:

2022-07-0623-48-03.png

MySQL XA 的问题

MySQL <5.7 版本会出现的问题:

  1. 已经 prepare (预提交)的事务,在客户端退出或者服务宕机的时候,二阶段提交 的事务会被回滚。

  2. 在服务器故障重启提交后,相应的 Binlog 被丢失

MySQL 5.6 版本在客户端退出的时候,自动把已经 prepare 的事务回滚了,那么 MySQL 为什么要这样做?

这主要取决于 MysQL 的内部实现,MySQL 5.7 以前的版本,对于 prepare 的事务,MySQL 是不会记录 binlog 的(官方说是减少 fsync,起到了优化的作用)。只有当分布式事务提交的时候才会把前面的操作写入 binloq 信息,所以对于 binloq 来说,分布式事务与普通的事务没有区别,而 prepare 以前的操作信息都保存在连接的 IO CACHE 中,如果这个时候客户端退出了,以前的 binloq 信息都会被丢失,再次重连后允许提交的话,会造成 Binloq 丢失,从而造成主从数据的不一致,所以官方在客户端退出的时候直接把已经 prepare 的事务都回滚了!

MySQL >5.7 版本的优化:对应文档

MySQL 对于分布式事务,在 prepare 的时候就完成了写 Binlog 的操作,通过新增一种叫 XA-preparelog-event 的 event 类型来实现,这是与以前版本的主要区别(以前版本 prepare 时不写 Binlog)

(2)手动通过 JDBC 操作 MySQL XA 事务

MySQL Connector/J 从 5.0.0 版本之后开始直接提供对 XA 的支持,也就是提供了 Java 版本 XA 接口的实现。 意味着可以直接通过 Java 代码来执行 MySQL XA 事务。

模拟下订单减库存:

  1. 下订单:创建订单
  2. 扣库存:更新库存数量
-- 数据库如下
CREATE DATABASE tx_msg_order;
CREATE TABLE `order`  (
  `id` bigint(20) NOT NULL COMMENT '主键',
  `create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
  `order_no` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '订单编号',
  `product_id` bigint(20) NULL DEFAULT 0 COMMENT '商品id',
  `pay_count` int(11) NULL DEFAULT NULL COMMENT '购买数量',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '模拟订单' ROW_FORMAT = Dynamic;

CREATE DATABASE tx_msg_stock;
CREATE TABLE `stock`  (
  `id` bigint(11) NOT NULL COMMENT '主键id',
  `product_id` bigint(20) NULL DEFAULT 0 COMMENT '商品id',
  `total_count` int(11) NULL DEFAULT 0 COMMENT '商品总库存',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '模拟库存' ROW_FORMAT = Dynamic;

代码如下:项目地址

public class Test {

    public static void main(String[] args) throws SQLException {
        // 创建订单库 RM实例
        Connection orderConnection = DriverManager.getConnection(
                "jdbc:mysql://127.0.0.1:3306/tx_msg_order?useUnicode=true&characterEncoding=UTF-8&useOldAliasMetadataBehavior=true&autoReconnect=true&failOverReadOnly=false&useSSL=false",
                "test", "test");
        // 这里的这个true参数,是说打印出来XA分布式事务的一些日志
        XAConnection orderXAConnection = new MysqlXAConnection(
                (com.mysql.jdbc.Connection)orderConnection, true);
        // 这个XAResource其实你可以认为是RM(Resource Manager)的一个代码中的对象实例
        XAResource orderResource = orderXAConnection.getXAResource();

        // 创建库存库 的RM实例
        Connection stockConnection = DriverManager.getConnection(
                "jdbc:mysql://127.0.0.1:3306/tx_msg_stock?useUnicode=true&characterEncoding=UTF-8&useOldAliasMetadataBehavior=true&autoReconnect=true&failOverReadOnly=false&useSSL=false",
                "test", "test");
        XAConnection stockXAConnection = new MysqlXAConnection(
                (com.mysql.jdbc.Connection)stockConnection, true);
        XAResource stockResource = stockXAConnection.getXAResource();

        // 下面俩东西是分布式事务id(txid)的构成部分
        byte[] gtrid = "g12345".getBytes();
        int formatId = 1;

        try {
            // 这是说在分布式事务中的订单库的子事务的标识
            // 我们在订单库要执行的操作隶属于分布式事务的一个子事务,子事务有自己的一个标识
            byte[] bqual1 = "b00001".getBytes();
            Xid xid1 = new MysqlXid(gtrid, bqual1, formatId); // 这个xid代表了订单库中的子事务

            // 这就是说通过START和END两个操作,定义好了分布式事务中,订单库中要执行的SQL语句
            // 但是这里的SQL绝对不会执行的,只是说先定义好我要在分布式事务中,这个数据库里要执行哪些SQL语句
            orderResource.start(xid1, XAResource.TMNOFLAGS);
            PreparedStatement orderPreparedStatement = orderConnection.prepareStatement(
                    "INSERT INTO `order` (id, create_time, order_no, product_id,  pay_count) " +
                            "VALUES (1, NOW(), 1, 1, 1)");
            orderPreparedStatement.execute();
            orderResource.end(xid1, XAResource.TMSUCCESS);

            // 这是说在分布式事务中的库存库的子事务的标识
            // 大家看下,库存库的子事务的xid中的,gtrid和formatId是一样的,bqual是不一样的
            // 在一个分布式事务中,涉及到多个数据库的子事务,每个子事务的txid,有一部分是一样的,一部分是不一样的
            byte[] bqual2 = "b00002".getBytes();
            Xid xid2 = new MysqlXid(gtrid, bqual2, formatId);
            // 这就是说通过START和END两个操作,定义好了分布式事务中,库存库中要执行的SQL语句
            stockResource.start(xid2, XAResource.TMNOFLAGS);
            PreparedStatement stockPreparedStatement = stockConnection.prepareStatement(
                    "UPDATE stock SET total_count = total_count - 1 where id = 1");
            stockPreparedStatement.execute();
            stockResource.end(xid2, XAResource.TMSUCCESS);

            // 到这里为止,其实还啥都没干呢,不过就是定义了分布式事务中的两个库要执行的SQL语句罢了

            // 2PC的阶段一:向两个库都发送prepare消息,执行事务中的SQL语句,但是不提交
            int orderPrepareResult = orderResource.prepare(xid1);
            int stockPrepareResult = stockResource.prepare(xid2);

            // 2PC的阶段二:两个库都发送commit消息,提交事务
            // 如果两个库对prepare都返回ok,那么就全部commit,对每个库都发送commit消息,完成自己本地事务的提交
            if (orderPrepareResult == XAResource.XA_OK
                    && stockPrepareResult == XAResource.XA_OK) {
                orderResource.commit(xid1, false);
                stockResource.commit(xid2, false);
            } else {
                // 如果如果不是所有库都对prepare返回ok,那么就全部rollback
                orderResource.rollback(xid1);
                stockResource.rollback(xid2);
            }
        } catch (XAException e) {
            e.printStackTrace();
        }
    }
}

日志输出:

Thu Jul 07 14:41:56 CST 2022 DEBUG: Executing XA statement: XA START 0x673132333435,0x623030303031,0x1
Thu Jul 07 14:41:56 CST 2022 DEBUG: Executing XA statement: XA END 0x673132333435,0x623030303031,0x1
Thu Jul 07 14:41:56 CST 2022 DEBUG: Executing XA statement: XA START 0x673132333435,0x623030303032,0x1
Thu Jul 07 14:41:56 CST 2022 DEBUG: Executing XA statement: XA END 0x673132333435,0x623030303032,0x1
Thu Jul 07 14:41:56 CST 2022 DEBUG: Executing XA statement: XA PREPARE 0x673132333435,0x623030303031,0x1
Thu Jul 07 14:41:56 CST 2022 DEBUG: Executing XA statement: XA PREPARE 0x673132333435,0x623030303032,0x1
Thu Jul 07 14:41:56 CST 2022 DEBUG: Executing XA statement: XA COMMIT 0x673132333435,0x623030303031,0x1
Thu Jul 07 14:41:56 CST 2022 DEBUG: Executing XA statement: XA COMMIT 0x673132333435,0x623030303032,0x1

查看数据库:

mysql> use tx_msg_order;
mysql> select * from `order`;
+---------------+---------------------+---------------+------------+-----------+
| id            | create_time         | order_no      | product_id | pay_count |
+---------------+---------------------+---------------+------------+-----------+
|             1 | 2022-07-07 06:41:56 | 1             |          1 |         1 |
+---------------+---------------------+---------------+------------+-----------+

mysql> use tx_msg_stock;
mysql> select * from stock;
+----+------------+-------------+
| id | product_id | total_count |
+----+------------+-------------+
|  1 |       1001 |        9999 |
|  2 |          1 |        9995 |
+----+------------+-------------+
2 rows in set (0.00 sec)

(3)JTA 事务

JTA (Java Transaction API):为 J2EE 平台提供了分布式事务服务的能力。

JTAXAA especificação é a Javaversão da XAespecificação , ou seja, a DTPinterface de interação do modelo especificada na especificação é abstraída nos métodos na Javainterface e que tipo de função deve ser implementada por cada método.

JTAA interface principal definida, no javax.transactionpacote :

  • TransactionInterface: Permite que o aplicativo controle o início, suspensão, confirmação, reversão, etc. da transação. Chamado por Javaum programa cliente ou .EJB
  • TransactionManagerInterface: usada pelo servidor de aplicativos para gerenciar o estado da transação
  • TransactionInterface: usada para realizar operações de transação relacionadas
  • XAResourceInterface: usada para coordenar o trabalho de gerenciadores de transações e gerenciadores de recursos em um ambiente de transações distribuídas
  • XidInterface: JavaMapeamento

Adoção JTA+ Atomikosestrutura de transação distribuída: a ideia subjacente também é o 2PCprincípio

  • JTA: Fornece principalmente um gerenciador de transações, ou seja, um mecanismo para controle de processos de transações distribuídas
  • AtomikosFramework: fornece suporte para fontes de DataSourcedados

Análise da estrutura do Atomikoscódigo-fonte :

2022-07-1018-16-52.png

Combate real: simule fazer um pedido para reduzir o estoque

Tome também como exemplo a simulação de fazer um pedido e reduzir o estoque: endereço do projeto

Pilha de tecnologia: SpringBoot+ Atomikos+JDBCTemplate

  1. Adicione as dependências correspondentes:pom
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>
  1. application.propertiesconfigurar
spring.jta.enabled=true

spring.jta.atomikos.datasource.primary.xa-properties.url=jdbc:mysql://localhost:3306/tx_msg_order?useUnicode=true&characterEncoding=UTF-8&useOldAliasMetadataBehavior=true&autoReconnect=true&failOverReadOnly=false&useSSL=false
spring.jta.atomikos.datasource.primary.xa-properties.user=test
spring.jta.atomikos.datasource.primary.xa-properties.password=test
spring.jta.atomikos.datasource.primary.xa-data-source-class-name=com.mysql.cj.jdbc.MysqlXADataSource
spring.jta.atomikos.datasource.primary.unique-resource-name=tx_msg_order
spring.jta.atomikos.datasource.primary.max-pool-size=25
spring.jta.atomikos.datasource.primary.min-pool-size=3
spring.jta.atomikos.datasource.primary.max-lifetime=20000
spring.jta.atomikos.datasource.primary.borrow-connection-timeout=10000

spring.jta.atomikos.datasource.secondary.xa-properties.url=jdbc:mysql://localhost:3306/tx_msg_stock?useUnicode=true&characterEncoding=UTF-8&useOldAliasMetadataBehavior=true&autoReconnect=true&failOverReadOnly=false&useSSL=false
spring.jta.atomikos.datasource.secondary.xa-properties.user=test
spring.jta.atomikos.datasource.secondary.xa-properties.password=test
spring.jta.atomikos.datasource.secondary.xa-data-source-class-name=com.mysql.cj.jdbc.MysqlXADataSource
spring.jta.atomikos.datasource.secondary.unique-resource-name=tx_msg_stock
spring.jta.atomikos.datasource.secondary.max-pool-size=25
spring.jta.atomikos.datasource.secondary.min-pool-size=3
spring.jta.atomikos.datasource.secondary.max-lifetime=20000
spring.jta.atomikos.datasource.secondary.borrow-connection-timeout=10000
  1. Configuração de várias fontes de dados
@Configuration
public class DataSourceConfiguration {

    @Primary
    @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() {
        return new AtomikosDataSourceBean();
    }
    @Bean
    public JdbcTemplate orderJdbcTemplate(@Qualifier("primaryDataSource") DataSource primaryDataSource) {
        return new JdbcTemplate(primaryDataSource);
    }
    @Bean
    public JdbcTemplate stockJdbcTemplate(@Qualifier("secondaryDataSource") DataSource secondaryDataSource) {
        return new JdbcTemplate(secondaryDataSource);
    }
}
  1. Código comercial
@Service
@RequiredArgsConstructor
public class OrderService {

    private final JdbcTemplate orderJdbcTemplate;
    private final JdbcTemplate stockJdbcTemplate;

    @Transactional(rollbackFor = Exception.class)
    public void createOrder() {

        orderJdbcTemplate.update("INSERT INTO `order` (id, create_time, order_no, product_id,  pay_count) " + " VALUES (2, NOW(), 2, 1, 1)");

        stockJdbcTemplate.update("UPDATE stock SET total_count = total_count - 1 where id = ?", 1);
    }
}
  1. teste: liguecreateOrder()
# 程序日志输出:开启 logging.level.root=DEBUG

2022-07-1003-49-56.png

Gerar log tx: transaction-logssob o diretório

{"id":"127.0.0.1.tm165739505160000001","wasCommitted":true,"participants":[{"uri":"127.0.0.1.tm1","state":"COMMITTING","expires":1657395061620,"resourceName":"tx_msg_order"},{"uri":"127.0.0.1.tm2","state":"COMMITTING","expires":1657395061620,"resourceName":"tx_msg_stock"}]}
{"id":"127.0.0.1.tm165739505160000001","wasCommitted":true,"participants":[{"uri":"127.0.0.1.tm1","state":"TERMINATED","expires":1657395061641,"resourceName":"tx_msg_order"},{"uri":"127.0.0.1.tm2","state":"TERMINATED","expires":1657395061641,"resourceName":"tx_msg_stock"}]}

Ver banco de dados:

mysql> select * from `order`;
+---------------+---------------------+---------------+------------+-----------+
| id            | create_time         | order_no      | product_id | pay_count |
+---------------+---------------------+---------------+------------+-----------+
|             1 | 2022-07-07 06:41:56 | 1             |          1 |         1 |
|             2 | 2022-07-09 19:30:51 | 2             |          1 |         1 |
+---------------+---------------------+---------------+------------+-----------+



Estou participando do recrutamento do programa de assinatura de criadores da Comunidade de Tecnologia Nuggets, clique no link para se cadastrar e enviar .

おすすめ

転載: juejin.im/post/7118689819923841061