ACID是数据库的四大基本属性,分别代表Atomicity(原子性)、Consistency(一致性)、Isolation(隔离性)和Durability(持久性)。这四个属性彼此相依相成,今天让我们来着重聊聊ACID中的A。
原子性确保事务被视为一个不可分割的最小工作单位。在事务中,所有的操作要么全部完成,要么全部不完成。 在深入到原子性的具体实现之前,先让我们问自己一个问题思考一下,哪些异常情况会导致ACID被破坏?
- 在一个事务中执行多个操作时,如果某个操作失败导致事务需要回滚,数据库如何撤销已经执行的操作,使数据库回到初始状态?(原子性)
- 如果事务成功提交,但是在将更改持久化到磁盘之前数据库发生宕机,如何确保这些变更不丢失?(持久性)
让我们看下MySQL InnoDB是如何做的。MySQL的InnoDB存储引擎通过日志和锁两大机制来确保原子性。
日志
redo log
redo log 记录了对数据库进行修改的操作,以便在数据库发生崩溃时可以利用这些记录恢复未写入磁盘的数据。也就是说,redo log主要用于确保事务的持久性。具体是怎么做的呢?
redo log记录的不是SQL操作语句(如INSERT、UPDATE等)本身,而是这些操作对数据库页(即数据文件中的数据块)执行的物理更改。这种方式称为物理日志记录,与逻辑日志记录(记录高级操作语句)不同。
Redo Log记录的内容包括两部分:
- 页的物理更改:例如,当执行一个
INSERT操作时,redo log并不记录INSERT语句本身,而是记录这个插入操作如何修改了数据库页的内容。这可能包括添加新行的具体字节更改,以及任何因此导致的索引页或其他结构的更新。
在需要恢复数据时,直接应用物理更改到数据库页是非常高效的,因为这避免了重新解析SQL语句和重新执行它们所需的开销。
- 事务信息:redo log还会包含一些事务级别的元数据,如事务的开始、结束标记,以便在系统恢复时能够重做(redo)或撤销(undo)事务所做的更改。
问: redo log 记录了数据页的物理变更,能用来恢复整个数据库吗?
答: redo log是循环写入的,因此不能用它来恢复这个数据库。
redo log分为两个主要部分:一是内存中的redo log buffer,用于缓存日志记录;二是磁盘上的redo log files,用于持久存储。当事务执行数据变更操作(如INSERT、UPDATE或DELETE)时,这些变更首先被记录在内存中的redo log buffer里,而不是直接写入到数据文件。这样做的目的是为了快速记录变更,同时减少磁盘I/O操作,提高性能。为了保证持久性,防止数据丢失,redo log遵循预写日志协议(Write-Ahead Logging, WAL),确保在修改实际写入数据库之前,先写入日志。当事务提交时,redo log buffer中的内容会被刷新(写入)到磁盘上的redo log files中。
让我们再回头看下开头对持久性保证提出的问题:如果事务成功提交,但是在将更改持久化到磁盘之前数据库发生宕机,如何确保这些变更不丢失? MySQL InnoDB给出的方案是:将对数据库的修改操作首先写入到redo log中,如果此时发生宕机,那么在数据库重启恢复之后,MySQL InnoDB会读取redo log的值进行数据恢复。
上面提到,redo log除了记录了页的物理变更,还记录了事务信息,包括事务的开始和提交信息,这为事务的原子性提供了额外的保障。例如,如果系统在事务提交之前崩溃,redo log中的信息可以帮助系统判断哪些事务需要回滚,以保持原子性。
redo log 解决了持久性的问题并且为原子性提供了额外的保障。那么在事务失败或被明确回滚时,又如何撤销已经做出的更改呢?那就请看undo log 是什么,又是如何发挥作用的。
undo log
undo log 存储了旧的数据版本。每当 InnoDB 引擎对一条记录进行操作(修改、删除、新增)时,要把回滚时需要的信息都记录到 undo log 里,比如:
- 在插入一条记录时,要把这条记录的主键值记下来,这样之后回滚时只需要把这个主键值对应的记录删掉就好了;
- 在删除一条记录时,要把这条记录中的内容都记下来,这样之后回滚时再把由这些内容组成的记录插入到表中就好了;
- 在更新一条记录时,要把被更新的列的旧值记下来,这样之后回滚时再把这些列更新为旧值就好了。
redo log记录的是数据页的物理变化,被称为物理日志记录,而undo log记录的是逻辑变化,被称为逻辑日志记录。
undo Log 对实现原子性至关重要。它记录了事务执行前的数据状态,如果事务失败或需要回滚,MySQL可以利用undo log来撤销(undo)已执行的修改,将数据恢复到事务开始前的状态。
undo log为修改过的每个数据行创建了版本历史,形成了所谓的“版本链”。当一个事务需要读取数据时,它会根据自己的事务版本号来选择合适的数据版本,从而看到事务开始时刻的一致性视图,这也是MVCC实现的基础。关于这部分内容,在后面聊到隔离性(Isolation)和一致性(Consistency)时会再详细聊聊。
锁
锁在数据库系统中主要是用来维护事务的隔离性,防止不同事务间的干扰,例如更新丢失、脏读、不可重复读和幻读等问题。然而,通过控制事务对数据的并发访问,锁机制也间接地对事务的原子性提供了支持。比如说:
- 事务的完整执行:原子性要求事务中的操作要么全部完成,要么全部不完成。锁通过确保事务在执行过程中对必要资源的独占访问,帮助保证事务可以在没有外部干扰的情况下完整执行。如果事务在获取所需的所有锁之后因为某些原因(比如错误或冲突)无法完成,那么事务会被回滚,所有已经获取的锁会被释放,事务对数据的所有修改也会被撤销,从而维持了原子性。
- 回滚与恢复:在执行事务回滚时,锁确保在恢复到事务开始前的状态期间不会有其他事务干扰,从而保持了操作的原子性。