文章目录
日志的类型
undo log
:回滚日志,是 Innodb 储存引擎层生成的日志,实现了事务的原子性,主要用于事务的回滚和MVCC
redo log
:重做日志,是 Innoddb 储存引擎层生成的日志,实现了事务的持久性,主要用于掉电等故障恢复
binlog
:归档日志,是 Server 层生成的日志,主要用于数据备份和主从复制
为什么需要 undo log
作用
事务回滚
这里就要提及到 事务
的原子性,无论是单条 sql 语句还是多条 sql 语句,都会需要开启事务,当开启事务后,就有可能会发生某条 sql 语句报错
当发生报错后,事务中之前修改的数据就需要去进行修复回滚,除了这种报错的情况,还有就是在执行事务的过程中 MySQL 发生了崩溃,当再次重启的时候,也需要去修复回滚数据
当回滚的时候,就能够使用到 undo log
了,它保证了事务的原子性
由图可以看出大概的流程,实际上的步骤就是记录 回滚所需要的信息 到 undo log 当中
比如:
- 插入记录,记录这条记录的主键,当回滚的时候,根据主键值进行删除
- 所以说当事务回滚后,如果主键是递增的,那么当前递增值就丢失了
- 插入 158 ,恢复删除158,再插入的时候就是从159 开始了
- 更新记录,记录更新前的值,当需要回滚的时候,更新到旧值
- 删除记录,记录删除前的整条记录,当需要回滚的时候,重新插入当前记录
这样就很清晰明了了,也就是要在操作之前记录上一步要变化的值,以防止事务失败而导致的不一致性
并且由于不同的操作,所需要的东西实际上是不相同的,所以记录的 undo log 格式自然也就不相同了
MVCC
MVCC 为多版本并发控制,初衷是因为读事务和写事务之间的冲突,避免写操作等待读操作,几乎所有的主流数据库都采用了MVCC的方式。而undo log
在这其中的作用就是记录历史的版本数据,来满足MVCC读取旧版本数据的需求
每条记录都会产生属于自己的版本链,在经历多次更新 / 删除操作时候,就会通过 roll_pointer 去传承一个链表,并且记录是哪个事务修改的
- 因此会有两个隐藏列 回滚指针
roll_pointer
和 事务idtrx_id
版本链的结构如下:
所以当事务要去进行快照读(Read View)的时候,就会去比对其中的 trx_id
判断当前事务是否是可以被读取的数据,从而读取到历史数据
- 每个事务都有一个唯一的id,并且事务id是递增的
为什么需要 redo log
首先需要提及到 MySQL 的持久化机制,是需要经历一个缓冲池 BufferPoll 的,也就是在命令执行完成后,并不是直接写到文件当中,而是先写到了缓冲池,再由缓冲池统一写到文件当中。那为什么需要缓冲池这一个机制呢?
为什么需要 BufferPoll
作用
简单来说,就是提升查询和插入的效率。
思考一个问题,IO一般来说是很慢的,如果每次都从文件中直接读取,那么读取的这个时间耗时就会很长,会很影响体验。除了读取,还有写入,我每写一条记录进行一次IO,那么 100 条记录,就进行了100次IO,是很耗时的,优化的方案应该是 100 条记录统一的写入到文件当中,把IO过程归到一起。
所以说 MySQL 设计了缓冲池这一机制
所以说 MySQL 的 BufferPoll 机制储存了一部分的记录
- 当要查询的时候,先走缓冲池,是否有相应数据,如果没有,才需要进行IO加载,并缓冲到缓冲池中
- 当修改的时候,同样也是先走缓冲池,数据库的读取实际上是按页的方式读取的,并不是一行行的进行读取,所以会去寻找当前行所在的页,修改该页的数据,然后将其标记为脏页(被修改的页面),这里的脏页的目的就是为了减少磁盘IO的次数,尽量统一的将修改的数据进行写入。
结构
缓冲池需要缓冲什么?
由于 InnoDB 会把存储的数据划分为若干个[页],以页作为磁盘和内存交互的基本单位,一个页的大小默认为 16KB
所以缓冲池也应当缓冲数据[页]
当 MySQL 启动的时候,InnoDB 会为 Buffer Pool 申请一片连续的内存空间,然后按照默认的 16KB 的大小划分出一个个的页,Buffer Pool 的页就叫做缓存页,此时这些缓存也都是空闲的,之后随着程序的运行,才会有磁盘上的页被缓存到 Buffer Pool 中
图中的数据页 + 索引页就是缓存的页面
其他内容不属于日志的内容,不详细深入,但是了解一下 Undo 页
Undo 页是指事务更新记录前要记录相应的 undo log
这里的Undo 页并不是说 undo log
也要像数据页一样走缓存写入,undo log
是直接进行的磁盘写入,而缓存中的 Undo 页则是为了更快的查询要进行回滚的数据,当事务失败需要进行回滚的时候,就可以直接走缓存,回滚当前事务相关的页面。
redo log
什么是 redo log
redo log 是物理日志,记录了某个数据页做了什么修改,对 XXX 表空间中的 YYY 数据页 ZZZ 偏移量的地方做了AAA 更新,每当执行一个事务就会产生这样的一条或者多条物理日志。
了解了 BufferPool 后,就可以知道他是作为缓存作用,加快查询和插入的效率,但是 BufferPool 是基于内存的,是不可靠的,因为 MySQL 也是一个程序,当 MySQL 崩溃后,内存中的脏页数据还没来得及写入磁盘就丢失了。
所以需要防止这种情况的发生,当有一条记录需要更新的时候,InnoDB 引擎就会先更新内存(同时标记为脏页),然后把记录写到 redo log 里面,这个时候更新就算完成
WLA(Write-Ahead Logging) 技术:指的是 MySQL 的写操作并不是立刻写到磁盘上,而是先写日志,然后在合适的时间在写到磁盘上
所以说在将数据写到缓冲池的时候,需要有一个日志redo log
来保证数据不会丢失,当数据完整写入缓冲池后,日志redo log
也应当是完整的,如果不是就需要回滚。
如果日志完整了就保证了缓冲池里的数据不会丢失,但此时的事务还没有提交,数据也还没有真正写到磁盘当中,只有当数据写到磁盘后,两者保持一致性,才是真正的完成了事务
上面有说到一个 Undo 页,是写入 undo log 的缓存,需要去记录对应的 redo log
因为开启事务后,InnoDB 层更新记录前,是需要记录 undo log 的,因为需要预防回滚的情况会发生。
所以需要保证 undo log 的写入,因此需要记录到 redo log
redo log 和 undo log 有什么区别?
两个都是属于 InnoDB 存储引擎的日志,它们的区别在于:
- redo log 记录了此次事务 完成后 的数据状态,记录的是更新之后的值
- undo log 记录了此次事务 开始前 的数据状态,记录的是更新之前的值
事务提交之前发生了崩溃,会通过 undo log 进行事务回滚,同时 redo log 也需要进行回滚
- redo log 需要和 binlog 保持一致性,下面会说到 bin log
当事务提交后,但是缓存中的脏页还没有刷盘到磁盘中,系统崩溃了,重启后,就会根据 redo log 的 commit 记录进行数据恢复
PS:要注意一个事情, redo log
是针对 完整页 的恢复,对于残缺页是没有办法恢复的
残缺页和完整页是怎么发生的?
残缺页导致的原因:当在刷盘的过程中的时候,遇到了宕机,可能会导致一个数据页 16KB,但只写入了15KB,这也就导致了残缺页的发生,因此会引入一个新的机制double write
二次写
double write
会创建一个新的结构,double write buffer ,这个区占用 2M 内存空间,同时有一个相对应的持久化文件 double write文件,当 BufferPool 中的脏页要进行刷盘的时候,就需要进行二次写
- 第一步是写入 double write buffer 中
- 第二部进行 IO 写入到 double write文件中,
- 当文件写入完毕,再进行数据页的刷盘
当有了这个结构后,再遇到残缺页的情况
在系统崩溃后,重启遇到残缺页,通过 double write 文件进行数据页恢复,这样就可以适配 redo log 的完整数据恢复了,且不会遇到残缺页的情况
redo log 和 数据都要写到磁盘,为何多此一举?
首先看两种的写法
- 数据写到磁盘:随机写,需要找到当前页所在的位置,然后在写入磁盘,页的位置不固定
- redo log 写入磁盘:顺序写,只需要在尾部添加记录
所以说 redo log 写入的效率会更高,因此,磁盘改为在缓存中保存后再写,减少了IO的次数
到这里,可以看到 redo log 有两大优势
- 保证了 ACID 中的 D 持久性,让数据库有 崩溃恢复 的功能,
crash-safe
,重启后之前提交的记录都不会丢失 - 将随机读变为了顺序读,减缓了IO次数,提升 MySQL 写入磁盘的能力
redo log 是直接写入磁盘么?
并不是,和数据页的情况一样,即使它是顺序写,但也承受不住IO次数过多,耗时也是会变长,所以MySQL也选择引入了一个缓冲池 redo log buffer
,每当产生一条 redo log 的时候,都写入缓冲池的时候,根据策略持久化到磁盘的 redo log 当中
redo log buffer 默认为 16MB,可以通过innodb_log_Buffer_size
参数动态的调整大小,根据需求的不同调节
redo log 刷盘策略
由于有一个缓冲池 redo log buffer
,所以需要有个刷盘的时机
- MySQL 关闭时
- redo log buffer 的写入量达到总容量的一半
- 后台线程每隔 1s 刷盘一次
- 事务提交后,写入刷盘(这个可以通过参数配置选择不同的方案)
- 除了本项,上三项都是固定的策略
参数详情
InnoDB 一共提供了三种策略,由参数 innodb_flush_log_at_trx_commit
参数控制(默认值为 1)
- 参数为 0 :事务提交后,不操作
- 参数为 1 :事务提交后,将 redo log buffer 中的 redo log 写入 redo log 文件,并刷盘(fsync)
- 参数为 2 :事务提交后,将 redo log buffer 中的 redo log 写入 redo log 文件,但不进行刷盘
- 这里并不意味着成功写入了磁盘,没有进行一个刷盘(fsync)操作,有可能会在文件系统的缓冲中(Page Cache),还没有完全写入文件
参数 1 对于事务是安全的,但 0 和 1 对于事务可能是不安全的
参数 0 : 由于没有操作,等待每一秒的刷盘操作,如果MySQL崩溃了,会导致上一秒的所有事务数据丢失,安全性无法保证
参数 2 : 相比较于 0 安全些,如果 MySQL 崩溃,并不会影响到数据的写入,只有当操作系统崩溃的时候,那会丢失上一秒的所有事务的数据
这三个参数有什么意义?它的应用场景在哪里?
- 数据安全性: 参数 1 > 参数 2 > 参数 0
- 写入效率:参数 0 > 参数 2 > 参数1
看到这两个性质,就能够明白了它的场景,数据安全性和写入效率是不可得兼的,根据两者的取舍决定使用哪个参数
- 比如需要高安全性,数据不能够丢失,那么只能够采取参数1了,但同时效率也会下降
- 再如可以忍受一秒数据的丢失,那可以采取参数0,使用写入效率最高的参数
- 或者取中,采用风险性和效率都是中间的参数2,只需要保证服务器不会重启,断电,即使数据库崩溃,也不会丢失数据,性能也比 1 更好
redo log 文件组
redo log 大小并不是无限的,而是固定的,目的是为了节省空间,毕竟有些旧的事务日志也就不必要储存了,可以清理掉。
默认情况下,InnoDB 有一个重做文件组,其中有两个 redo log 文件,,这两个 redo 日志的文件名叫 :ib_logfile0
和 ib_logfile1
两个文件各自的大小分别是 1GB,总共就可以记录 2GB 的操作
为了复用,所以采取一种循环写的方式,循环队列
- write pos 是写入的指针,向左移动
- check point 是对应数据脏页还未刷盘的开始指针,可以说是队头,也是向左移动
- write pos ~ check point 之间的数据,就是已经完成了脏页1的刷盘操作,不需要redolog保证脏页会丢失,也就已经是无用的数据
- check point ~ write pos 之间的数据,是对应的脏页还没有完成刷盘的操作,需要去保证脏页不会丢失的一个情况
那有没有可能整个redo log 都是还未刷盘的脏页对应日志
是有可能出现这个情况的,因此,在 redo log 日志满了之后,需要主动触发脏页刷新机制,也就是主动让缓存池中对应的脏页写到磁盘,做持久化的操作
为什么需要 binlog
redo log 和 undo log 都是 innodb 引擎独有的,但 binlog 是大家都有的
binlog 文件时记录了所有数据库表结构变更和表数据修改的日志,不会记录查询类的操作,比如 SELECT…
这么看来的话,binlog 和 redo log 的记录十分相像,但并不相同,最明细的就是目的,binlog 是保留了全量的日志,用于主从同步的目的,redo log 是保存一段时间的日志,主要是实现 crash-safe 能力,有一定的容灾能力
redo log 和 binlog 有什么不同
主要分为 4 个方向的不同
- 适用对象不同
- binlog 是 MySQL 的 Server 层实现的日志,所有的存储引擎都可以使用
- redo log 是 InnoDB 存储引擎实现的日志
- 文件格式不同
- binlog 有 3 中格式类型
STATEMENT
:每条修改的SQL记录都记录到 binlog,记录了逻辑操作,相当于逻辑记录- 缺点:对于动态函数的问题,无法保证主从复制的从库执行结果是否一致
ROW
:记录行数据最终被修改成了什么结果,可以说是物理日志,详细记录了每条记录的值被修改成了什么值- 缺点:会造成文件过大,因为每次修改都要写入,当一条 update 更新了几十条记录的时候,
STATEMENT
格式可能只记录一条SQL,而ROW
则需要记录几十上百条数据变动
- 缺点:会造成文件过大,因为每次修改都要写入,当一条 update 更新了几十条记录的时候,
MIXED
:STATEMENT
和ROW
混合使用,会根据不同的情况使用ROW
和STATEMENT
- binlog 有 3 中格式类型
- 写入方式不同
- binlog 是追加写,记录的是全量的数据,当一个文件写满后,就创建新的文件继续写,不会去覆盖旧日志
- redo log 是 循环写,空间大小是固定的,主要是对脏页保证的作用
- 用途不同
- binglog 用于 备份恢复、主从复制
- redo log 用于 服务器断电、宕机等故障恢复
bin log 刷盘策略
事务执行过程中,先把日志写到 binlog cache (Server 层的 cache),事务提交的时候,再把 binlog cache 写到 binlog 文件中
MySQL 给 binlog cache 分配了一片内存,每个线程一个,参数 binlog_cache_size
用于控制单个线程内 binlog cache 所占内存的大小。如果超过了这个内存限制,就要被写到磁盘当中
每个线程写入的都是同一个 binlog
- 图中的 write 就是把日志写道 binlog 文件,但是由于还没有进行刷盘操作,仍会在 操作系统的 page cache 当中,所以 write 的速度是比较快的,因为不涉及磁盘IO
- 图中的 fsync,才是将数据持久化到磁盘的操作,会涉及到磁盘IO,比较缓慢
MySQL 提供了 sync_binlog
控制同步的时间,和 redo log 的有些许相似
- 参数为 0 :提交事务后只 write ,不 fsync,由操作系统决定刷盘
- 参数为 1 :提交事务后 write 并立刻 fsync
- 参数为 N:提交事务后 write,但累计 N 个事务后才 fsync
完整的事务执行过程
优化器分析出最小的执行计划后,执行器按照计划执行
UPDATE student SET name = 'hello world' where id = 1;
以一条更新SQL为例书写流程:
- 执行器调用存储引擎的接口,通过主键索引树获取 id = 1 这一行记录
- 如果该行记录对应的页在缓存 BufferPool 中,可以直接获取
- 如果该行记录对应的页不在 BufferPool 中,需要将数据页从磁盘中读取到 BufferPool,然后返回
- 执行器得到记录后,会查看当前记录和更新后的记录是否相同
- 相同:跳过后续流程,不需要变更
- 不相同:把更新前 和 更新后的数据作为参数传递给 InnoDB 层,让 InnoDB 执行更新记录操作
- 开启事务,更新记录前,先记录 undo log,在写入磁盘
undo log文件
的同时,也要写到 Buffer Pool 中的 Undo 页(方便后续读取),同时undo log 文件
的修改需要记录对应的 redo log(这个过程是防止undo log中的历史记录丢失)到 redo log buffer 当中 - 开始更新记录,将修改的行对应的页,更新到 Buffer Pool 当中(标记为脏页),将记录写入 redo log中。由于采用 WAL 技术,不会立刻将脏页刷盘到磁盘当中,而是先写完日志。
- 记录更新完毕,需要记录对应的 binlog,此时开始写到 binlog cache
- 当 binlog cache 写入完毕,就进入到提交阶段
- 提交阶段又分为两阶段提交
两阶段提交
原理
上面说到 redo log 和 binlog 要保持一致性,为什么?
redo log是为主库的持久化提供一个 crash-safe 功能的,主要是保证数据不会丢失
binlog 则是为从库提供数据同步功能的
由于这两个特性,如果他们不一致,会造成主从数据不一致的问题,下面举例说明:
- 当 redo log 写入完成,发生断电重启,binlog 没有写入,就会导致主库成功恢复记录,而从库复制过去的数据记录缺少了记录,发生主从不一致
- 当 binlog 写入完成,发生断电重启,redo log 没有写入,就会导致主库没有成功恢复记录,而从库因为 binlog 多出来了记录,发生主从不一致
所以,要抛弃这种半成功的状态,需要保证 redo log 和 binlog 的一致性
就提出了两阶段提交这种方案
在 MySQL 的 InnoDB 存储引擎中,为了维持两个日志的一致性,使用了 内部XA事务,由 binlog 作为协调者,存储引擎是参与者
两阶段提交将单个事务拆分了2个阶段
- 准备 prepare:将 XID(内部XA事务的ID)写入 redo log,同时将 redo log 对应的事务状态设置为 prepare,将 redo log 刷盘
- 提交 commit: 将 XID 写到 binlog,然后将 binlog 刷新到磁盘,然后调用提交事务接口,将 redo log 状态设置为 commit(也需要刷盘到 redo log)
故障时刻分析
当发生故障(断电重启,宕机恢复)等情况,重启MySQL后,恢复的阶段首先是去读取 redo log,寻找相应的XID(内部XA事务)
- A时刻:此时 redo log 和 binlog 都还没有进行刷盘,也就是 redo log 中找不到对应的 XID 事务,同样 binlog 中也不存在,所以相当于这个事务是丢失了的,可以抛弃,一致性也相同
- 由于WAL,日志肯定是要比数据先写入磁盘当中,可以看缓冲池脏页的刷新策略,理论上日志肯定在脏页之前刷盘
- redo log 日志满了的时候,主动触发脏页刷新
- 缓冲池空间不足,主动触发脏页刷新(日志肯定早早写入了,因为淘汰策略,已经准备淘汰的脏页,相应的日志肯定早已经生成了)
- MySQL 认为空闲的时候,后台线程定期将适量脏页刷新到磁盘(既然都空闲了,redo log 也应该写完了)
- MySQL 正常关闭时,刷盘脏页,redo log 同样也刷盘,并且每隔 1 s 刷盘一次
- 由于WAL,日志肯定是要比数据先写入磁盘当中,可以看缓冲池脏页的刷新策略,理论上日志肯定在脏页之前刷盘
- B时刻:此时 redo log 写入到了一半的内容,就会出现 redo log 中能查询到 XID,而 binlog 中没有。这个时候也是需要回滚事务,因为这相当于说这个事务失败了,需要删除这段 redo log 日志
- 理论上 redo log 只会丢失 1s 的数据,redo log 并不是完全依赖提交事务后才进行持久化的,前面有说到后台线程会每隔 1s 对 redo log buffer 进行一次刷盘
- C时刻:此时 redo log 写入完毕,但是没有 binlog对应,查询不到 binlog 对应的 XID,所以需要进行事务回滚
- D时刻:此时 redo log 写入完毕,而 bin log 只写入了一半,在这里我查不到相应的资料,我倾向于两个方案
- 第一个 XID 是在完全刷盘后才会进行写入的,这样比对XID就可以找出这种情况
- 第二种 XID 在开头写入了,两者都有 XID 的情况下,遍历进行比较数据是否一致,但由于结构不相同,应该不是这种。
- E时刻:此时 redo log 和 binlog 都写入完成,XID 都能够找到,所以可以修复数据,将 redo log 置为 commit,完成事务
如果事务没有提交,redo log 有可能会被刷新到磁盘么?
这个看前面的 redo log 刷盘策略,有一个固定的刷盘策略,每隔 1s 后台线程就会将 redo log buffer 中的 redo log 持久化到磁盘当中,所以是有可能的
弊端
两阶段提交虽然保证了两个日志的数据一致性,但是有两个方面的影响:
- 磁盘 I/O次数高:每个事务都需要进行两次 fsync (刷盘)
- 锁竞争激烈:两阶段提交虽然保证了单事务一致,但是在多事务的情况下需要保证原子性才能够一致,因此需要加锁
- 加锁的时机:prepare 阶段获取锁,commit阶段结束,释放锁,性能不佳
组提交
MySQL 引入 binlog 组提交(group commit)机制,当有多个事务提交的时候,会将多个 binlog 刷盘操作合并成一个,从而减少磁盘 I/O 的次数
引入组提交后, prepare 阶段无变化,只针对于 commit 阶段,将 commit 阶段分为三个过程:
- flush 阶段:多个事务按进入的顺序将 binlog 从 cache 写入文件(不刷盘)
- sync 阶段:对 binlog 文件进行刷盘操作
- commit 阶段:对各个事务按顺序做 InnoDB commit 操作
每一个阶段都有一个队列,每个阶段都有锁进行保护,因此保证了事务的写入顺序,第一个进入队列的事务会成为 leader,领导所在的队列的所有事务
binlog 有组提交,那么 redo log 有组提交么?
在 MySQL 5.6 之前没有 redo log 组提交,在 5.7 版本加入 redo log 组提交
- 5.6 的时候,每个事务各自在 prepare 阶段对 redo log 进行刷盘
- 5.7 的时候,prepare 阶段不再让事务各自执行,将 prepare 阶段移动到 flush 阶段之中,在 sync 阶段之前完成写入,通过延迟刷盘时机,进行组写入
资料来源
MySQL 崩溃恢复过程分析
double write(二次写)
庖丁解InnoDB之UNDO LOG
MySQL 日志 | 小林 coding