事务的特性
提到事务,肯定会想到ACID:
- Atomicity [ˌætəˈmɪsəti] 原子性
- Consistency [kənˈsɪstənsi] 一致性
- Isolation [ˌaɪsəˈleɪʃn] 隔离性
- Durability [ˌdʊrəˈbɪləti] 持久性
Atomicity 原子性
事务的原子性是指,一组操作要么全部成功,要么全部失败。原子性由 undo log 日志 来保证, 因为 undo log 记录着数据修改前的信息。
比如,insert 一条数据的时候,undo log 就会记录一条对应的 delete 语句; update 一条记录的时候,那么 undo log 机会记录一条之前旧值的 update 语句。
如果执行事务中出现异常的情况,那么执行 “回滚”,InnoDB 引擎就是利用 undo log 记录下的 日志,来将数据 “恢复” 到事务开始之前的样子。
Isolation 隔离性
隔离性是指,在事务 并发 执行时,他们内部的操作不能相互干扰。如果多个事务可以在同一时刻 操作同一份数据,那么就可能产生 脏读
、重复读
、幻读
的问题。于是,在事务中需要存在一定 的隔离。在InnoDB引擎中,定义了四种隔离级别供我们使用:
- 读未提交
- 读已提交
- 可重复读
- 串行化
Durability 持久性
持久性是指,一旦提交了事务,他对数据库的改变就应该是永久性的,也就是将数据持久化在磁盘上。 而持久性,由 redo log 日志来保证。
当要修改数据时, MySQL 是先将这条记录所在的页找到,然后把该页加载到内存中, 将对应记录进行修改。为了防止在修改完内存的时候,数据库挂掉(如果内存修改完,数据库挂掉, 那么这次修改就丢失了),MySQL 引入了 redo log,内存写完的时候,会写一份redo log, 这个 redo log 记载着这次在哪个页上做了什么修改,即便MySQL中途挂掉,还可以根据 redo log 进行重放,对数据进行恢复。
redo log 是顺序记载,写入速度很快,并且它记录的是物理修改(XX页做了XX修改),文件的体积很小, 恢复速度也快。
Consistency 一致性
一致性可以理解为使用事务的目的,而 原子性、隔离性、持久性 均是为了保障 一致性 采用的手段, 保证一致性,需要由应用程序代码来保证。
比如,如果在事务过程中出现了异常,就需要回滚事务,而不是强行提交事务而导致数据不一致。
事务可能产生的问题
简单来说,事务就是要保证一组数据库操作,要么全部成功,要么全部失败。在 MySQL 中,事务支持是在引擎层实现的。 MySQL 是一个支持多引擎的系统,但并不是所有的引擎都支持事务。比如 MySQL 原生的 MyISAM 引擎就不支持事务, 这也是 MyISAM 被 InnoDB 取代的重要原因之一。
脏读 (dirty read)
脏读指的是读到了其他事务未提交的数据,未提交意味着这些数据可能会回滚,也就是可能最终不会存到数据库中, 也就是不存在的数据。读到了并一定最终存在的数据,这就是脏读。
可重复读 (non-repeatable read)
可重复读指的是在一个事务内,最开始读到的数据和事务结束前的任意时刻读到的同一批数据都是一致的。通常针对 数据更新(UPDATE)操作。
不可重复读 (repeatable read)
对比可重复读,不可重复读指的是在同一事务内,不同的时刻读到的同一批数据可能是不一样的,可能会受到其他事 务的影响,比如其他事务改了这批数据并提交了。通常针对数据更新(UPDATE)操作。
幻读 (phantom read)
幻读 (phantom read [ˈfæntəm]) 是针对数据插入(INSERT)操作来说的。假设事务A对某些行的内容作了更改, 但是还未提交,此时事务B插入 了与事务A更改前的记录相同的记录行,并且在事务A提交之前先提交了,而这时,在事 务A中查询,会发现好像刚刚的更改对于某些数据未起作用,但其实是事务B刚插入进来的,让用户感觉很魔幻,感觉出 现了幻觉,这就叫幻读。
事务隔离级别
SQL 标准定义了四种隔离级别,MySQL 全都支持。这四种隔离级别分别是:
读未提交(READ UNCOMMITTED)
读未提交是指,一个事务还没提交时,它做的变更就能被别的事务看到。
读提交 (READ COMMITTED)
读提交是指,一个事务提交之后,它做的变更才会被其他事务看到。
可重复读 (REPEATABLE READ)
可重复读是指,一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别 下,未提交变更对其他事务也是不可见的。
串行化 (SERIALIZABLE)
串行化,顾名思义是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须 等前一个事务执行完成,才能继续执行。
从上往下,隔离强度逐渐增强,性能逐渐变差。采用哪种隔离级别要根据系统需求权衡决定,其中,可重复读是 MySQL 的默认级别。
事务隔离其实就是为了解决上面提到的脏读、不可重复读、幻读这几个问题,下面展示了 4 种隔离级别对这三个问题的解决程度。
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交 | 可能 | 可能 | 可能 |
读提交 | 不可能 | 可能 | 可能 |
可重复读 | 不可能 | 不可能 | 可能 |
串行化 | 不可能 | 不可能 | 不可能 |
只有串行化的隔离级别解决了全部这 3 个问题,其他的 3 个隔离级别都有缺陷。
事务相关指令
# 查询当前事务级别
SELECT @@tx_isolation;
show variables like 'tx_isolation';
# 或者
SELECT @@transaction_isolation;
show variables like 'transaction_isolation';
# 查询当前正在执行的事务
select * from information_schema.innodb_trx;
# 修改事务隔离级别
set global transaction isolation level read committed;
# set [作用域] transaction isolation level [事务隔离级别]
# SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}
# 查看事务是否自动提交的配置
SELECT @@autocommit;
show variables like 'autocommit';
# 事务的开启和结束由下面三个指令控制
begin; # begin 回车后并不会立即开启事务,而是在执行真正的CRUD语句后才开启事务
commit; # 提交事务,操作生效并持久化
rollback; # 回滚,撤销所有操作(可能出现脏读)
事务问题复现详解
读未提交
MySQL 事务隔离其实是依靠锁来实现的,加锁自然会带来性能的损失。而读未提交隔离级别是不加锁的,所以它的性能 是最好的,没有加锁、解锁带来的性能开销。但有利就有弊,这基本上就相当于裸奔啊,所以它连脏读的问题都没办法解决。 任何事务对数据的修改都会第一时间暴露给其他事务,即使事务还没有提交。
下面来做个简单实验验证一下,首先设置全局隔离级别为读未提交。
set global transaction isolation level read uncommitted;
设置完成后,只对之后新起的 session 才起作用,对已经启动 session 无效。如果用 shell 客户端那就要重新连接 MySQL,如果用 Navicat 那就要创建新的查询窗口。
启动两个事务,分别为事务A和事务B,在事务A中使用 update 语句,修改 age 的值为10,初始是1 ,在执行完 update 语句之后,在事务B中查询 user 表,会看到 age 的值已经是 10 了,这时候事务A还没有提交,而此时事务B有可能拿着 已经修改过的 age=10 去进行其他操作了。在事务B进行操作的过程中,很有可能事务A由于某些原因,进行了事务回滚操作, 那其实事务B得到的就是脏数据了,拿着脏数据去进行其他的计算,那结果肯定也是有问题的。
顺着时间轴往表示两事务中操作的执行顺序,重点看图中 age 字段的值。
读提交
既然读未提交没办法解决脏数据问题,那么就有了读提交。读提交就是一个事务只能读到其他事务已经提交过的数据,也就是其他 事务调用 commit 命令之后的数据。那脏数据问题迎刃而解了。
读提交事务隔离级别是大多数流行数据库的默认事务隔离界别,比如 Oracle,但是不是 MySQL 的默认隔离界别。
我们继续来做一下验证,首先把事务隔离级别改为读提交级别。
set global transaction isolation level read committed;
之后需要重新打开新的 session 窗口,也就是新的 shell 窗口才可以。
同样开启事务A和事务B两个事务,在事务A中使用 update 语句将 id=1 的记录行 age 字段改为 10。此时,在事务B中使用 select 语句进行查询,我们发现在事务A提交之前,事务B中查询到的记录 age 一直是1,直到事务A提交,此时在事务B中 select 查询,发现 age 的值已经是 10 了。
这就出现了一个问题,在同一事务中(本例中的事务B),事务的不同时刻同样的查询条件,查询出来的记录内容是不一样的,事务A 的提交影响了事务B的查询结果,这就是不可重复读,也就是读提交隔离级别。
每个 select 语句都有自己的一份快照,而不是一个事务一份,所以在不同的时刻,查询出来的数据可能是不一致的。 读提交解决了脏读的问题,但是无法做到可重复读,也没办法解决幻读。
可重复读
可重复是对比不可重复而言的,上面说不可重复读是指同一事物不同时刻读到的数据值可能不一致。而可重复读是指,事务不会 读到其他事务对已有数据的修改,及时其他事务已提交,也就是说,事务开始时读到的已有数据是什么,在事务提交前的任意时 刻,这些数据的值都是一样的。但是,对于其他事务新插入的数据是可以读到的,这也就引发了幻读问题。
同样的,需改全局隔离级别为可重复读级别。
set global transaction isolation level repeatable read;
在这个隔离级别下,启动两个事务,两个事务同时开启。
首先看一下可重复读的效果,事务A启动后修改了数据,并且在事务B之前提交,事务B在事务开始和事务A提交之后两个时间节点 都读取的数据相同,已经可以看出可重复读的效果。
可重复读做到了,这只是针对已有行的更改操作有效,但是对于新插入的行记录,就没这么幸运了,幻读就这么产生了。我们看一下这个过程:
- 事务A开始后,执行 update 操作,将 age = 1 的记录的 name 改为“风筝2号”;
- 事务B开始后,在事务执行完 update 后,执行 insert 操作,插入记录 age =1,name = 古时的风筝,这和事务A修改的那条记录值相同,然后提交。
- 事务B提交后,事务A中执行 select,查询 age=1 的数据,这时,会发现多了一行,并且发现还有一条 name = 古时的风筝,age = 1 的记录,这其实就是事务B刚刚插入的,这就是幻读。
串行化
串行化是4种事务隔离级别中隔离效果最好的,解决了脏读、可重复读、幻读的问题,但是效果最差,它将事务的执行变为顺序执行, 与其他三个隔离级别相比,它就相当于单线程,后一个事务的执行必须等待前一个事务结束。
总结
- MySQL InnoDB 引擎使用 redo log(重做日志) 保证事务的持久性,使用 undo log(回滚日志) 来保证事务的原子性。
- MySQL InnoDB 引擎通过 锁机制、MVCC 等手段来保证事务的隔离性( 默认支持的隔离级别是 REPEATABLE-READ )。
- 保证了事务的持久性、原子性、隔离性之后,一致性才能得到保障。
参考资料
文档信息
- 本文作者:Bob.Zhu
- 本文链接:https://adolphor.github.io/2021/05/18/mysql-transaction-isolation/
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)