在开发中,为了降低单点压力,通常会根据业务情况进行分表分库,将表分布在不同的库中(库可能分布在不同的机器上)。在这种场景下,事务的提交会变得相对复杂,因为多个节点(库)的存在,可能存在部分节点提交失败的情况,即事务的ACID特性需要在各个不同的数据库实例中保证。比如更新db1库的A表时,必须同步更新db2库的B表,两个更新形成一个事务,要么都成功,要么都失败。
那么我们如何利用mysql实现分布式数据库的事务呢?
Mysql 为我们提供了分布式事务解决方案
5.7.7以前由于mysql的binlog中不会记录prepare事件,所以对XA事务支持并不完善。这是mysql5.7的文档
这里先声明两个概念:
- 资源管理器(resource manager):用来管理系统资源,是通向事务资源的途径。数据库就是一种资源管理器。资源管理还应该具有管理事务提交或回滚的能力。
- 事务管理器(transaction manager):事务管理器是分布式事务的核心管理者。事务管理器与每个资源管理器(resource
manager)进行通信,协调并完成事务的处理。事务的各个分支由唯一命名进行标识。
mysql在执行分布式事务(外部XA)的时候,mysql服务器相当于xa资源管理器,与mysql链接的客户端相当于事务管理器。
分布式事务原理:两阶段提交
分布式事务通常采用2PC协议,全称Two Phase Commitment Protocol。该协议主要为了解决在分布式数据库场景下,所有节点间数据一致性的问题。分布式事务通过2PC协议将提交分成两个阶段:
- prepare
- commit/rollback
阶段一为准备(prepare)阶段。即所有的参与者准备执行事务并锁住需要的资源。参与者ready时,向transaction manager报告已准备就绪。
阶段二为提交阶段(commit)。当transaction manager确认所有参与者都ready后,向所有参与者发送commit命令。
如下图所示:
因为XA 事务是基于两阶段提交协议的,所以需要有一个事务协调者(transaction manager)来保证所有的事务参与者都完成了准备工作(第一阶段)。如果事务协调者(transaction manager)收到所有参与者都准备好的消息,就会通知所有的事务都可以提交了(第二阶段)。MySQL 在这个XA事务中扮演的是参与者的角色,而不是事务协调者(transaction manager)。
Mysql的XA事务分为外部XA和内部XA
- 外部XA用于跨多MySQL实例的分布式事务,需要应用层作为协调者,通俗的说就是比如我们在PHP中写代码,那么PHP书写的逻辑就是协调者。应用层负责决定提交还是回滚,崩溃时的悬挂事务。MySQL数据库外部XA可以用在分布式数据库代理层,实现对MySQL数据库的分布式事务支持,例如开源的代理工具:网易的DDB,淘宝的TDDL等等。
- 内部XA事务用于同一实例下跨多引擎事务,由Binlog作为协调者,比如在一个存储引擎提交时,需要将提交信息写入二进制日志,这就是一个分布式内部XA事务,只不过二进制日志的参与者是MySQL本身。Binlog作为内部XA的协调者,在binlog中出现的内部xid,在crash recover时,由binlog负责提交。(这是因为,binlog不进行prepare,只进行commit,因此在binlog中出现的内部xid,一定能够保证其在底层各存储引擎中已经完成prepare)。
MySQL XA事务基本语法
XA {START|BEGIN} xid [JOIN|RESUME] 启动xid事务 (xid 必须是一个唯一值; 不支持[JOIN|RESUME]子句)
XA END xid [SUSPEND [FOR MIGRATE]] 结束xid事务 ( 不支持[SUSPEND [FOR MIGRATE]] 子句)
XA PREPARE xid 准备、预提交xid事务
XA COMMIT xid [ONE PHASE] 提交xid事务
XA ROLLBACK xid 回滚xid事务
XA RECOVER 查看处于PREPARE 阶段的所有事务
MySQL XA 是什么?
MySQL XA 是基于Open Group 的<
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事务。
下图是XA事务状态变迁图:
从分布式事务的变迁中可以看出,有两条路径可以使事务达到提交状态,有两条路径是回滚并结束事务。我们将这四条路径进行横向对比,看看每个阶段是如何实现分布式事务的ACID特性的(相关分析是在RR隔离级别下进行的,暂不考虑RC隔离级别)。
如上图可以看出,
当xa start开启事务后,DML也会在对应的RM上创建undo以及read view(该read view是instance级别的)。
当xa prepare 时会将子事务置于PREPARED状态,此时子事务已经完成事务提交前的所有准备工作(获得锁,并将PREPARED状态记录到共享表空间中,会将xa start到xa end之间操作记录在binlog中)。
当xa commit 时会在binlog中记录xa commit xid, 并将innodb中PREPARED状态转化为COMMITED状态。
当xa commit one phase 时会同时进行prepare和commit 两种操作,是在TM发现全局的分布式事务只涉及一个RM时进行的(因为不需要等待其他RM的反馈结果)。
当xa rollback在xa prepare前时,因为没有写binlog和redo,只会释放undo, read view以及lock。
当xa rollback 在xa prepare之后时,除了需要释放undo, read view以及lock,还需要binlog中记录xa rollback xid(使得从库不会提交该事务)以及innodb中将PREPARED状态转化为ROLLBACK状态。
PHP调用MYSQL XA事务示例
1、首先要确保mysql开启XA事务支持
SHOW VARIABLES LIKE '%xa%'
如果innodb_support_xa的值是ON就说明mysql已经开启对XA事务的支持了。
如果不是就执行:
SET innodb_support_xa = ON
开启
2、代码如下:
$dbtest1 = new mysqli("172.20.101.17","public","public","dbtest1")or die("dbtest1 连接失败");
$dbtest2 = new mysqli("172.20.101.18","public","public","dbtest2")or die("dbtest2 连接失败");
//为XA事务指定一个id,xid 必须是一个唯一值。
$xid = uniqid("");
//两个库指定同一个事务id,表明这两个库的操作处于同一事务中
$dbtest1->query("XA START '$xid'");//准备事务1
$dbtest2->query("XA START '$xid'");//准备事务2
try {
//$dbtest1
$return = $dbtest1->query("UPDATE member SET name='唐大麦' WHERE id=1") ;
if($return == false) {
throw new Exception("库[email protected]执行update member操作失败!");
}
//$dbtest2
$return = $dbtest2->query("UPDATE memberpoints SET point=point+10 WHERE memberid=1") ;
if($return == false) {
throw new Exception("库[email protected]执行update memberpoints操作失败!");
}
//阶段1:$dbtest1提交准备就绪
$dbtest1->query("XA END '$xid'");
$dbtest1->query("XA PREPARE '$xid'");
//阶段1:$dbtest2提交准备就绪
$dbtest2->query("XA END '$xid'");
$dbtest2->query("XA PREPARE '$xid'");
//阶段2:提交两个库
$dbtest1->query("XA COMMIT '$xid'");
$dbtest2->query("XA COMMIT '$xid'");
}
catch (Exception $e) {
//阶段2:回滚
$dbtest1->query("XA ROLLBACK '$xid'");
$dbtest2->query("XA ROLLBACK '$xid'");
die($e->getMessage());
}
$dbtest1->close();
$dbtest2->close();
XA的性能问题
XA的性能很低。一个数据库的事务和多个数据库间的XA事务性能对比可发现,性能差10倍左右。因此要尽量避免XA事务,例如可以将数据写入本地,用高性能的消息系统分发数据。或使用数据库复制等技术。只有在这些都无法实现,且性能不是瓶颈时才应该使用XA。并发高的情况下建议使用TCC或者 消息事务+最终一致性(变种TCC),这里的协调者就变成业务代码了。高并发下分布式事务的解决方案
XA 的限制
在MySQL 5.7.7 之前,MySQL一直存在一个"bug"。在事务达到PREPARED状态后,客户端断开与MySQL的连接,MySQL 会自动回滚该事务,这个行为不符合分布式事务的规范,MySQL将PREPARED的事务丢失了。之所以MySQL这么实现是因为MySQL 5.7.7 之前PREPARED的事务并不会记录到binlog中。客户端退出后会丢失该信息,如果允许再提交,那么binlog缺少事务信息,会造成主从不一致。
在MySQL 5.7.7 之后,MySQL 新增了一个XA_prepare_log_event的事件,会把xa start到xa prepare中间的操作记录到Binlog中。Slave读取Relay log 进行回放,当SQL Thread读取到PREPARED的事务后,在读取xa commit或者xa rollback前,会进行一个类似客户端断开的操作,继续读取后续的事务信息,不会阻塞SQL Thread的执行。从以上的结果看,Oracle在MySQL 5.7.7 上确实完美的解决了MySQL XA一直存在的一个"bug"。
XA 的实践
MySQL XA 要满足线上高并发的访问要求,在使用时还需要解决两个问题:分布式死锁问题和分布式读一致性问题。分布式死锁问题是指MySQL Server 是可以检测和解决单个MySQL实例中的死锁问题,但涉及到跨越多个MySQL 实例的分布式事务时候,需要程序层面实现死锁的检测和解决。分布式读一致性问题是指MySQL的read view 也是实例级别的,对于全局分布式事务来说无法实现读一致,只能通过select ... lock in share mode在读请求上加锁的串行化隔离级别来实现,这必然会带来并发性能的下降。这就需要在程序层面构建全局的read view来实现全局的MVCC 。
--------------割------------------------
1. 三阶段提交的定义
三阶段提交(Three-phase commit),也叫三阶段提交协议(Three-phase commit protocol),是二阶段提交(2PC)的改进版本。
所谓的三个阶段分别是:询问,然后再锁资源,最后真正提交。
- 第一阶段:CanCommit
- 第二阶段:PreCommit
- 第三阶段:Do Commit
与两阶段提交不同的是,三阶段提交有两个改动点。
- 引入超时机制 - 同时在协调者和参与者中都引入超时机制。
- 在第一阶段和第二阶段中插入一个准备阶段,保证了在最后提交阶段之前各参与节点的状态是一致的。
转载自:https://blog.csdn.net/soonfly/article/details/70677138
转载自:https://www.jianshu.com/p/7003d58ea182