ACID 中的原子性主要通过 Undo Log 来实现,持久性通过 Redo Log 来实现,隔离性由 MVCC 和锁机制来实现,一致性则由其他三大特性共同保证。
如何保证原子性?
事务对数据进行修改前,会记录一份快照到 Undo Log,如果事务中有任何一步执行失败,系统会读取 Undo Log 将所有操作回滚,恢复到事务开始前的状态,从而保证事务要么全部成功,要么全部失败。
1)BEGIN;
2)UPDATEuserSET balance = balance - 100WHEREid = 1;
=> 写入 Undo Log:记录 id=1 的原始余额 500
3)UPDATEuserSET balance = balance + 100WHEREid = 2;
=> 写入 Undo Log:记录 id=2 的原始余额 300
4)COMMIT;
=> 清空 Undo Log,事务成功
❗如果失败:
=> 执行 ROLLBACK:根据 UndoLog 把数据还原!
如何保证持久性?
MySQL 的持久性主要由预写 Redo Log、双写机制、两阶段提交以及 Checkpoint 刷盘机制共同保证。
当事务提交时,MySQL 会先将事务的修改操作写入 Redo Log,并强制刷盘,然后再将内存中的数据页刷入磁盘。这样即使系统崩溃,重启后也能通过 Redo Log 重放恢复数据。
在将数据页写入到磁盘时,如果发生崩溃,可能会导致数据页不完整。InnoDB 的数据页大小为16KB,通常大于操作系统的 4KB页大小。
为了解决只写入部分的问题,MySQL 采用了双写机制,脏盘刷页时,先将数据页写入到一个双写缓冲区中,2M 的连续空间,然后再将其写入到磁盘的实际位置。
崩溃恢复时,如果发现数据页不完整,会从双写缓冲区中恢复副本,确保数据页的完整性。
在涉及主从复制时,MySQL 通过两阶段提交保证 Redo Log 和 Binlog 的一致性:第一阶段,写入 Redo Log 并标记为 prepare 状态;第二阶段,写入 Binlog 再提交 Redo Log 为 commit 状态。
崩溃恢复时,如果发现 Redo Log 是 prepare 但 Binlog 完整,则会提交事务;反之会回滚,避免主从不一致。
另外,由于 Redo Log 的容量有限,Checkpoint 机制会定期将内存中的脏页刷到磁盘,这样能减少崩溃恢复时需要处理的 Redo Log 数量。
如何保证隔离性?
隔离性主要通过锁机制和 MVCC 来实现。
比如说一个事务正在修改某条数据时,MySQL 会通过临键锁来防止其他事务同时进行修改,避免数据冲突。
同时,临键锁可以防止幻读现象的发生。比如事务 A 查询 id > 10 的记录,那么临键锁不仅会锁住 id=10 的行,还会锁住 10 后面的“间隙”,防止其他事务插入 id=15 的数据。
假如表中的主键有 id: 5, 10, 15, 20, 25,那么 InnoDB 会对以下区间和记录加锁:
加锁对象 | 类型 | 锁定含义 |
---|---|---|
(10, 15] | 临键锁 | 锁住 id=15 和前间隙,防止插入11~14 |
(15, 20] | 临键锁 | 锁住了 id=20 和前间隙 |
(20, 25] | 临键锁 | 锁住了 id=25 和前间隙 |
(25, +∞) | 间隙锁 | 锁住尾部防止插入30等 |
MVCC 主要用来优化读操作,通过保存数据的历史版本,让读操作不需要加锁就能直接读取快照,提高读的并发性能。
不同的隔离级别对应不同的实现策略,比如说在可重复读隔离级别下,事务第一次查询时会生成一个 Read View,之后所有读操作都复用这个视图,保证多次读取的结果一致。
如何保证一致性?
MySQL 的一致性并不是靠某一个机制单独保证的,而是原子性、隔离性和持久性协同作用的结果。