概述
事务具备四种特性,分别是:原子性、一致性、隔离性、持久性。其中隔离性是由锁机制实现,而事务的原子性、一致性、持久性则是依靠redo日志和undo日志来保证。
其中redo日志被直译为重做日志,主要保证了事务的持久性 。总的来说,每次事务在提交时首先会将该事务的所有(操作)日志写入到重做日志之中进行持久化正因如此,当数据库系统宕机时,可以依靠redo日志进行数据恢复。
undo日志称之为回滚日志,主要用处是当事务需要进行回滚操作的时候,依靠undo日志回滚到某个特定版本。显然,undo日志中也必然记录了事务产生的操作日志,不过undo日志并非是记录了某个版本的具体数据,宏观上讲是对每个版本的语句以逻辑性的将操作进行逆操作(为了方便理解,不仅仅是这样!),比如insert语句对应生成delete语句,而update生成一个原始数据的update。
以此可得知,undo日志不仅能够实现事务回滚策略还可以实现获取某个版本的数据,也就是后面要说的一致性非锁定读。
Redo日志
作用
对于redo日志,第一个问题就是要知道为什么需要它,以及它到底解决了什么问题。总的来说,之所以使用redo日志主要出于两个方面的需求,其一是性能,其二是为了在事务提交后必须要保证持久性。
由InnoDB的执行流程可知,当引擎响应对应的修改操作时,只是对加载到内存的页在内存中进行修改,并不会立即的将其写入到磁盘,而是依赖于名为checkpoint的机制去决定何时写入磁盘的。
也就是说,如果事务已成功提交,而checkpoint并没有执行写入磁盘的操作,但就在此时系统宕机了,这也就造成了持久性的无法保证——数据的丢失。对于此的简单粗暴的解决办法就是要什么checkpoint机制,每次修改数据都给它立即刷新到磁盘!但此举并不能解决持久性的问题,还会对数据库产生极大的性能影响。
于是,为了解决这样的问题,InnoDB会在事务提交时,先将对应的操作日志进行持久化处理(较之将数据页完整的刷新到磁盘,记录操作日志产生的影响微乎极微), 而无需去管数据是否成功写入到磁盘,即使磁盘没有被成功写入,后续也可以使用redo日志进行恢复。
刷盘策略
前述说明了redo日志的必要性,但在其后仍然要了解它究竟是如何保证redo日志的持久性,以此保证系统的持久性呢?
重做日志可以分为两个部分,其一是重做日志缓冲(redo log buffer),它是存放在内存之中,是易失的。其二是重做日志文件(redo log file),顾名思义是存储在磁盘的日志文件。显然,即使为了保证绝对安全的持久性,我们也不能将重做日志的更改设置为每次都要进行一次IO操作,这将对性能产生严重的影响,因此也就出现了重做日志缓冲。
所以重做日志缓冲到重做日志文件的过程是我们需要了解的,即刷盘策略。不过仍需补充一点,文件系统在底层为了性能考虑,想要将重做日志缓冲成功写到重做日志文件还需要经历文件系统缓存阶段。
也就是:重做日志缓冲 ——> 文件系统缓存 ——> 重做日志文件
而想要强制从文件系统缓存刷新到磁盘之中,则需要执行fsync指令。
了解了以上的过程就可以往下继续开展。InnoDB提供了名为innodb_flush_log_at_trx_commit的参数进行控制刷盘策略,共有三种策略。
- 事务提交时不进行刷盘,依赖于主线程的刷新策略为其刷入磁盘(每秒执行一次)
- 事务提交并“写入到文件系统缓存”,且执行fsync
- 事务提交但仅“写入到文件系统缓存”,不执行fsync
显然,为了保证数据的持久性,InnoDB对该参数的默认策略为1。而以此也导致该策略的执行效率相较于其他两种要底下的多。
《MySQL技术内幕》一书中对0、1、2三种策略得出的时间分别为:13.9秒、1分53秒、23.37秒。这意味着如果我们紧迫的需要提升性能可以选择切换redo日志的刷盘策略。
Undo日志
redo日志记录了事务的具体操作,而undo日志则是记录在那个具体操作前的对数据的“保存”。这样我们就可以在事务出现异常时通过undo日志进行回滚到之前的版本。像前面说的一样,undo日志并非是直接存储了前面的数据,而是以逻辑性的对对应的语句进行逆操作。
而不仅如此,undo日志在事务提交后并不会立即清除,而是放入undo日志链表,等待purge线程进行删除。而在尚未被purge线程(主要负责清理undo页和被标记为要删除的数据行) 删除的期间,可以通过undo日志读取到之前版本的数据,以及对应版本是由那个事务ID操作的,这也就是MVCC的实现。
而具体的undo日志主要分为两种类型,分别是insert undo log与update undo log。前者存储insert产生的undo日志,后者记录delete和update操作产生的数据。
之所以将两者区分开是因为在对它们进行清理的时候有不同的策略。具体是因为insert实际上因为事务隔离性的要求,其对其他事务是不可见的。也就是当事务提交后便会被删除。而update日志则是依赖于purge线程进行删除任务。
执行流程
当存储引擎准备执行更新操作时。
- 如果缓存未命中对应数据页,则从磁盘中读取
- 将对应语句的逆操作(或以update为主体的原始数据)存入undo日志
- 更新数据,并写入redo日志buffer
- redo日志提交
- 写入binlog
其中值得注意的是,undo日志的产生亦会被持久化保护。而具体的undo日志会写入行信息中的回滚指针之中,并且如果此时的undo日志之前的undo日志未被删除的话,会将之前的日志写入到当前新的日志之中,并将新的undo日志指向之前的。
这也意味着,如果之前的undo日志未被删除,我们可以往前回滚多个版本。