文章目录
- 原子性是通过 `undo log`实现的。
- 一致性是通过 `redo log`实现的。
- 隔离性的实现 (分事务的隔离级别讨论)
- 持久性是利用 redo log 实现的
- 写入过程
原子性是通过 undo log
实现的。
事务的所有 修改操作 (增、删、改)的相反操作都会写入undo log, 比如事务执行了一条insert语句,那么undo log就会记录一条相应的 delete 语句。所以 undo log 是一个逻辑文件,记录的是相应的SQL语句一旦由于故障,导致事务无法成功提交,系统则会执行undo log中相应的撤销操作,达到事务回滚的目的。
一致性是通过 redo log
实现的。
事务的所有 修改操作 (增、删、改),数据库都会生成一条 redo log 记录. 区别于undo log记录SQL语句、redo log记录的是事务对数据库的哪个数据页做了什么修改,属于物理日志。
redo日志应用场景:数据库系统直接崩溃,需要进行恢复,一般数据库都会使用按时间点备份的策略,首先将数据库恢复到最近备份的时间点状态,之后读取该时间点之后的 redo log 记录,重新执行相应记录,达到最终一致性目的。
隔离性的实现 (分事务的隔离级别讨论)
- 已提交读
实现策略:数据的读取不加锁,数据的写入、修改、删除需要加行锁,可以克服脏读,但无法避免不可重复读问题。
如下图是一个脏读场景,事务T2读取了T1未提交的数据。
使用加锁策略后,T1写数据x时,先获取了x的锁,导致T2的读操作等待,T1进行数据回滚后,释放锁,T2可以继续读取原来数据,不存在读取到脏数据的可能。
- 可重复读
实现策略:MVCC(多个版本行控制)策略。
下图是一个不可重复读的场景。由于T1的更新操作,导致T2两次读取的数据不一致。
单纯加行锁是无法解决的,T2先读取x值,T1之后经过加锁、解锁步骤,更新x的值,提交事务。T2再读的话,读出来的是T1更新后的值,两次读取结果不一致。
前面讲的行级锁是一个悲观锁,而MVCC是一个乐观锁,乐观锁在一定程度上可以避免加锁操作,因此开销更低。InnoDB的MVCC实现,是通过保存数据在某个时间点的快照来实现的。一个事务,不管其执行多长时间,其内部看到的数据是一致的。也就是事务在执行的过程中不会相互影响。
具体实现如下:
MVCC,通过在每行记录后面保存两个隐藏的列来实现:一个保存了行的创建时间,一个保存行的过期时间(删除时间),当然,这里的时间并不是时间戳,而是系统版本号,每开始一个新的事务,系统版本号就会递增。
- selelct操作。只查找版本早于(包含等于)当前事务版本的数据行。可以确保事务读取的行,要么是事务开始前就已存在,或者事务自身插入或修改的记录。行的删除版本要么未定义,要么大于当前事务版本号。可以确保事务读取的行,在事务开始之前未删除。
- insert操作。插入的行保存当前事务版本号为行版本号。
- delete操作。删除的行保存当前事务版本号为删除标识。
- update操作。变为insert和delete操作的组合,insert的行保存当前版本号为行版本号,delete则保存当前版本号到原来的行作为删除标识。
通过MVCC策略,可以确保一个事务里面读取的是同一个数据库版本快照。
持久性是利用 redo log 实现的
为了提高性能,和数据页类似,redo log 也包括两部分:一是内存中的日志缓冲(redo log buffer),该部分日志是易失性的;二是磁盘上的重做日志文件(redo log file),该部分日志是持久的。redo log是物理日志,记录的是数据库中物理页的情况 。
当数据发生修改时,InnoDB不仅会修改Buffer Pool中的数据,也会在redo log buffer记录这次操作;当事务提交时,会对redo log buffer进行刷盘,记录到redo log file中。如果MySQL宕机,重启时可以读取redo log file中的数据,对数据库进行恢复。这样就不需要每次提交事务都实时进行刷脏了。写入过程
注意点:
- 先修改Buffer Pool,后写 redo log buffer。
- redo日志比数据页先写回磁盘:事务提交的时候,会把redo log buffer写入redo log file,写入成功才算提交成功(也有其他场景触发写入,这里就不展开了),而Buffer Pool的数据由后台线程在后续某个时刻写入磁盘。
- 刷脏的时候一定会保证对应的redo log已经落盘了,也即是所谓的WAL(Write Ahead Log)(预写式日志),否则会有数据丢失的可能性。