1. 什么是事务
事务是 DBMS 执行过程中的一个逻辑单位。这个逻辑单位由一个有限的数据库操作序列构成,不可分割,要么全部成功,要么全部不成功。
2. 事务基本语法
begin/start transaction
语句1;
语句2;
savepoint s3;
语句3;
rollback to s3;
commit;
隐式提交:在 begin 开启事务后,输入了一些语句,在没 commit 前,如果执行了 DDL、使用或修改 mysql 这个数据库中的数据、begin 等,MySQL 会自动提交这些语句之前到 begin 之间的语句。
3. 事务的 4 大特性
- 原子性(Atomicity)
一个事务必须被视为一个不可分割的最小单元,整个事务中的所有操作要么全部提交成功,要么全部失败,对于一个事务来说,不能只执行其中的一部分操作。 - 一致性(Consistency)
一致性是指事务将数据库从一种一致性转换到另外一种一致性状态,在事务开始之前和事务结束之后数据库中数据的完整性没有被破坏。 - 持久性(Durability)
一旦事务提交,其所做的修改就会永久保存到数据库中。此时即使系统崩溃,已经提交的修改数据也不会丢失。 - 隔离性(Isolation)
MySQL 有多个客户端连接,每个客户端连接可执行一个事务。一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
4. 事务并发会带来的问题
脏读、不可重复读、幻读。
- 脏读
当一个事务读取到了另一个事务修改但并未提交的数据。
- 不可重复读
当事务内相同的记录被检索两次,但两次得到的结果不同。
- 幻读
在事务执行过程中,另一事务将新纪录添加到正在读取的事务中。
MySQL 把同一个事务中两次相同条件的 SQL 读取,如果是第二次读取比第一次读取到另外的记录,叫幻读,如果是第一次读取到的记录,第二次读取时被删除读不到了,叫不可重复读。
5. MySQL 中的隔离级别
MySQL 在可重复读时基本解决了幻读:在快照读时用 MVCC 解决;在当前读时用间隙锁解决。
6. 事务原理
在事务的实现机制上,MySQL 采用的是 WAL(Write-ahead logging,预写式日志)来实现的。
在使用 WAL 的系统中,所有的修改都先被写入到日志中,然后再被应用到系统中。通常包含 redo 和 undo 两部分信息。
为什么需要使用 WAL,然后包含 redo 和 undo 信息呢?举个例子,如果一个系统直接将变更应用到系统状态中,那么在机器掉电重启之后系统需要知道操作是成功了,还是只有部分成功或者是失败了(为了恢复状态)。如果使用了 WAL,那么在重启之后系统可以通过比较日志和系统状态来决定是继续完成操作还是撤销操作。
redo log:重做日志,每当有操作时,在数据变更之前将操作写入 redo log, 这样当发生掉电之类情况时系统可以在重启后继续操作。
undo log:撤销日志,当一些变更执行到一半无法完成时,可以根据撤销日志恢复到变更之前的状态。
MySQL 中用 redo log 来在系统 Crash 重启之类的情况时修复数据(事务的持久性),而 undo log 来保证事务的原子性。
6.1 redo
redo 日志格式:
绝大部分类型的 redo 日志都有下边这种通用的结构:
崩溃恢复为啥不用二进制日志,而用 redo 日志。
6.2 undo
insert 对应的 undo 日志:
delete 对应的 undo 日志:
update 对应的 undo 日志:
6.3 MVCC
MVCC 用更好的方式去处理读—写请求,做到在发生读—写请求冲突时不用加锁。
这个读是指的快照读,而不是当前读;当前读是一种加锁操作,是悲观锁。
MySQL 在 REPEATABLE READ 隔离级别下,是可以很大程度避免幻读问题的发生的,MySQL 是怎么做到的?
1. 版本链
对于使用 InnoDB 存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列:
- trx_id:每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的事 务 id 赋值给 trx_id 隐藏列。
- roll_pointer:每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到 undo 日志中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。
每次对记录进行改动,都会记录一条 undo 日志,每条 undo 日志也都有一 个 roll_pointer 属性(INSERT 操作对应的 undo 日志没有该属性,因为该记录并没有更早的版本),可以将这些 undo 日志都连起来,串成一个链表,所以现在的情况就像下图一样:
可以利用这个记录的版本链来控制并发事务访问相同记录的行为,那么这种机制就被称之为多版本并发控制(Mulit-Version Concurrency Control MVCC)。
2. ReadView
ReadView 中主要包含 4 个比较重要的内容:
- m_ids:表示在生成 ReadView 时当前系统中活跃的读写事务的事务 id 列表。
- min_trx_id:表示在生成 ReadView 时当前系统中活跃的读写事务中最小的事务 id,也就是 m_ids 中的最小值。
- max_trx_id:表示生成 ReadView 时系统中应该分配给下一个事务的 id 值。 注意 max_trx_id 并不是 m_ids 中的最大值,事务 id 是递增分配的。比方说现在 有 id 为 1,2,3 这三个事务,之后 id 为 3 的事务提交了。那么一个新的读事务 在生成 ReadView 时,m_ids 就包括 1 和 2,min_trx_id 的值就是 1,max_trx_id 的值就是 4。
- creator_trx_id:表示生成该 ReadView 的事务的事务 id。
有了这个 ReadView,这样在访问某条记录时,只需要按照下边的步骤判断 记录的某个版本是否可见:
READ UNCOMMITTED 隔离级别的事务来说,由于可以读到未提交事务修改过的记录,所以直接读取记录的最新版本就好了。
SERIALIZABLE 隔离级别的事务来说,InnoDB 使用加锁的方式来访问记录。
READ COMMITTED 和 REPEATABLE READ 隔离级别的事务来说,都必须保证读到已经提交了的事务修改过的记录,也就是说假如另一个事务已经修改了记录但是尚未提交,是不能直接读取最新版本的记录的,核心问题就是: READ COMMITTED 和 REPEATABLE READ 隔离级别在不可重复读和幻读上的区别是从哪里来的,这两种隔离级别关键是需要判断一下版本链中的哪个版本是当前事务可见的。
READ COMMITTED 和 REPEATABLE READ 隔离级别的一个非常大的区别就是它们生成 ReadView 的时机不同。