分布式事务XA+JTA+TCC++RocketMQ+SEATA原理加实战

事务大家都很熟悉,小范围来说就是数据库为了保证数据的一致性而做的操作规范和限制。
常见的就是AICD四大特性:

  • 原子性(atomicity):一个事务中的所有操作,不可分割,要么全部成功,要么全部失败;
  • 一致性(consistency):一个事务执行前与执行后数据的完整性必须保持一致;
  • 隔离性(isolation):一个事务的执行,不能被其他事务干扰,多并发时事务之间要相互隔离;
  • 持久性(durability):一个事务一旦被提交,它对数据库中数据的改变是永久性的。

分布式事务顾名思义就是分布式系统当中的事务如何保证这些特性。
分布式事务分为两块来理解:

  1. 分布式:那么就会涉及到多个数据库(支持事务),内存和磁盘数据等硬件都不在一起,想从操作系统层面控制是不可能了。那么只能找出一个协调者来组织本地事务一起工作。
  2. 事务:必须本地数据库支持(无论关系型数据库还是非关系型数据库)

分布式系统有一个很著名的CAP理论:

  • C 一致性:all nodes see the same data at the same time

所有节点在相同时间看到的数据是相同的,这个可以看作是强一致性,而不是最终一致性。

  • A 可用性:reads and writes always succeed 服务一直可用,而且是在有效时间内。下图是针对可用性的一些标准

在这里插入图片描述

  • P 分区容错性:the system continues to operate despite arbitrary message loss or failure of part of the system

分布式系统在遇到节点或者网络分区故障的时候,仍然可以提供一致性和可用性的服务。
很好的一篇文章介绍cap的
https://blog.csdn.net/w372426096/article/details/80437198

提供cap原理可以知道,不可能cap全部保证的。所以引出了base理论


Base理论 :

  • 基本可用(Basically Available):分布式系统在出现故障时,保证核心可用,允许损失部分可用性。(响应时间上的损失、功能上的损失
  • 软状态/中间状态(Soft State):系统中的数据允许存在中间状态,中间状态不影响系统的整体可用性。(支付中处理中等)
  • 最终一致性(Eventually Consistent):系统中的数据不可一直处于软状态,必须在有时间期限,在期限过后应当保证数据的一致性。(支付中变为支付成功

经过以上对分布式系统的cap和base理论的理解,可以得知分布式事务只能保证最终一致性
常见的分布式事务解决方案都不能脱离这两个理论。

分布式事务一些大神也给出了一些理论知识

两阶段提交2PC

  • 2PC是一个强一致性中心化原子提交协议
  • 在这个协议里面有两个角色 一个协调者和多个参与者

         --协调者(Coordinater):事务管理器(TM)

         --参与者(participants):资源管理器(RM)

  • 两个阶段 分别是投票(预提交)阶段和提交(执行)阶段

在这里插入图片描述
存在的问题
性能问题/同步阻塞
无论是第一阶段还是第二阶段,所以参与者和协调者资源都是被锁定的状态,这个过程比较慢的话,会影响整个系统的性能。
单点故障/数据不一致
一旦协调者在某个阶段出现故障,参与者将处与一直阻塞的状态,尤其第二阶段则参与者处以资源锁定状态。
2pc出现单点问题的三种情况
协调者正常,参与者宕机
参与者中的一个或者几个宕机就无法给协议者反馈,那么进入超时机制,一旦参与者在指定时间内会没有反馈,协调者就发送终止事务请求。
协调者宕机,参与者正常
一旦协调者宕机,无论处于哪个阶段,所以参与者将都会阻塞。需要进入协调者备份,且记录操作日志。检测一段时间还没有恢复,则进行激活备份机器,查看操作日志,重新发起请求。
协调者宕机,参与者也宕机
此种情况又可以分为三种情况:

  • 发生在第一阶段

因为发生在第一阶段,从参与者选择一个作为协调者,重新发起第一阶段和第二阶段接可以了。

  • 发生在第二阶段且挂了的参与者没有收到commit提交请求

选出新的协调者重新执行第一阶段和第二阶段

  • 发生在第二阶段且挂了的参与者收到了commit提交请求

此时会出现数据不一致的现象部分提交了,部分没有提交,此种情况2pc是无法解决。


三阶段提交3pc:是对两阶段2pc提交的一个改进版本。
主要是针对阻塞/性能问题进行了改进,2pc存在的问题是当协调者故障的时候,参与者会一直阻塞直到协调者恢复
主要改动点

  1. 引入超时机制,无论是参与者还是协调者都引入。
  2. 加入一个准备阶段,在第一阶段和第二阶段之间。以此保证在最后的提交阶段所有节点都能够保持一致。

主要分为3个阶段

  • canCommit

协调者发起是否可以进行提交的请求,参与者本地获取锁,成功返回消息给协调者

  • preCommit

协调者在收到可以进行预提交消息之后,进行预提交

  • doCommit

真正的执行阶段,进行提交或者回滚

XA理论 

  • XA是由X/Open组织提出的分布式事务的规范。 XA规范主要定义了 (全局)事务管理器™ 和 (局部)资源管理器(RM) 之间的接口。主流的关系型数据库产品都是实现了XA接口的。
  • XA接口是双向的系统接口,在事务管理器 ™ 以及一个或多个资源管理器(RM) 之 间形成通信桥梁。
  • 是一套跨语言的标准XA事务的处理模型

在这里插入图片描述

xa的流程
在这里插入图片描述
XA的2阶段提交 2PC

在这里插入图片描述

来一个xa实战

环境jdk1.8 idea 构建 springboot项目

pom.xml配置:

    <!-- db相关配置开始-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.0.8</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jta-atomikos</artifactId>
    </dependency>
    <!--db相关配置结束 -->


    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>org.junit.vintage</groupId>
                <artifactId>junit-vintage-engine</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>RELEASE</version>
        <scope>test</scope>
    </dependency>

项目源码:

package com.liu.transactionmarket;

import com.mysql.jdbc.jdbc2.optional.MysqlXADataSource;
import com.mysql.jdbc.jdbc2.optional.MysqlXid;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import javax.sql.XAConnection;
import javax.transaction.xa.XAResource;
import javax.transaction.xa.Xid;
import java.sql.Connection;
import java.sql.Statement;

/**
 * xa实现
 */
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = TransactionMarketApplication.class)
public class XATransactionn {

    @Test
    public void xATest() throws Exception {
        XAConnection xaConnection1 = null;
        XAConnection xaConnection2 = null;
        XAResource xaResource1 = null;
        XAResource xaResource2 = null;
        Connection connection1 = null;
        Connection connection2 = null;
        Statement statement1 = null;
        Statement statement2 = null;
        Xid xid1 = null;
        Xid xid2 = null;
        try {
            //数据源1
            MysqlXADataSource mysqlXADataSource1 = new MysqlXADataSource();
            mysqlXADataSource1.setUrl(“jdbc:mysql:172.20.xxxx.xxx:3306/law_online?useUnicode=true&characterEncoding=UTF8&zeroDateTimeBehavior=convertToNull”);
            mysqlXADataSource1.setUser(“aaaa”);
            mysqlXADataSource1.setPassword(“aaaaxxxxx”);
            //数据源2
            MysqlXADataSource mysqlXADataSource2 = new MysqlXADataSource();
            mysqlXADataSource2.setUrl("jdbc:mysql://192.168.xx.xxx:6606/sccs?useUnicode=true&characterEncoding=UTF8&zeroDateTimeBehavior=convertToNull");
            mysqlXADataSource2.setUser("xxxx");
            mysqlXADataSource2.setPassword("xxxxxx");

            //获取数据库链接1
            xaConnection1 = mysqlXADataSource1.getXAConnection();
            xaResource1 = xaConnection1.getXAResource();
            connection1 = xaConnection1.getConnection();
            statement1 = connection1.createStatement();

            //获取数据库链接2
            xaConnection2 = mysqlXADataSource2.getXAConnection();
            xaResource2 = xaConnection2.getXAResource();
            connection2 = xaConnection2.getConnection();
            statement2 = connection2.createStatement();

            //创建分支事务id
            xid1 = new MysqlXid("72891".getBytes(), "72891".getBytes(), 1);
            xid2 = new MysqlXid("72892".getBytes(), "72892".getBytes(), 1);

            //分支事务1关联事务处理sql
            xaResource1.start(xid1, XAResource.TMNOFLAGS);
            int result1 = statement1.executeUpdate("update base_info set status=0 where id=1");
            xaResource1.end(xid1, XAResource.TMSUCCESS);
            //分支事务2关联事务处理sql
            xaResource2.start(xid2, XAResource.TMNOFLAGS);
            int result2 = statement2.executeUpdate("update sccs_borrower_info set sex=1 where id=57");
            xaResource2.end(xid2, XAResource.TMSUCCESS);

            //两阶段提交协议第一阶段-预提交
            //分支事务1预提交
            int isOk1 = xaResource1.prepare(xid1);
            //分支事务2预提交
            int isOk2 = xaResource2.prepare(xid2);
            
            //两阶段提交协议第二阶段-提交/回滚
            if (XAResource.XA_OK == isOk1 & XAResource.XA_OK == isOk2) {
                xaResource1.commit(xid1, false);
                xaResource2.commit(xid2, false);
            } else {
                xaResource1.rollback(xid1);
                xaResource2.rollback(xid2);
            }
        } catch (Exception e) {
            //记录错误日志,进行回滚
            xaResource1.rollback(xid1);
            xaResource2.rollback(xid2);
        } finally {
            if (null != xaConnection1) {
                xaConnection1.close();
            }
            if (null != xaConnection2) {
                xaConnection2.close();
            }
        }
    }
}

JTA概念

  • Java Transaction API :java根据XA规范提供的事务处理标准
  • 目的:统一一个API 简化学习
  • JTA实战:

在这里插入图片描述
在这里插入图片描述

开源的实现 TM 提供商:

  • Java Open Transaction Manager (JOTM)
  • JBoss TS Bitronix Transaction Manager (BTM)
  • Atomikos
  • Narayana

RM的提供商:
在这里插入图片描述

JTA-Atomikos实战

package com.liu.transactionmarket;

import com.atomikos.icatch.jta.UserTransactionManager;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.jta.atomikos.AtomikosDataSourceBean;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import javax.transaction.SystemException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Properties;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = TransactionMarketApplication.class)
public class JTATransaction {
    @Test
    public void jtaTest() {

        Connection lawConnetion = null;
        Connection sccsConnection = null;
        String sql = null;
        int lawResult;
        int sccsResult;

        AtomikosDataSourceBean lawOnlineDataSource = new AtomikosDataSourceBean();
        lawOnlineDataSource.setUniqueResourceName("lawOnlineDataSource");
        lawOnlineDataSource.setXaDataSourceClassName("com.mysql.jdbc.jdbc2.optional.MysqlXADataSource");
        Properties properties1 = new Properties();
        properties1.setProperty("url", "jdbc:mysql://172.20.xx.xx:3306/law_online?useUnicode=true&characterEncoding=UTF8&zeroDateTimeBehavior=convertToNull");
        properties1.setProperty("user", "bbbxxx");
        properties1.setProperty("password", "aaaxxx");
        lawOnlineDataSource.setXaProperties(properties1);
        
        AtomikosDataSourceBean sccsDataSource = new AtomikosDataSourceBean();
        sccsDataSource.setUniqueResourceName("sccsDataSource");
        sccsDataSource.setXaDataSourceClassName("com.mysql.jdbc.jdbc2.optional.MysqlXADataSource");
        Properties properties2 = new Properties();
        properties2.setProperty("url", "jdbc:mysql://192.168.xx.xxx:6606/sccs?useUnicode=true&characterEncoding=UTF8&zeroDateTimeBehavior=convertToNull");
        properties2.setProperty("user", "xxx");
        properties2.setProperty("password", "xxxx!");
        sccsDataSource.setXaProperties(properties2);

        UserTransactionManager userTransactionManager = new UserTransactionManager();
        userTransactionManager.setForceShutdown(true);

        try {
            //开启一个全局事务
            userTransactionManager.begin();

            sccsConnection = sccsDataSource.getConnection();
            sql = "update sccs_borrower_info set sex=1 where id=57";
            PreparedStatement clmgPreparedStatement = sccsConnection.prepareStatement(sql);
            sccsResult = clmgPreparedStatement.executeUpdate();

            lawConnetion = lawOnlineDataSource.getConnection();
            sql = "update base_info set status=0 where id=1";
            PreparedStatement lawPreparedStatement = lawConnetion.prepareStatement(sql);
            lawResult = lawPreparedStatement.executeUpdate();
            
            if (lawResult > 0 && sccsResult > 0) {
                userTransactionManager.commit();
            } else {
                userTransactionManager.rollback();
            }
        } catch (Exception e) {
            try {
                userTransactionManager.rollback();
            } catch (SystemException ex) {
                ex.printStackTrace();
            }
        } finally {
            if (null != lawConnetion) {
                try {
                    lawConnetion.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if (null != sccsConnection) {
                try {
                    sccsConnection.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

XA或者JTA这种实现是强一致性的体现
按照互联网的实际场景都是最终一致性,柔性事务
常见的分布式事务组件

  • Tcc tcc-byte,tcc-transaction,gts(seata)
  • 本地消息表
  • 支持事务的消息中间件,比如Rocketmq
  • Atomikos

接下来介绍一下柔性事务 Tcc-transaction
tcc原理还是来源于两阶段提交
在这里插入图片描述
业务架构
在这里插入图片描述

Saga(补偿)

Saga的核心是补偿,与TCC不同的是Saga不需要Try,而是直接进行confirmcancel操作。

  • Confirm:依次按顺序依次执行资源操作,各个资源直接处理本地事务,如无问题,二阶段什么都不用做;
  • Cancel:异常情况下需要调用的补偿事务(逆操作)来保证数据的一致性。

可以看出,Saga和TCC有些类似,都是补偿型事务

优势:

  • 一阶段提交本地事务,无锁,高性能;
  • 事件驱动模式,参与者可异步执行,高吞吐;
  • 应用成本低,补偿服务易于实现;

劣势:

  • 无法保证隔离性(脏写)

可靠消息最终一致性(RocketMQ)

有一些情况,服务间调用时异步的,服务A将消息发送到MQ,服务B进行消息的消费。这时我们就需要用到可靠消息最终一致性来解决分布式事务问题。首先字面理解,

  • 可靠消息:即这个消息一定是可靠的,并且最终一定需要被消费的。
  • 最终一致性:过程中数据存在一定时间内的不一致,但超过限定时间后,需要最终会保持一致。

确保以上两点情况下,通过消息中间件(RocketMQ)来完成分布式事务处理,因为RocketMQ支持事务消息,可以方便的让我们进行分布式事务控制。
因此首先需要了解一下,RocketMQ的事务消息的原理。

摘自网络

half message:半消息,此时消息不能被consumer所发现和消费,需producer进行二次消息确认。

  • producer发送half messageMQ Server
  • producer根据MQ Server应答结果判断half message是否发送成功;
  • producer处理本地事务;
  • producer发送最终确认消息commit / rollback

    commitconsumer对消息可见并进行消费;

    rollbackdiscard抛弃消息,consumer无法进行消息消费;

  • 如遇异常情况下step4最终确认消息为达到MQ ServerMQ Server会定期查询当前处于半消息状态下的消息,主动进行消息回查来询问producer该消息的最终状态;
  • producer检查本地事务执行的最终结果;
  • producer根据检查到的结果,再次提交确认消息,MQ Server仍然按照step4进行后续操作。

事务消息发送对应步骤1、2、3、4,事务消息回查对应步骤5、6、7。
由以上步骤可以看出通过事务性消息的两步操作,避免了消息直接投递所产生一些问题。最终投递到MQ Server的消息,是真实可靠且必须被消费的。

tcc-transation实战

1.配置整理
1.1 首先到github上面下载最新的tcc-transaction项目源码
下载地址:https://github.com/changmingxie/tcc-transaction
最新版本地址
https://github.com/changmingxie/tcc-transaction/tree/master-1.2.x
1.2 修改源码里面的主pom.xml配置文件
在这里插入图片描述
修改tcc-transaction-server里面的pom文件
在这里插入图片描述

1.3 清理项目并且打包install到本地
在这里插入图片描述
1.4 进行本地dubbo测试验证
1.4.1执行sql脚本
在这里插入图片描述
一共有4个脚本文件,需要创建4个数据库
为什么是这些表?结合实际业务场景就明白了。
tcc-transaction :示例演示在下完订单后,使用红包帐户和资金帐户来付款,红包帐户服务和资金帐户服务在不同的系统中。示例中,有两个SOA提供方,一个是CapitalTradeOrderService,代表着资金帐户服务,另一个是RedPacketTradeOrderService,代表着红包帐户服务.
create_db_red.sql:红包账户相关的表,总共包括两个表,红包账户表red_red_packet_account和红包交易表red_trade_order

create_db_cap.sql: 资金账户相关的表,总共包括两个表,商家账户表cap_capital_account和账户交易表cap_trade_order

create_db_ord.sql: 订单相关的表,总共包括四个表,商家表ord_shop、商品表ord_product、订单商品数量表ord_order_line和订单详细信息表ord_order

create_db_tcc.sql:tcc-transaction框架事务相关的表,总共包括四个表,订单事务表tcc-transaction_ord、红包事务表tcc-transaction_red、账户事务表tcc_transaction_cap和事务重试表tcc_transaction_ut

create_db_ord 数据源配置
在这里插入图片描述
create_db_cap 数据源配置
在这里插入图片描述
transaction_red 数据源配置
在这里插入图片描述

数据源都配置完成之后启动应用
tcc_transaction_dubbo_capital
启动成功 hello tcc transacton dubbo sample capital

tcc_transaction_dubbo_redpacket
启动成功hello tcc transacton dubbo sample red packet

tcc-transaction-dubbo-order
启动成功
在这里插入图片描述

点击商品列表进行支付购买
在这里插入图片描述

由于没有配置好数据源导致异常了,刚好也验证事务回滚机制
红包没有减少,账户也没有减少。看一下各表的状态
在这里插入图片描述
数据表不存在
在这里插入图片描述

tcc-transaction的核心代码在tcc-transaction-core里面
其中非常重要的两个拦截器ResourceCoordinatorInterceptor,CompensableTransactionInterceptor
在这里插入图片描述

demo操作成功的页面
在这里插入图片描述

后续继续和spring整合以及 seata的研究

seata是阿里巴巴开源的一个分布式事务中间件(解决方案)

阿里开源的Seata 是一款分布式事务解决方案,提供了 AT、TCC、SAGA XA 事务模式。

Seata架构的亮点主要有几个:

  • 应用层基于SQL解析实现了自动补偿,从而最大程度的降低业务侵入性;
  • 将分布式事务中TC(事务协调者)独立部署,负责事务的注册、回滚(支持多种注册中心形式以及本地文件形式);
  • 通过全局锁实现了写隔离与读隔离。

中文官方文档:http://seata.io/zh-cn/docs/overview/what-is-seata.html
首先下载seata demo
https://github.com/seata/seata-samples
使用zk作为注册中心的配置文件的修改localhost:2181改为自己的zk服务器地址配置
在这里插入图片描述

分别在三个业务库执行sql,并修改jdbc.properties配置
注意:如果分为多个业务库,则每个业务库都需要执行undo_log.sql

CREATE TABLE undo_log (
    id bigint(20) NOT NULL AUTO_INCREMENT,
    branch_id bigint(20) NOT NULL,
    xid varchar(100) NOT NULL,
    context varchar(128) NOT NULL,
    rollback_info longblob NOT NULL,
    log_status int(11) NOT NULL,
    log_created datetime NOT NULL,
    log_modified datetime NOT NULL,
    PRIMARY KEY (id),
    UNIQUE KEY ux_undo_log (xid,branch_id)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

下载服务器端server或者修改源码seata-server
server有两种配置启动方式 默认是file也可以改为db
下面看一下db方式持久化方式
修改conf/file.conf
改动两个地方store.mode=‘db’
同时db配置 url, user,password需要修改为执行持久化表

    drop table global_table;
    create table global_table (
        xid varchar(128)not null,
        transaction_id bigint,
        status tinyint not null,
        application_id varchar(64),
        transaction_service_group varchar(64),
        transaction_name varchar(128),
        timeout int,
        begin_time bigint,
        application_data varchar(2000),
        gmt_create datetime,
        gmt_modified datetime,
        primary key(xid),
        key idx_gmt_modified_status (gmt_modified, status),
        key idx_transaction_id (transaction_id)
    );

    –- the table to store BranchSession data
    drop table branch_table;
    create table branch_table (
        branch_id bigint not null,
        xid varchar(128)not null,
        transaction_id bigint,
        resource_group_id varchar(128),
        resource_id varchar(256),
        lock_key varchar(256),
        branch_type varchar(8),
        status tinyint,
        client_id varchar(64),
        application_data varchar(2000),
        gmt_create datetime,
        gmt_modified datetime,
        primary key(branch_id),
        key idx_xid (xid)
    );

    –- the table to store lock data
    drop table lock_table;
    create table lock_table (
        row_key varchar(128)not null,
        xid varchar(128),
        transaction_id long,
        branch_id long,
        resource_id varchar(256),
        table_name varchar(64),
        pk varchar(128),
        gmt_create datetime,
        gmt_modified datetime,
        primary key(row_key)
    );

在这里插入图片描述

配置改为之后启动server
本地db持久化方式启动
./seata-server.sh -h 127.0.0.1 -p 8091 -m db
在这里插入图片描述
服务端启动成功之后,那么客户端demo就可以运行了。
非别按照顺序启动DubboAccountServiceStarter,DubboStorageServiceStarter,DubboOrderServiceStarter,最后启动DubboBusinessTester

在这里插入图片描述
正常执行之后,可以看到成功日志

账户扣减日志
在这里插入图片描述

库存扣减日志
在这里插入图片描述

插入订单成功日志
在这里插入图片描述

到此简单的demo完成。

原文https://blog.csdn.net/qingmuqingnian/article/details/106221705

参考:https://www.jianshu.com/p/60a100eee74a

史上最强Tomcat8性能优化

阿里巴巴为什么能抗住90秒100亿?--服务端高并发分布式架构演进之路

B2B电商平台--ChinaPay银联电子支付功能

学会Zookeeper分布式锁,让面试官对你刮目相看

SpringCloud电商秒杀微服务-Redisson分布式锁方案

查看更多好文,进入公众号--撩我--往期精彩

一只 有深度 有灵魂 的公众号0.0

猜你喜欢

转载自blog.csdn.net/a1036645146/article/details/109241727