1. 事务的 ACID 特性
-
Actomic,原子性:一个事务中的所有操作,要么全部完成,要么全部失败。
-
Consistency:一致性:事务执行前和执行后数据库都处于一致的状态。
-
Isolation:隔离性:同时进行中的事务不会看到其他事务的中间状态。
-
Durability,持久性:事务提交成功后,对数据库所做的变更都是持久的。
2. SQL 标准的事务隔离级别
-
读未提交, read uncommitted:一个事务还没提交,它做的变更就能被别的事务看到。问题:会产生”脏读”,读到的数据可能是不一致的。
-
读已提交, read committed:一个事务提交后,它做的变更才会被别的事务看到。问题:不可重复读,在一个事务里,前后两次执行同一个 SQL 看到的数据是不一致的,产生的原因有:1. 数据被修改或删除(称为”不可重复读”),2. 有新插入的数据(称为”幻读”)。
-
可重复读, repeatable read:一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。解决了”不可重复读”的问题,但没有解决”幻读”问题。
-
串行化, serializable:对于同一行记录,“写”会加写锁,“读”会加读锁。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。解决了”幻读”的问题,但效率低。
3. InnoDB 里事务的实现
在 MySQL 中,事务支持是在存储引擎层实现的。支持事务的引擎主要是 InnoDB 。
InnoDB 的默认隔离级别是可重复读,但达到了 SQL 标准的串行化级别,能解决”幻读”问题。可以通过下面的命令查看隔离级别:
mysql> show variables like 'transaction_isolation';
+-----------------------+-----------------+
| Variable_name | Value |
+-----------------------+-----------------+
| transaction_isolation | REPEATABLE-READ |
+-----------------------+-----------------+
1 row in set, 1 warning (0.01 sec)
快照读:普通的查询是快照读,只能看到事务开始时所有已提交事务的修改。
当前读:特殊的读操作,插入/更新/删除操作,要求读到当前所有已提交的记录的最新值,属于当前读,需要加锁。用于支持事务里的 DML 语句。
select * from table where ? lock in share mode;
select * from table where ? for update;
insert into table values (…);
update table set ? where ?;
delete from table where ?;
InnoDB 通过 MVCC 机制解决了快照读的”不可重复读”问题。通过行锁和间隙锁 gap-lock 解决”幻读”问题,用行锁防止事务之间的并发更新,用间隙锁防止插入新的数据。
3.1 MVCC
MVCC 是用来解决事务的一致性读,用于支持可重复读(repeatable read)、读已提交(read commited)。
MVCC 实现:每次更新数据都会在 undo log 里新生成一条记录,记录了数据修改前的状态,该记录同时记录了事务的ID, row trx_id。
同一条记录的修改记录组成一条链,通过在链上回滚可以读出以往版本的值。这样同一条记录在系统中有多个版本,因此称为多版本并发控制。
redo/undo log 在重启之后重新生成,事务 ID 在内存中维护、分配,重启后再重新开始。
InnoDB 在事务启动时为事务记录下所有启动了但还没提交的事务 ID,记录在一个数组里。
这组事务ID的最小值记为低水位,最大值加1 记为高水位。
对数据的可见性判断规则为:
1. 小于低水位或者是本事务修改的可见;
2. trx_id 在高-低水位之间,且不在事务ID数组里的,是已提交的,可见。
不能只根据高低水位的值判断,高低水位之间的某些事务可能已提交。
InnoDB是通过在每行记录后面保存两个隐藏的列来实现的。一个保存行的创建时间,一个保存行的过期时间(或删除时间)。这个不是实际的时间值,而是系统版本号(System Version Number)。
每开始一个新的事务,系统版本号都会自动递增。事务开始时刻的的系统版本号作为事务的版本号,用来和查询到的每行记录的版本号进行比较。
read-view,视图,是由事务 ID 加上可见性规则来实现的,不是物理存在的。
当系统里没有比这个回滚日志更早的 read-view 的时候,就可以删除这个回滚日志。如果系统里存在长事务,在这个事务提交之前,它可能用到的回滚记录都必须保留,这会导致占用大量的存储空间。
查询持续时间超过 60 秒的事务:
select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60;
3.2 不同隔离级别的读取实现
在实现上,数据库会创建一个视图,访问的时候以视图的逻辑结果为准。
在可重复读隔离级别下,视图在事务启动的时候创建,整个视图存在期间都使用这个视图。
在读提交隔离级别下,这个视图是在每个 SQL 语句开始执行的时候创建的。
读未提交隔离级别下直接返回记录上的最新值,没有视图概念。
串行化隔离级别下直接用加锁的方式来避免并行访问。
4. 两阶段提交
redo log 和 binlog 有一个共同的数据字段叫 XID 把他们关联起来。
两阶段提交:
1. 写入 redo log,处于 prepare 阶段;
时刻A
2. 写入 binlog;
时刻B
3. 提交事务,处于 commit 状态。
崩溃恢复时:
- 已经有 commit 标记的直接提交;
- 扫描 prepare 状态的 redo log,拿 XID 去 binlog 查找相应的 binlog 记录:
-
- 如果没有找到,此时肯定是在时刻A崩溃,此时有 redo log,没有 binlog,则回滚这个 redo log,不恢复事务,否则主备库会不一致。
-
- 如果找到了,是在时刻B崩溃,继续提交事务即可。
欢迎关注我的微信公众号: coderbee笔记 。