本文为《MySQL归纳学习》专栏的第十三篇文章,同时也是关于《MySQL事务》知识点的开篇文章。
欢迎您阅读《MySQL探秘:解码事务、分布式事务与长事务的奥秘》。事务在MySQL中是如何实现的?分布式事务又是如何达成的呢?我们又该如何避免陷入一些不良的事务习惯?更别提那些让人头疼的长事务问题。本篇文章将揭示这些问题的答案,让你深入了解事务分类、分布式事务的实现以及长事务的特性。
事务分类
从事务理论的角度来看,可以把事务分为以下五种类型:
- 扁平事务(Flat Transactions)
- 带有保存点的扁平事务(Flat Transactions with Savepoints)
- 链事务(Chained Transactions)
- 嵌套事务(Nested Transactions)
- 分布式事务(Distributed Transactions)
上述五大事务分类的讲解,推荐阅读《MySQL技术内幕 InnoDB存储引擎 第2版》章节中的事务,仅作了解即可。
对于InnoDB存储引擎 来说,其支持扁平事务 ,带保存点的事务 ,链事务 ,分布式事务 。对于嵌套事务,其原生不支持。因此对有并行事务需求的用户来说,MySQL数据库或InnoDB存储引擎就显得无能为力,然而用户仍可以通过带保存点的事务来模拟串行的嵌套事务。
事务实现方式
原子性实现
undo log
名为回滚日志,是实现原子性的关键,当事务回滚时能够撤销所有已经成功执行的sql语句,它需要记录你要回滚的相应日志信息。 例如
- (1)当你 delete 一条数据的时候,就需要记录这条数据的信息,回滚的时候,insert这条旧数据
- (2)当你 update 一条数据的时候,就需要记录之前的旧值,回滚的时候,根据旧值执行update操作
- (3)当年 insert 一条数据的时候,就需要这条记录的主键,回滚的时候,根据主键执行delete操作
undo log
记录了这些回滚需要的信息,当事务执行失败或调用了rollback,导致事务需要回滚,便可以利用undo log中的信息将数据回滚到修改之前的样子。
隔离性实现
利用的是锁和MVCC机制。
关于锁,分为表级锁和行级锁,某一事务先获取到锁,则其他事务因获取不到锁,无法操作数据,进入阻塞状态。
至于MVCC,即多版本并发控制(Multi Version Concurrency Control),一个行记录有多个版本的快照数据,这些快照数据在undo log
中。
如果一个事务读取的行正在做 DELELE 或者 UPDATE 操作,读取操作不会等行上的锁释放,而是读取该行的快照版本。
持久性实现
redo log(重做日志)用来实现事务的持久性,即事务 ACID 中的D。其由两部分组成∶一是内存中的重做日志缓冲(redo log buffer),其是易失的;二是重做日志文件(redo log fle),其是持久的。 redo log 是 innodb 引擎层实现的,并不是所有引擎都有。
一致性实现
从数据库层面,数据库通过原子性、隔离性、持久性来保证一致性。也就是说ACID四大特性之中,C(一致性)是目的,A(原子性)、I(隔离性)、D(持久性)是手段,是为了保证一致性,数据库提供的手段。数据库必须要实现AID三大特性,才有可能实现一致性。例如,原子性无法保证,显然一致性也无法保证。
但是,如果你在事务里故意写出违反约束的代码,一致性还是无法保证的。例如,你在转账的例子中,你的代码里故意不给B账户加钱,那一致性还是无法保证。因此,还必须从应用层角度考虑。
从应用层面,通过代码判断数据库数据是否有效,然后决定回滚还是提交数据!
上面提到的 redo log、undo log、锁和MVCC 机制,后续会有专门的章节进行讲解。
分布式事务
基于 2pc 的分布式事务
分布式事务有多种实现方式,如 2PC(二阶段提交)、3PC(三阶段提交)、TCC(补偿事务)等,MySQL 是基于 2PC 实现的分布式事务,下面介绍 2PC 分布式事务实现方式。
两阶段提交:Two-Phase Commit , 简称 2PC,为了使基于分布式系统架构下的所有节点在进行事务提交时保持一致性而设计的一种算法。
2PC 的算法思路可以概括为,参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报,决定各参与者是否要提交操作还是中止操作。这里的参与者可以理解为 Resource Manager (RM),协调者可以理解为 Transaction Manager(TM)。
下图说明了 RM 和 TM 在分布式事务中的运作过程:
- 第一阶段提交:TM 会发送 Prepare 到所有 RM 询问是否可以提交操作,RM 接收到请求,实现自身事务提交前的准备工作并返回结果。
- 第二阶段提交:根据 RM 返回的结果,所有 RM 都返回可以提交,则 TM 给 RM 发送 commit 的命令,每个 RM 实现自己的提交,同时释放锁和资源,然后 RM 反馈提交成功,TM 完成整个分布式事务;如果任何一个 RM 返回不能提交,则涉及分布式事务的所有 RM 都需要回滚。
MySQL 分布式事务 XA
InnoDB 存储引擎提供了对 XA 事务的支持,并通过XA 事务来支持分布式事务的实现。分布式事务指的是允许多个独立的事务资源(transactional resources)参与到一个全局的事务中。事务资源通常是关系型数据库系统,但也可以是其他类型的资源。全局事务要求在其中的所有参与的事务要么都提交,要么都回滚,这对于事务原有的 ACID 要求又有了提高。另外,在使用分布式事务时,InnoDB 存储引/擎的事务隔商级别必须设置为 SERIALIZABLE。
MySQL 分布式事务 XA 是基于上面的 2pc 框架实现,下面详细介绍 MySQL XA 相关内容。
XA 规范中分布式事务由 AP、RM、TM 组成:
- **资源管理器(Resource Managers):**提供访问事务资源的方法。通常一个数据库就是一个资源管理器
- **事务管理器(Transaction Manager):**协调参与全局事务中的各个事务。需要和参与全局事务的所有资源管理器进行通信
- **应用程序(Application Program):**定义事务的边界,指定全局事务中的操作
在MySQL数据库的分布式事务中,资源管理器就是MySQL数据库,事务管理器为连接MySQL服务器的客户端。
分布式事务使用两段式提交的方式:
- **第一阶段,**所有参数全局事务的节点都开始准备(PREPARE),告诉事务管理器它们准备好提交了。
- **第二阶段,**事务管理器告诉资源管理器质性ROLLBACK还是COMMIT。如果任何一个节点显示不能提交,则所有的节点都被告知需要回滚。
可见与本地事务不同的是,分布式事务需要多一次的PREPARE操作,待收到所有节点的同一信息后,再进行COMMIT或是ROLLBACK操作。
避免不好的事务习惯
1、在循环中提交SQL
我们先以 DBA 的角度来循环执行 SQL,下面有3个存储过程。
SQL
CREATE TABLE `t_color` (
`id` int(11) NOT NULL,
`c` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE DEFINER=`root`@`localhost` PROCEDURE `idata1`()
begin
declare i int;
set i=1;
start transaction;
while(i<=100000) do
insert into t_color values(i,i);
commit;
set i=i+1;
end while;
end
CREATE DEFINER=`root`@`localhost` PROCEDURE `idata2`()
begin
declare i int;
set i=1;
while(i<=100000) do
insert into t_color values(i,i);
set i=i+1;
end while;
end
CREATE DEFINER=`root`@`localhost` PROCEDURE `idata3`()
begin
declare i int;
set i=1;
start transaction;
while(i<=100000) do
insert into t_color values(i,i);
set i=i+1;
end while;
commit;
end
上述3个存储过程中,其中 idata1 和 idata2 区别在于多了 start transaction 和 commit 语句,其实有没有都不影响,因为 InnoDB 引擎默认为自动提交,所以有没有这两条语句结果都一样。
接下来我们分别执行这三个存储过程,对比耗时。
SQL
call idata1();
truncate table t_color;
call idata2();
truncate table t_color;
call idata3();
显然,第三种方法要快得多!这是因为每一次提交都要写一次重做日志,存储过程 1oadl 和 load2 实际写了10000 次重做日志文件,而对于存储过程 load3 来说,实际只写了 1次。可以对第二个存储过程 load2 的调用进行调整,同样可以达到存储过程 load3 的性能,如:
SQL
start transaction;
call idata2();
commit;
不过,作为后端开发,我们更多接触的是在代码中做批量操作,所以需要注意的是,能批量操作就不要在循环中执行插入或更新操作。
2、使用自动提交
MySQL事务的默认启动方式是自动提交模式(autocommit mode)。在自动提交模式下,每个单独的SQL语句都被视为一个独立的事务,即每个SQL语句执行后都会立即提交事务。这意味着如果你不明确地开始一个事务,每个SQL语句的更改都将立即生效,并且不能回滚。要使用显式的事务控制,可以通过使用BEGIN或START TRANSACTION语句来开始一个事务,并通过COMMIT或ROLLBACK语句来结束或回滚事务。
MySQL 的事务手动启动方式有以下几种:
- 显式启动事务语句, begin 或 start transaction。配套的提交语句是 commit,回滚语句是 rollback。begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个操作 InnoDB 表的语句,事务才真正启动。
- set autocommit=0,这个命令会将这个线程的自动提交关掉。意味着如果你只执行一个 select 语句,这个事务就启动了,而且并不会自动提交。这个事务持续存在直到你主动执行 commit 或 rollback 语句,或者断开连接。
- 如果你想要马上启动一个事务,可以使用 start transaction with consistent snapshot 这个命令。
如果你是 DBA,那么建议在复杂的情况下执行 SQL 语句采用手动提交事务的形式;如果你是 Java 后端开发,碰巧使用的 Spring 框架,那么在使用 Spring 事务管理器的前提下,合理使用 @Transantion 注解。
当加了@Transcational注解后,进入该方法时,相当于执行 begin 或 start transaction,配套的提交语句是 commit,回滚语句是 rollback。begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个操作 InnoDB 表的语句,事务才真正启动。
3、使用自动回滚
比如说Java后台服务,关于SQL的执行大都调用 ORM 插件,比如说Mybatis、Mybatis plus等,偶尔自己写一些批量操作语句,记得在执行语句前后加上begin和commit,以及对异常的捕获并回滚。
长事务
长事务,顾名思义,指的是执行时间很长的事务。
为什么不建议使用长事务呢?
1、长事务导致对应的事务视图长时间存在,那么对应的回滚日志也是会一直存在的,这就会导致大量占用存储空间。
2、除了对回滚段的影响,长事务占有锁资源,可能拖垮整个库。
在 information_schema 库的 innodb_trx 这个表中查询长事务,比如下面这个语句,用于查找持续时间超过 60s 的事务。
SQL
select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60
如何应对长事务?
首先,从应用开发端来看:
- 确认是否使用了 set autocommit=0。这个确认工作可以在测试环境中开展,把 MySQL 的 general_log 开起来,然后随便跑一个业务逻辑,通过 general_log 的日志来确认。一般框架如果会设置这个值,也就会提供参数来控制行为,你的目标就是把它改成 1。
- 确认是否有不必要的只读事务。有些框架会习惯不管什么语句先用 begin/commit 框起来。我见过有些是业务并没有这个需要,但是也把好几个 select 语句放到了事务中。这种只读事务可以去掉。
- 查询语句优化 ,分解长事务。
- 业务连接数据库的时候,根据业务本身的预估,通过 SET MAX_EXECUTION_TIME 命令,来控制每个语句执行的最长时间,避免单个语句意外执行太长时间。
其次,从数据库端来看:
- 监控 information_schema.Innodb_trx 表,设置长事务阈值,超过就报警 / 或者 kill;
- Percona 的 pt-kill 这个工具不错,推荐使用;
- 在业务功能测试阶段要求输出所有的 general_log,分析日志行为提前发现问题;
- 如果使用的是 MySQL 5.6 或者更新版本,把 innodb_undo_tablespaces 设置成 2(或更大的值)。如果真的出现大事务导致回滚段过大,这样设置后清理起来更方便。