MySQL 基础(三)事务与 MVCC
事务
事务是一组原子性的 SQL 操作,或者被称为一个独立的工作单元,如果数据库引擎能够成功地对数据库应用该组的全部 SQL 语句,那么就会全部执行,否则全部不执行。
事务的特性
在关系数据库管理系统中,事务需要满足 ACID 四个基本特征,具体解释如下[1]:
-
A(Atomicity)原子性
一个事务(transaction)中的所有操作,或者全部完成,或者全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。即,事务不可分割、不可约简
-
C(Consistency)一致性
在事务开始之前和事务提交之后,数据库的完整性没有被破坏,即在事务发生前后数据依旧满足原有的约束条件、级联回滚等
-
I(Isolation)隔离性
数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致的数据不一致。事务的隔离级别从低到高分为四个等级:读未提交(read uncommitted)、提交读(read committed)、可重复读(repeatable read)和可串行化(serializable)
-
D(Duration)持久性
事务处理结束之后,对于数据的修改是永久的,即便系统发生故障也不会丢失
事务的状态
事务的可能状态如下图所示:
- active:表示事务已经开始了,可以通过显式地执行
BEGIN
或START TRANSACTION
语句来开启一个事务 - partially committed:部分提交状态,此时事务已经执行结束了,但是不会直接将最终结果直接写入到磁盘中,在这一步是将结果写入到内存中
- committed:将数据刷新磁盘上,此步骤完成则表示确实是成功提交了事务
- failed:事务执行过程中失败或者从内存写入到磁盘中的过程失败,此时需要执行回滚操作
- aborted:回滚执行完成之后的状态
事务的隔离级别
事务并发执行时可能会存在的一些一致性问题:
-
脏读
一个事务读取了另一个事务还 没有提交 的数据,此时读取到的数据是脏数据,因此被称为 “脏读”
-
不可重复读
一个事务开始时,只能看见已经提交了的事务对数据所做的修改,未提交的事务对数据的修改不可见,当同一个事务两次读取同一个数据时,两次读取到的数据不一致。这是因为在这两次读取的时间间隔中,有其它的事务提交对该数据造成了修改,使得两次读取的数据不一致,这就被称为 ”不可重复读“
-
幻读
当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,此时当前的事务再次读取该范围内的数据时,将会产生 “幻行” 问题,即两次读取出现了数据量不一致的情况,这就被称之为 ”幻读“
SQL 标准定义了以下四种隔离级别:
-
read uncommitted(读未提交)
最低级别的隔离级别,在这种隔离级别中,即使事务还未提交,对于数据的修改对于其它事务来讲也是可见的,这种情况特别危险,一般情况下不要使用
-
read committed(提交读)
所有的事务只能看到已经提交的事务对数据的修改,即一个事务从开始直到提交之前,所做的任何修改对于其它事物都是不可见的。在这种隔离级别下,避免了 ”脏读“ 问题的出现, 但是不能解决 ”不可重复读“ 问题
-
repeatable read(可重复读)
在这种隔离级别下,避免了 ”脏读“ 和 ”不可重复读“ 问题的出现,这也是 MySQL 默认的事务隔离级别。SQL 标准并不要求在这种隔离级别下解决 ”幻读“ 的问题,但是 MySQL 通过 MVCC 的方式在这种隔离级别下解决了 ”幻读“ 的问题[2]
-
serializable(串行化)
在串行化的隔离级别下,事务与事务之间通过串行的方式执行,从根源上避免了并发执行时出现的一系列问题,缺点在于由于串行化,使得事务的执行效率没有那么高,因此一般情况下也不会采用这种隔离级别
四种隔离级别的比较如下表所示:[4]
隔离级别 | 脏读 | 不可重复读 | 幻影读 |
---|---|---|---|
未提交读 | 可能发生 | 可能发生 | 可能发生 |
提交读 | - | 可能发生 | 可能发生 |
可重复读 | - | - | 可能发生 |
可序列化 | - | - | - |
MVCC
MVCC(Multi-Version Concurrency Control):多版本并发控制,通过记录的版本链和 ReadView
(一致性视图)来控制并发事务访问相同的记录时的行为。MVCC 没有固定的标准,具体取决于各个数据库管理系统的具体实现
版本链
每次对数据执行操作之后,都会将旧值放入到放入到一条 undo 日志中,随着修改的次数增多,所有的版本都通过 record
的 roll_pointer
属性连接成为一条链表,这个链表就称为 ”版本链“
具体的示意图如下图所示:[3]
假设现在两个事务 T1 和 T2,两者的事务 id 分别为 trx=85 和 trx=90,现在 T1 开启了事务,准备将 tb_student
中 id = 5 的记录的 name
列的值首先更改为 "a",再更改为 "b",T2 也开启了事务,准备将 tb_student
中 id = 5 的记录的 name
的列的值首先修改为 ”c“,再修改为 ”d“。
假设在执行事务之前,id = 5 的记录中 name
属性的值为 "muse",现在按照 执行时刻 的顺序,首先 T1 的两次更新操作将会被执行,同时将记录写入到 undo log 中,形成对应的版本链(trx_id=85
的部分);然后,T2 在按照时间顺序继续执行更新操作,将记录写入到 undo log 中,与之前的 undo log 链形成最终的 undo log 链。注意将当前记录与 undo log 链的整体看做同一个链
ReadView
ReadView 中关键的四个属性:
m_ids
:在生成 ReadView 时,当前系统中活跃的读写事务的事务 id 列表min_trx_id
:在生成 ReadView 时,当前系统中活跃的读写事务中 最小的事务 id,也就是m_ids
中的最小值max_trx_id
:在生成 ReadView 时,系统应该分配给下一个事务的事务 id 的值creator_trx_id
:生成该 ReadView 的事务的事务 id(当前记录中的trx_id
)
版本的可见性的规则:
- 如果
trx_id == creator_trx_id
,则表示当前事务正在访问它自己修改过的记录,所以该版本可以被当前的事务所访问 - 如果
trx_id < min_trx_id
,则表明生成该版本的事务在当前事务生成 ReadView 之前已经提交,所以该版本可以被当前的事务访问 - 如果
trx_id >= max_trx_id
,则表明生成该版本的事务在当前事务生成 ReadView 之后才开始,所以该版本不可以被当前事务访问 - 如果
trx_id
在m_ids
中,说明创建 ReadView 的时候生成该版本的事务还是活跃的,该版本不可以被访问 - 如果
trx_id
不在m_ids
中,说明创建 ReadView 时生成该版本的事务已经被提交了,该版本可以被访问
如果某个版本的数据对于当前事务不可见,那么就顺着版本链找到下一个版本的数据,并继续通过以上的规则来判断记录的可见性,直到找到版本链中的最后一个版本
ReadView 的生成时机
Read Committed 和 Repeatable Read 隔离级别在 MVCC 上的最大区别在于 ReadView 的生成时机的不同,这种不同直接导致了这两种隔离级别对于 ”不可重复读“ 问题的处理。
对于 Read Committed,在一个事务中,每次读取数据之前都会生成一个 ReadView,这样的话就会使得其它事务对于数据的修改对于当前事务来讲也是可见的,因此存在 “不可重复读“ 的问题,而对于 Repeatable Read,在一个事务中,只有在第一次读取数据时生成一个 ReadView,这样就保证了在当前事务的执行过程中是无法看到别的事务对于数据的修改,这就避免了 “不可重复读” 问题的出现
参考:
[1] https://zh.wikipedia.org/wiki/ACID
[2] 《高性能 MySQL》(第三版)