数据库事务是多个SQL构成一个业务整体,必须同时提交或同时回滚。而分布式事务中,一个大操作由多个小操作组成,各个小操作处于不同的物理节点(或进程),这些小操作必须作为一个整体,同时提交或同时回滚。本文总结了事务的特性,以及 MySQL/Redis/MQ 的分布式事务的具体解决方案。
作者:王克锋
出处:https://kefeng.wang/2018/03/01/distributed-transaction/
版权:自由转载-非商用-非衍生-保持署名,转载请标明作者和出处。
1 存在场景
根本上说,最终归结于数据存储被拆分,需要保证数据存储一致性。
- 数据存储被拆分:数据库(MySQL)分库分表、缓存(Redis)分节点分库、消息(RocketMQ)多节点
- 业务功能被拆分:多子系统、SOA 服务化应用
2 事务的 ACID 特性
2.1 事务的 ACID
参考资料:ACID - 维基百科
ACID 的概念在 ISO/IEC 10026-1:1992 文件的第四段内有所说明。
ACID 是指数据库管理系统(DBMS)在进行写操作时,为保证事务正确可靠,必须具备的四个特性:
- 原子性(Atomicity): 事务是个
不可拆分的整体
,其中的各操作要么全部执行,要么全部回滚至执行前的状态,不会处于中间状态; - 一致性(Consistency): 无论事务开始之前,还是事务结束之后(无论提交还是回滚),数据库都能保证各数据一致性;
- 隔离性(Isolation): 允许多个事务并发对数据进行读写操作,各事务相互隔离互不干扰,一个事务看不到另一个事务尚未提交的数据变化;
- 持久性(Durability): 事务结束后(无论提交还是回滚),结果数据都会持久化,即使系统故障,数据也不会丢失。
2.2 事务隔离级别
参考资料:事务隔离 - 维基百科
对于隔离性,事务隔离(Transaction Isolation)定义了一个事务的操作结果,在何时以何种方式,可以被其他事务看到。
不同 DBMS 默认的隔离级别是不一样的(MySQL 默认为 Repeatable Read),大多数据库允许用户指定。
根据 ANSI/ISO SQL 规范,事务的隔离级别分为:
- 串行化(Serializable): 最高的隔离级别(性能最差),在选定对象上的读锁和写锁持有直到事务结束后才释放,要求“范围锁”(Range-Locks);
- 可重复的读(Repeatable Read): 含义是同一行数据读取多次的结果相同。对选定对象的读锁和写锁一直保持到事务结束,不要求“范围锁”;
- 提交的读(Read Committed): 对选定对象的写锁一直保持到事务结束,但是读锁在读操作完成后马上释放,不要求“范围锁”;
- 未提交的读(Read Uncommitted): 最低的隔离级别,允许“脏读”(Dirty Reads),一个事务可以看到其他事务“尚未提交”的修改。
3 MySQL 分布式事务(XA)
3.1 MySQL 分布式构架方案
MySQL 的分布式可以采用两种架构方案:
- 中间代理: 由第三方软件实现 MySQL 节点的汇合,比如 MyCAT;
- 客户端: 客户端实现 MySQL 节点的汇合,该方案对业务有侵入,下面指的是本情景;
3.2 分布式事务协议 XA
XA 是 X/Open DTP 组织 1994 年定义的分布式事务协议,协议规范如下:
THE XA SPECIFICATION、THE XA+ SPECIFICATION, VERSION 2
采用两阶段提交(2PC, Two Phase Commitment)协议:
- Prepare: 征询每个节点,其事务是否可提交,若可提交则资源本地记录操作并响应为同意,若不可提交则回滚并响应为拒绝;
- Commit/Rollback: 所有节点都同意时,才全部提交;只要一个节点拒绝就全部回滚。
XA 协议模型的组成部分:
- 应用程序(AP): 实现业务功能,比如 MySQL 客户端程序;
- 事务管理器(TM): 事务的全局调度者,协调各方提交或回滚,MySQL 客户端程序兼当事务管理器;
- 资源管理器(RM): 数据库(MySQL)或消息中间件(MQ)实现 XA 接口函数,被 TM 调用;
XA 定义了 TM/RM 间的接口规范(函数),TM 通过接口控制事务的开始和结束、提交和回滚等。
三阶段提交协议(3PC, Three Phase Commitment) 用于缓解两阶段提交的以下缺点(但不论2PC还是3PC,都应尽量避免分布式事务):
- 单点问题: 事务管理器(TM)可能发生单点故障;
- 同步阻塞: TM 协调各 AP/RM,期间处于阻塞状态,性能很低(可低至 10%)。
3.3 MySQL XA 相关说明
官方资料:XA Transactions
MySQL XA 分为两类:
- 内部 XA: 用于同一数据库实例下跨多个引擎的事务,由 binlog 作为协调者;
- 外部 XA: 用于跨多数据库实例的分布式事务,需要应用层介入作为协调者,下面指的是本情境;
早期 MySQL 版本对分布式事务的支持是有缺陷的,会话客户端异常断开时事务会丢失。
相关链接:Xa recovery and client disconnection、Binlogging XA-prepared transaction
直至 5.7.7 才稳定,现在的最新版本是 5.7.21(2018-01-14)。
3.4 MySQL XA 功能启用
- 服务器参数必须开启 XA(默认为开启):
innodb_support_xa=on
- 存储引擎必须使用 InnoDB
- 事务隔离级别必须设置为 SERIALIZABLE;
3.5 MySQL XA 操作步骤
MySQL> XA BEGIN|START 'trx'; ## 事务块开始
## DML 操作(INSERT/DELETE/UPDATE/SELECT)
MySQL> XA END 'trx'; ## 事务块结束
MySQL> XA PREPARE 'trx'; ## 准备事务并写入 binlog(若有 Slave 节点,binlog 也会被同步过去)
MySQL> XA RECOVER; ## 【可选】查看处于 PREPARE 阶段的 XA 事务
MySQL> XA COMMIT|ROLLBACK 'trx'; ## 事务提交或回滚(若有 Slave 节点,也同时提交)
4 Redis 分布式事务
分布式锁相关命令 SETNX key value
含义为 SET if Not eXists。
若 key 不存在,则设置并返回1(成功);若 key 已存在,则不作任何动作,返回 0(失败)。
Redis 为单进程单线程模式,使用队列将并发请求变成串行请求,保证了所有客户端不冲突。
使用 Redis 分布式锁的伪代码如下:
retval = <SETNX key value> ## 尝试占用
if (retval == 1) { ## 占用成功
expire key timeout ## 以便本客户端异常中止时,该 KEY 会超时自动释放
## 主体操作
delete key ## 释放锁
} else { ## 占用未遂
}
5 RocketMQ 分布式事务
官方资料:RocketMQ Documentation
RocketMQ 中间件支持事务消息,可确保本地数据与MQ服务器上数据一致:
- 发送一个准备消息,收到消息接口的回查地址;
- 执行本地操作,执行结果可能是成功或者失败;
- 确认消息发送,通过第一步得到的接口地址执行回查,并修改状态(根据本地事务的成功与否,修改状态为已提交或已回滚)。
6 JTA 事务
JDBC 事务局限于单个数据库,它在 JDBC Connection 中实现事务管理器。
而 JTA(Java Transaction API)提供了多个数据库(甚至包括其他资源如 MQ)的事务管理能力。
6.1 概述
官方资料:Java Transaction API、JSR 907
维基百科:Java事务API
JTA 规范定义了分布式事务中的事务管理器(TM)与资源管理器(RM)、应用程序(AP)等的 Java 接口。
JTA 是以 X/Open XA 体系结构为基础设计的。
6.2 API 组成
相关 Java 包有:javax.transaction、javax.transaction.xa
- javax.transaction.UserTransaction: 应用程序接口(AP),提供了编程控制事务边界的能力;
- javax.transaction.TransactionManager: 事务管理器接口(TM);
- javax.transaction.xa.XAResource: XA 协议的标准 Java 映射(RM)。
6.3 实例
SpringBoot 中使用 JTA 处理分布式事务(作者 Freud Kang)