一、前言
关系型数据的四大特性包括了原子性、一致性、隔离性、持久性(ACID)。
总的来说,InnoDB存储引擎的原子性是通过undo log来保证,
事务的持久性是通过redo log来实现的,
事务的隔离性是通过读写锁+MVCC机制来实现的。
而原子性、持久性、隔离性都只是手段,其目的是为了实现一致性。MySQL满足的是其自身内部数据的一致性,而对于具体业务的一致性,还需要应用程序本身遵守一致性规约。
MySQL事务实现的机制是WAL(Write-ahend logging,预写式日志),这是比较主流的方案(除此还有Commint Logging,Shadow Paging)。
在MySQL服务异常奔溃后,使用WAL,可以在系统重启之后,通过比较日志和系统状态来决定继续之前的操作或者是撤销之前的操作。
redo log(重做日志):每当操作时,在磁盘数据变更之前,将操作(要变更的数据)写入redo log,这样当系统奔溃重启后可以继续执行。
undo log(回滚日志):当一个事务执行一半无法继续执行时,可以根据回滚日志将之前的修改恢复到变更之前的状态(变更前的数据)。
除了WAL(预写式日志)外,还有Commit Logging(提交日志)和Shadow Paging(影子分页)都可以实现事务的原子性和持久性。
Commit Logging只有在日志记录全部都安全写入磁盘之后,数据库在日志中看到代表事务成功的“提交记录”(Commit Record)之后,才会根据日志上的信息对真正的数据进行修改,修改完成后,再在日志中加入一条“结束记录”(End Record)表示事务已完全持久化。与WAL的区别是:WAL允许在事务提交之前,提前写入变动数据,而Commit Loggin不行;同时WAL中有undo log,Commmit Logging中却没有。
注:阿里的OceanBase使用的Commint Logging来实现事务。
Shadow Paging的实现是数据的变动并不直接修改原来的数据,而是对需要修改的数据生成一个副本,保留原数据,修改副本数据。因此在整个事务过程中,需要修改的数据会同时存在两份,即修改前的数据和修改后的数据,当事务成功提交,所有数据的修改都成功持久化之后,最后一步是去修改数据引用的指针,将引用从原数据修改为副本数据,最后修改指针的这个操作被认为是原子操作。
二、Redo log
在前面Buffer Pool的文章中已经介绍过,MySQL操作数据是在内存中完成的,然后再把内存中的数据页写入到磁盘中。
如果每次修改一条数据,就把整个内存页数据刷新到磁盘是非常浪费的,并且由于一个事务可能包含了多个执行语句,而执行语句对应的数据可能分散在不同的数据页,这样写磁盘就是多次随机IO操作,性能是非常低下的。
所以InnoDB引擎就引入了Redo Log来提高性能,Buffer Pool中的数据修改,并不需要立即就刷新到磁盘中,具体的刷盘时机可以参考《Buffer Pool》这篇文章中关于脏页刷盘的介绍。
但每一条数据的修改,都会记录一条redo log的记录,同样redo log也有自己的缓冲区存放数据修改的记录。当每个事务提交时,就会把缓存区redo log中的记录刷新到磁盘中,同时由于磁盘中redo log的写入是顺序IO,所以效率也很高。
变相来说,redo log实现了内存页数据刷新到磁盘从随机IO变成了顺序IO,当然Buffer Pool本身在刷新数据到磁盘中可能还是随机IO。
与在事务提交时将所有修改过的内存中的页面刷新到磁盘中相比,只将该事务执行过程中产生的redo日志刷新到磁盘的好处如下:
redo日志占用的空间非常小
存储表空间ID、页号、偏移量以及需要更新的值所需的存储空间是很小的。
redo日志是顺序写入磁盘的
在执行事务的过程中,每执行一条语句,就可能产生若干条redo日志,这些日志是按照产生的顺序写入磁盘的,也就是使用顺序IO。
三,事务提交
MySQL在事务执行的过程中,会记录相应SQL语句的UndoLog 和 Redo Log,然后在内存中更新数据并形成数据脏页。
接下来Redo Log会根据一定规则触发刷盘操作,Undo Log 和数据脏页则通过刷盘机制将数据持久化至磁盘文件。
事务提交时,会将当前事务相关的所有Redo Log刷盘,只有当前事务相关的所有Redo Log 刷盘成功,事务才算提交成功。
注:undo log也需要记录 redo log
四,事务恢复
如果MySQL由于某种原因崩溃或者宕机,就需要数据的恢复或者回滚操作。
如果事务在执行至上面的第8步(事务未成功提交),即事务提交之前(事务已经执行了部分),MySQL 崩溃或者宕机,此时会先使用Redo Log恢复数据,然后使用Undo Log回滚数据。
如果在执行第8步之后MySQL崩溃或者宕机,此时会使用Redo Log恢复数据,大体流程如下图所示。
MySQL崩溃恢复后,首先会获取日志检查点信息,随后根据日志检查点信息使用Redo Log进行恢复。MySQL崩溃或者宕机时事务未提交,则接下来使用Undo Log回滚数据。如果在MySQL崩溃或者宕机时事务已经提交,则用Redo Log恢复数据即可
4.1 恢复机制
MySQL可以根据redo日志中的各种LSN值,来确定恢复的起点和终点。
然后将redo日志中的数据,以哈希表的形式,将一个页面下数据放到哈希表的一个槽中。
之后就可以遍历哈希表,因为对同一个页面进行修改的redo日志都放在了一个槽里,所以可以一次性将一个页面修复好(避免了很多读取页面的随机IO)。并且通过各种机制,避免无谓的页面修复,比如已经刷新的页面,进而提升崩溃恢复的速度。
4.2崩溃后的恢复为什么不用binlog?
binlog 会记录表所有更改操作,包括更新删除数据,更改表结构等等,主要用于人工恢复数据,而 redo log 对于我们是不可见的,它是 InnoDB 用于保证 crash-safe 能力的,也就是在事务提交后MySQL崩溃的话,可以保证事务的持久性,即事务提交后其更改是永久性的。
一句话概括:binlog 是用作人工恢复数据,redo log 是 MySQL 自己使用,用于保证在数据库崩溃时的事务持久性。
redo log 是 InnoDB 引擎特有的,binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用。
redo log是物理日志,记录的是“在某个数据页上做了什么修改”,恢复的速度更快;binlog是逻辑日志,记录的是这个语句的原始逻辑,比如“给ID=2这的c字段加1 ”。
redo log是“循环写”的日志文件,redo log 只会记录未刷盘的日志,已经刷入磁盘的数据都会从 redo log 这个有限大小的日志文件里删除。binlog 是追加日志,保存的是全量的日志。
当数据库 crash 后,想要恢复未刷盘但已经写入 redo log 和 binlog 的数据到内存时,binlog 是无法恢复的。虽然 binlog 拥有全量的日志,但没有一个标志让 innoDB 判断哪些数据已经入表(写入磁盘),哪些数据还没有。
4.3 redo log和undo log关系
数据库崩溃重启后,需要先从redo log中把未落盘的脏页数据恢复回来,重新写入磁盘,保证用户的数据不丢失。
当然,在崩溃恢复中还需要把未提交的事务进行回滚操作。由于回滚操作需要undo log日志支持,undo log日志的完整性和可靠性需要redo log日志来保证,所以数据库崩溃需要先做redo log数据恢复,然后做undo log回滚。
redo log是物理日志,记录的是数据库页的物理修改操作。所以undo log(可以看成数据库的数据)的写入也会伴随着redo log的产生,这是因为undo log也需要持久化的保护。
事务进行过程中,每次sql语句执行,都会记录undo log和redo log,然后更新数据形成脏页。
事务执行COMMIT操作时,会将本事务相关的所有redo log进行落盘,只有所有的redo log落盘成功,才算COMMIT成功。然后内存中的undo log和脏页按照同样的规则进行落盘。如果此时发生崩溃,则只使用redo log恢复数据。
4.5 redo log和binlog一致性
当我们开启了MySQL的BinLog日志,很明显需要保证BinLog和事务日志的一致性,为了保证二者的一致性,使用了两阶段事务2PC(所谓的两个阶段是指:第一阶段:准备阶段和第二阶段:提交阶段)。步骤如下:
当事务提交时InnoDB存储引擎进行prepare操作。
MySQL上层会将数据库、数据表和数据表中的数据的更新操作写入BinLog文件。
InnoDB存储引擎将事务日志写入Redo Log文件中。
4.6:redo log一定能保证事务的持久性吗?
不一定,这要根据redo log的刷盘策略决定,因为redo log buffer同样是在内存中,如果提交事务之后,redo log buffer还没来得及将数据刷新到redo log file进行持久化,此时发生宕机照样会丢失数据。
那该如何解决呢?刷盘写入策略。
4.7 redo log 写入策略
当redo log空间满了之后又会从头开始以循环的方式进行覆盖式的写入。MySQL 支持三种将 redo log buffer 写入 redo log file 的时机,可以通过 innodb_flush_log_at_trx_commit 参数配置,各参数含义如下:
-
0(延迟写):表示每次事务提交时都只是把 redo log 留在 redo log buffer 中,开启一个后台线程,每1s刷新一次到磁盘中 ;
-
1(实时写,实时刷):表示每次事务提交时都将 redo log 直接持久化到磁盘,真正保证数据的持久性;
-
2(实时写,延迟刷):表示每次事务提交时都只是把 redo log 写到 page cache,具体的刷盘时机不确定。
除了上面几种机制外,还有其它两种情况会把redo log buffer中的日志刷到磁盘。
-
定时处理:有线程会定时(每隔 1 秒)把redo log buffer中的数据刷盘。
-
根据空间处理:redo log buffer 占用到了一定程度( innodb_log_buffer_size 设置的值一半)占,这个时候也会把redo log buffer中的数据刷盘。