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错误导致问题还是条件异常导致错误,都不会导致分布式事务问题。
作者:small-water
出处:https://www.cnblogs.com/small-water/p/17870082.html
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?