19.分布式事务编程

InnoDB存储引擎存储了对于XA事务的支持,并通过XA事务来支持分布式事务的实现。

1.什么是分布式事务?

分布式事务是指允许多个独立的事务资源(transactional resources)参与到一个全局事务的事务中,其中独立的事务资源可以理解为不同的数据库。
全局事务要求在其中所有参与的事务要么都提交,要么都回滚,这对于事务原有的ACID要求又有了提高。
注意:在使用分布式事务时,InnoDB存储引擎的事务隔离级别必须设置为SERIALIZABLE

2.什么时XA事务?

XA事务允许不同数据库之间的分布式事务,如一台服务器是MySQL数据库的,另一台是Oracle数据库的,可能还有一台服务器是SQL Server数据库的,只要参与到全局事务中的每个节点都支持XA事务即可。
分布式事务可能在银行系统的转账中比较常见,比如用户麻子(账户id为1)需要从上海转10000元到北京的用户四郎(账户id为2)的存储卡中:

Bank@shanghai:
UPDATE account SET money = money - 10000 WHERE acctId=1;

Bank@Beijing:
UPDATE account SET money=money+10000 WHERE acctId=2;

在上面的情况下,一定需要使用分布式事务来保证数据安全。如果发生的操作不能都提交或回滚,那么任何一个节点出现问题都会导致严重的后果,例如第一个事务提交了,但第二个事务回滚了,那么就会出现麻子的钱扣了但四郎没收到钱的情况,反之就会出现麻子的钱没扣但四郎的账户多了钱的情况,这两种情况都是违背法律的。

在这里XA事务就可以解决上面的问题,那么XA事务是如何解决的呢?**

1)首先XA事务由一个或多个资源管理器(resource manager),一个事务管理器(transaction manager)以及一个应用程序(application program)组成。

  • 资源管理器:它提供访问事务资源的方法。通常一个数据库就是一个资源管理器。
  • 事务管理器:协调参与全局事务中的各个事务。需要和参与到全局事务中的所有资源管理器进行通信。
  • 应用程序:定义事务的边界,指定全局事务中的操作。
    在MySQL数据库的分布式事务中,资源管理器就是MySQL数据库,事务管理器为连接到MySQL服务器的客户端。 其中三者的关系如下图:
    在这里插入图片描述

2)其次就是分布式事务的执行过程
分布式事务使用两端式提交(two-phase commit)的方式进行的。
在第一阶段,所有参与全局事务的节点都开始准备(PREPARE),所有节点告诉事务管理器他们准备好了提交了。
然后第二阶段事务管理器就会告诉资源管理器执行ROLLBACK还是COMMIT。如果其中任何一个节点显示不能提交,则所有的节点都会被告知需要回滚。
MySQL数据库XA事务的SQL语法如下:

XA {
    
    START|BEGIN} xid [JOIN|RESUME]
XA END xid [SUSPEND [FOR MIGRATE]]
XA PREPARE xid
XA COMMIT xid [ONE PHASE]
XA ROLLBACK xid
XA RECOVER

在这里插入图片描述

在单个节点上运行分布式事务没有太大实际意义,但是要在MySQL数据库的命令下演示多个节点参与的分布式事务也是行不通的。
注意:通常来说,都是通过编程语言来完成分布式事务的操作的。

3)所以下面展示如何使用JTA(jdk的JTA API,可以很好的支持MySQL的分布式事务)来调用MySQL的分布式事务:
首先在自己本地创建两个数据库(我是用的已经有的):
在这里插入图片描述

然后在这两个数据库中创建相同的表:

create table accountBank(
  userid int(11),
  username varchar(20),
  money decimal(17,2),
  primary key(userid)
);
接着在employees库插入数据:insert into accountBank values(1,'麻子',1000000);
peixun数据库插入:insert into accountBank values(2,'四郎',100);

通过上面的操作,我们已经具备分布式事务编程的场景了(不同的数据库之间保证事务数据一致性):
下面进行代码编写:
首先编写资源管理器唯一标识id生成器:

package XaStudy;

import javax.transaction.xa.Xid;

public class MyXid implements Xid {
    
    
    public int formatId;
    public byte gtRid[];
    public byte bqUal[];
    public MyXid(){
    
    

    }
    public MyXid(int formatId,byte gtRid[],byte bqUal[]){
    
    
        this.formatId=formatId;
        this.gtRid=gtRid;
        this.bqUal=bqUal;
    }
    @Override
    public int getFormatId() {
    
    
        return formatId;
    }

    @Override
    public byte[] getGlobalTransactionId() {
    
    
        return gtRid;
    }

    @Override
    public byte[] getBranchQualifier() {
    
    
        return gtRid;
    }
}

然后编写全局事务管理器:

package XaStudy;

import com.mysql.cj.jdbc.MysqlXADataSource;

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

public class XADemo {
    
    
    public static MysqlXADataSource GetDataSource(String commString,String user,String passwd){
    
    
        try{
    
    
            MysqlXADataSource ds=new MysqlXADataSource();
            ds.setUrl(commString);
            ds.setUser(user);
            ds.setPassword(passwd);
            return ds;
        }catch (Exception e){
    
    
            e.printStackTrace();
            return null;
        }
    }

    public static void main(String[] args) {
    
    
        //数据库1地址
        String connString1="jdbc:mysql://localhost:3306/employees";
        //数据库2地址
        String connString2="jdbc:mysql://localhost:3306/peixun";
        try{
    
    
            //创建资源管理器1
            MysqlXADataSource ds1=GetDataSource(connString1,"root","yanjiadou");
            //创建资源管理器2
            MysqlXADataSource ds2=GetDataSource(connString2,"root","yanjiadou");

            //获得资源管理器1的连接
            XAConnection xaConnection1=ds1.getXAConnection();
            XAResource xaResource1=xaConnection1.getXAResource();
            Connection connection1=xaConnection1.getConnection();
            Statement statement1=connection1.createStatement();
            //获得资源管理器2的连接
            XAConnection xaConnection2=ds2.getXAConnection();
            XAResource xaResource2=xaConnection2.getXAResource();
            Connection connection2=xaConnection2.getConnection();
            Statement statement2=connection2.createStatement();

            //创建资源管理器1的唯一标识
            Xid xid1=new MyXid(100,new byte[]{
    
    0x01},new byte[]{
    
    0x02});
            //创建资源管理器2的唯一标识
            Xid xid2=new MyXid(100,new byte[]{
    
    0x11},new byte[]{
    
    0x12});

            try{
    
    
              xaResource1.start(xid1,XAResource.TMNOFLAGS);
              statement1.execute("update accountBank set money=money-10000 where userid=1");
              xaResource1.end(xid1,XAResource.TMSUCCESS);

              xaResource2.start(xid2,XAResource.TMNOFLAGS);
              statement2.execute("update accountBank set money=money+10000 where userid=2");
              xaResource2.end(xid2,XAResource.TMSUCCESS);
              //资源管理器1第一阶段准备
              int ret1=xaResource1.prepare(xid1);
              //资源管理器2第一阶段准备
              int ret2=xaResource2.prepare(xid2);
              //事务管理器提交
              if(ret1 == XAResource.XA_OK&&ret2 == XAResource.XA_OK){
    
    
                  xaResource1.commit(xid1,false);
                  xaResource2.commit(xid2,false);
              }
            }catch (Exception e){
    
    
                e.printStackTrace();
            }
        }catch (Exception e){
    
    
            e.printStackTrace();
        }
    }
}

执行之:
在这里插入图片描述

看看数据库是否发生变化:
在这里插入图片描述

我们可以看到麻子的账户确实少了10000元,四郎的账户确实多了10000元。

4)上面的是正常的情况,下面如果测试下不正确的情况下,全局事务管理器是否会防止事故发生

我们故意将58行的:
statement2.execute("update accountBank set money=money+10000 where userid=2");
改成:
statement2.execute("update accountBank set money=money+10000 where userid=1");

然后执行,发现数据库中果然出现了事故:
在这里插入图片描述
在这里插入图片描述

这里由于某种原因导致四郎的userid出错了(但sql没错),实际上,四郎并没有接受到钱,但麻子的账户却扣了钱。

那么这里我们该如何解决呢?
我们可以先获取受影响的行来进行判断是否进行是否进行提交:

try{
    
    
  xaResource1.start(xid1,XAResource.TMNOFLAGS);
  int num1=statement1.executeUpdate("update accountBank set money=money-10000 where userid=1");
  xaResource1.end(xid1,XAResource.TMSUCCESS);
  xaResource2.start(xid2,XAResource.TMNOFLAGS);
  int num2=statement2.executeUpdate("update accountBank set money=money+10000 where userid=1");
  xaResource2.end(xid2,XAResource.TMSUCCESS);
  if(num1==1&&num2==1){
    
    
      //资源管理器1第一阶段准备
      int ret1=xaResource1.prepare(xid1);
      //资源管理器2第一阶段准备
      int ret2=xaResource2.prepare(xid2);
      //事务管理器提交
      if(ret1 == XAResource.XA_OK&&ret2 == XAResource.XA_OK){
    
    
          xaResource1.commit(xid1,false);
          xaResource2.commit(xid2,false);
      }
  }
}catch (Exception e){
    
    
    e.printStackTrace();
}

这样,不论是sql错误导致问题还是条件异常导致错误,都不会导致分布式事务问题。

猜你喜欢

转载自blog.csdn.net/c1776167012/article/details/121437703
今日推荐