第 3 章 事务支持
3.1 Mycat 里的数据库事务
Mycat 目前没有出来跨分片的事务强一致性支持,目前单库内部可以保证事务的完整性,如果跨库事务,在执行的时候任何分片出错,可以保证所有分片回滚,但是一旦应用发起 commit 指令,无法保证所有分片都成功考虑到某个分片挂的可能性不大所以称为弱 xa。
3.2 XA 事务原理
分布式事务处理( Distributed Transaction Processing , DTP )指一个程序或程序段,在一个或多个资源如数据库或文件上为完成某些功能的执行过程的集合,分布式事务处理的关键是必须有一种方法可以知道事务在任何地方所做的所有动作,提交或回滚事务的决定必须产生统一的结果(全部提交或全部回滚)。X/Open 组织(即现在的 Open Group )定义了分布式事务处理模型。 X/Open DTP 模型( 1994 )包括应用程序( AP )、事务管理器( TM )、资源管理器( RM )、通信资源管理器( CRM )四部分。一般,常见的事务管理器( TM )是交易中间件,常见的资源管理器( RM )是数据库,常见的通信资源管理器( CRM )是消息中间件,下图是 X/Open DTP 模型:
一般的编程方式是这样的:
- 配置 TM,通过 TM 或者 RM 提供的方式,把 RM 注册到 TM。可以理解为给 TM 注册 RM 作为数据源。一个 TM 可以注册多个 RM。
- AP 从 TM 获取资源管理器的代理(例如:使用 JTA 接口,从 TM 管理的上下文中,获取出这个 TM 所管理的 RM 的 JDBC 连接或 JMS 连接)
- AP 向 TM 发起一个全局事务。这时,TM 会通知各个 RM。XID(全局事务 ID)会通知到各个 RM。
- AP 通过 1 中获取的连接,直接操作 RM 进行业务操作。这时,AP 在每次操作时把 XID(包括所属分支的信息)传递给 RM,RM 正是通过这个 XID 与 2 步中的 XID 关联来知道操作和事务的关系的。
- AP 结束全局事务。此时 TM 会通知 RM 全局事务结束。
- 开始二段提交,也就是 prepare - commit 的过程。
- XA 协议(XA Specification),指的是 TM 和 RM 之间的接口,其实这个协议只是定义了 xa_和 ax_系列的函数原型以及功能描述、约束和实施规范等。至于 RM 和 TM 之间通过什么协议通信,则没有提及,目前知名的数据库,如 Oracle, DB2 等,都是实现了 XA 接口的,都可以作为 RM。Tuxedo、TXseries 等事务中间件可以通过XA 协议跟这些数据源进行对接。JTA(Java Transaction API)是符合 X/Open DTP 的一个编程模型,事务管理和资源管理器支架也是用了 XA 协议。
下面两个图片分别给出了 XA 成功与失败的两种情况,首先是 XA 事务成功的流程图:
然后,是 XA 事务失败的流程图:
XA 事务的关键在于 TM 组件,其中的难点技术点如下:
第二段提交时,当 RM1 commit 完成了,而 RM2 commit 还没有完成,这时 TM 需要进行协调,当 RM2恢复以后,重新提交之前没有 Commit 的事务,或者自动回滚之前 Rollback 的事务。
因此 TM 需要记录 XA 事务的状态,以及在各个 RM 上的执行情况,这个日志文件需要存储在可靠的地方,用来进行 XA 事务异常之后的补救工作。
在 The XA Specification 里的 2.3 小节:Transaction Completion and Recovery 明确提到 TM 是要记录日志的:
代码语言:javascript复制In Phase 2, the TM issues all RMs an actual request to commit or roll back the transaction branch,
as the case may be. (Before issuing requests to commit, the TM stably records the fact that it decided to
commit, as well as a list of all involved RMs.) All RMs commit or roll back changes to shared resources
and then return status to the TM. The TM can then discard its knowledge of the global transaction.
TM 是一定要把事务的信息,比如 XID,哪个 RM 已经完成了等保存起来的。只有当全部的 RM 提交或者回滚完后,才能丢弃这些事务的信息。
于是我们明白 TM 是一个单点,要非常可靠才行。
以 Java 分布式事务的开源 TM 组件 atomikos 为例,它是通过在应用的目录下生成日志文件来保证,如果失败,在重启后可以通过日志来完成未完成的事务。
Mycat 未来计划以 Zookeeper 作为 XA 事务的日志存储手段,实现 TM 角色以支持 XA 事务.
3.3 XA 事务的问题和 MySQL 的局限
XA 事务的明显问题是 timeout 问题,比如当一个 RM 出问题了,那么整个事务只能处于等待状态。这样可以会连锁反应,导致整个系统都很慢,最终不可用,另外 2 阶段提交也大大增加了 XA 事务的时间,使得 XA 事务无法支持高并发请求。
避免使用 XA 事务的方法通常是最终一致性。
举个例子,比如一个业务逻辑中,最后一步是用户账号增加 300 元,为了减少 DB 的压力,先把这个放到消息队列里,然后后端再从消息队列里取出消息,更新 DB。那么如何保证,这条消息不会被重复消费?或者重复消费后,仍能保证结果是正确的?在消息里带上用户帐号在数据库里的版本,在更新时比较数据的版本,如果相同则加上 300;比如用户本来有 500 元,那么消息是更新用户的钱数为 800,而不是加上 300;
另外一个方式是,建一个消息是否被消费的表,记录消息 ID,在事务里,先判断消息是否已经消息过,如果没有,则更新数据库,加上 300,否则说明已经消费过了,丢弃。
前面两种方法都必须从流程上保证是单方向的。
其实严格意义上,用消息队列来实现最终一致性仍然有漏洞,因为消息队列跟当前操作的数据库是两个不同的资源,仍然存在消息队列失败导致这个账号增加 300 元的消息没有被存储起来(当然复杂的高级的消息队列产品可以避免这种现象,但仍然存在风险),而第二种方式则由于新的表跟之前的事务操作的表示在一个 Database中,因此不存在上述的可能性。
MySQL 的 XA 事务,长期以来都存在一个缺陷: MySQL 数据库的主备数据库的同步,通过 Binlog 的复制完成。而 Binlog 是 MySQL 数据库内部 XA 事务的协调者,并且 MySQL 数据库为 binlog 做了优化——binlog 不写 prepare 日志,只写 commit 日志。所有的参与节点 prepare 完成,在进行 xa commit 前 crash。crash recover 如果选择 commit 此事务。由于binlog 在 prepare 阶段未写,因此主库中看来,此分布式事务最终提交了,但是此事务的操作并未写到 binlog中,因此也就未能成功复制到备库,从而导致主备库数据不一致的情况出现。
代码语言:javascript复制Prior to MySQL 5.7.7, XA transactions were not compatible with replication. This was because an XA
transaction that was in PREPARED state would be rolled back on clean server shutdown or client
disconnect. Similarly, an XA transaction that was in PREPARED state would still exist in PREPARED state
in case the server was shutdown abnormally and then started again, but the contents of the transaction
could not be written to the binary log. In both of these situations the XA transaction could not be
replicated correctly.
In MySQL 5.7.7 and later, there is a change in behavior and an XA transaction is written to the
binary log in two parts. When XA PREPARE is issued, the first part of the transaction up to XA PREPARE
is written using an initial GTID. A XA_prepare_log_event is used to identify such transactions in the
binary log. When XA COMMIT or XA ROLLBACK is issued, a second part of the transaction containing
only the XA COMMIT or XA ROLLBACK statement is written using a second GTID. Note that the initial
part of the transaction, identified by XA_prepare_log_event, is not necessarily followed by its XA
COMMIT or XA ROLLBACK, which can cause interleaved binary logging of any two XA transactions. The
two parts of the XA transaction can even appear in different binary log files. This means that an XA
transaction in PREPARED state is now persistent until an explicit XA COMMIT or XA ROLLBACK
statement is issued, ensuring that XA transactions are compatible with replication.
3.4 XA 事务使用指南
Mycat 从1.6.5 版本开始支持标准 XA 分布式事务,考虑到 mysql5.7 之前版本 xa 的2 个bug,所以推荐最佳搭配 XA 功能使用 mysql 5.7 版本。
Mycat 实现 XA 标准分布式事务,mycat 作为 xa 事务协调者角色,即使事务过程中 mycat 宕机挂掉,由于 mycat 会记录事务日志,所以 mycat 恢复后会进行事务的恢复善后处理工作。
考虑到分布式事务的性能开销比较大,所以只推荐在全局表的事务以及其他一些对一致性要求比较高的场景。
代码语言:javascript复制使用示例:
XA 操作说明
1. set autocommit=0;
XA 事务 需要设置手动提交
2. set xa=on;
使用该命令开启 XA 事务
3. insert into travelrecord(id,name)
values(1,'N'),(6000000,'A'),(321,'D'),(13400000,'C'),(59,'E');
执行相应的 SQL 语句部分
4.commit;
对事务进行提交,事务结束
3.5 保证 repeatable read
mycat 有一个特性,就是开事务之后,如果不运行 update/delete/select for update 等更新类语句 SQL 的话,不会将当前连接与当前 session 绑定。如下图所示:
这样做的好处是可以保证连接可以最大限度的复用,提升性能。
但是,这就会导致两次 select 中如果有其它的在提交的话,会出现两次同样的 select 不一致的现象,即不能 repeatable read,这会让人直连 mysql 的人很困惑,可能会在依赖repeatable read 的场景出现问题。所以做了一个开关,当 server.xml 的system 配置了strictTxIsolation=true 的时候(true),会关掉这个特性,以保证 repeatable read,加了开关后如下图所示: