- undo log(回滚日志):引擎层生成的日志,实现了事务的原子性,用于事务回滚和MVCC。
- redo log(重做日志):引擎层生成的日志,实现了事务的持久性,用于非正常关闭的数据恢复。
- bin log(归档日志):Server层生成的日志,主要用于数据备份和主从复制。
undo log详解:
undo log是一种用于撤销回退的日志,在事务没提交之前,会先记录更新前的数据到undo log日志文件中,事务回滚时,可以利用undo log来进行回滚。
undo log中记录着update、insert、delete等修改操作的反操作。例如删除一条记录时,undo会先将记录中的内容先记录下来,并使用insert操作。
每个undo log都有一个roll_pointer指针和一个trx_id事务id:
- 通过事务id我们可知本次修改是哪个事务执行的
- 通过roll_pointer我们可将undo log串成一个链表,从最新指向最旧,这个链表就是版本链
- 这两个字段是每行数据的隐藏字段
- 每次事务开启时,roll_pointer会指向开启前的undo log ,且该undo log作为版本链新的头节点
undo log利用版本链和Read View实现MVCC(多版本并发控制):
- 对于读已提交和可重复读隔离级别的事务来说,它们的快照读(普通select 语句)是通过Read View+undo log来实现的,它们的区别在于创建Read View的时机不同。
- 读已提交:每次select语句后都会生成一个新的Read View,也就是说可以看到其他事务提交的结果,可能会造成不可重复读。
- 可重复读:首次select语句后生成一个Read View,后续的查询都使用该Read View,期间其他事务的已提交数据无法感知,避免了不可重复读的问题。
- 串行化:无快照读,不依赖Read View,通过加锁实现
Read View详解:
核心字段:
- trx_ids:记录生成视图时系统中所有活跃(未提交)的事务id的列表
- min_trx_id:活跃事务列表中的最小事务id,表示活跃事务的起始边界
- max_trx_id+1:即将分配的事务id,也就是当前最大事务id+1,表示未来事务的边界
- m_creator_trx_id:生成视图的当前事务id
可见性判断规则:
- 可见:事务id<min_trx_id(该版本在Read View生成前就已提交)
- 不可见:事务id>max_trx_id||事务id包含在活跃事务列表中(Read View生成时处于未提交状态)
- 例外可见:事务id=m_creator_id(修改操作是生成视图的事务执行的)
在实现这两个隔离级别的事务时,查询的行数据会通过事务id字段与Read View中的字段进行对比,通过roll pointer在版本链中遍历,直到找到可见的数据记录,从而并发控制事务访问同一个数据记录的行为,这就称为MVCC。
redo log详解:
Buffer Pool提高了读写效率,但它是基于内存的,遇到非正常关闭时会导致数据丢失。
所以数据库更新记录时,会先在Buffer Pool中更新并标记为脏页,随后将本次修改以redo log的形式记录下来,这时候更新操作才算完成了。
脏页数据刷盘时依然是根据Buffer Pool,日志用于崩溃恢复时重建脏页数据。
后续,引擎会选择合适的时机异步刷盘脏页数据,这就是WAL(Write-Ahead Logging)技术。
WAL:MySQL的写操作并不直接写入到磁盘上,而是先记录日志,再在合适的时机写入磁盘中。
redo具体记录什么?
- redo log是物理日志,记录了某个数据页做了什么修改,例如对XXX表空间中的YYY数据页ZZZ偏移量的地方做了AAA更新,每当执行一个事务时就有一条或者多条这样的物理日志。
- 事务提交时,只需将redo log持久化到磁盘中即可,无需等待缓存中的脏页数据持久化。且redo log是顺序写入磁盘,Buffer Pool是随机写入,需要时间寻找可用空间。因此日志的持久化是远远快于完整数据持久化的。
- 系统崩溃时,MySQL就可以根据redo log重构脏页数据,恢复到最新状态。
产生的redo log是否直接写入磁盘中?
- 实际上,redo log也并非直接写入磁盘中,因为这样会产生大量IO操作,而且磁盘的运行速度远低于内存。
- redo log也有自己的缓存——redo log buffer,每当有redo log产生,会先写入到缓存中,后续再进行持久化。缓存默认大小16mb,可以通过innodb_log_Buffer_size参数动态调整大小,增大它的大小可以让MySQL处理大事务时不必直接写入磁盘。
redo log什么时候刷盘?
- MySQL正常关闭时
- redo log buffer记录的写入量大于redo log buffer内存空间的一半
- InnoDB的后台线程每隔一秒刷盘一次
- 每次事务提交时都将缓存持久化到磁盘(可选策略,通过innodb_flush_log_at_trx_commit参数控制)
redo log文件写满了怎么办?
- 默认情况下,InnoDB存储引擎有1个重做日志组,日志组由2个redo log文件组成,分别名为ib_logfile0和ib_logfile1
- 两个文件的大小是固定且一致的,文件组是以循环写的方式工作的,类似于循环队列
- 所以file0会先被写满,写满后切换至file1,file1写满后又会回到file0
- redo log是为防止脏页数据意外丢失而存在的,当脏页数据已经持久化到磁盘中,我们就需要从redo log中移除对应的记录。check point相当于要擦除(覆盖)的位置(类似于redis的环形缓存区)
- write pos和checkpoint的移动都是顺时针方向;
- write pos到checkpoint之间的部分,用于记录新的更新操作
- checkpoint到write pos之间的部分,为待持久化的脏页数据记录
- 若write pos追上了checkpoint,意味着redo log文件已满,此时MySQL不能执行新的更新操作,导致MySQL阻塞。阻塞时会将Buffer Pool的脏页刷新到磁盘中,然后标记redo log哪些记录可以被擦除,接着对旧的redo log记录进行擦除,等待文件腾出空间后checkpoint继续移动,MySQL恢复正常运行。
- 对于并发量大的系统,适当设置redo log的文件大小非常重要,否则容易导致MySQL阻塞。
redo log和undo log的区别?
- redo log:记录的是事务修改后的数据状态,记录的是更新后的值,用于事务崩溃恢复,保证事务的持久性。
- undo log:记录了事务修改前的数据状态,记录的是更新前的值,用于事务回滚,保证事务的原子性。
- 事务提交前发生错误可通过undo log回滚,即使是崩溃也没事,因为事务没提交的数据本身就是无效的。
- redo log针对的是事务提交后但数据还未持久化就发生崩溃的场景,重启后MySQL可根据redo log恢复事务
有了redo log后,通过WAL技术,就可以保证即使数据库异常关闭重启,之前提交的记录都不会丢失,这个能力就称为crash-safe(崩溃恢复)。保证了数据的持久性和一致性。
binlog详解
前文介绍的undo log和redo log都是Innodb存储引擎生成的。
MySQL完成更新操作后,server层会生成一条binlog,事务提交后会将该事务执行产生的所有binlog统一写入binlog文件中。
binlog文件记录了所有数据库表结构变更、表数据修改的操作,但不会记录查询相关操作。
为什么还需要redo log?
最开始MySQL并没有Innodb引擎,使用的是MyISAM引擎,所以没有crash-safe特性,bin log只能用于归档,而redo log可实现crash-safe。
redo log和bin log的区别:
1、适用对象不同:
- binlog是MySQL的server层实现的日志,所有引擎都可以使用
- redo log是Innodb引擎实现的日志
2、文件格式不同:
- binlog 有 3 种格式类型,分别是 STATEMENT(默认格式)、ROW、 MIXED,区别如下:
- STATEMENT:每一条修改数据的 SQL 都会被记录到 binlog 中(相当于记录了逻辑操作,所以针对这种格式, binlog 可以称为逻辑日志),主从复制中 slave 端再根据 SQL 语句重现。但 STATEMENT 有动态函数的问题,比如你用了 uuid 或者 now 这些函数,你在主库上执行的结果并不是你在从库执行的结果,这种随时在变的函数会导致复制的数据不一致;
- ROW:记录行数据最终被修改成什么样了(这种格式的日志,就不能称为逻辑日志了),不会出现 STATEMENT 下动态函数的问题。但 ROW 的缺点是每行数据的变化结果都会被记录,比如执行批量 update 语句,更新多少行数据就会产生多少条记录,使 binlog 文件过大,而在 STATEMENT 格式下只会记录一个 update 语句而已;
- MIXED:包含了 STATEMENT 和 ROW 模式,它会根据不同的情况自动使用 ROW 模式和 STATEMENT 模式;
- redo log是物理日志,记录的是某个数据页做了什么修改,比如对XXX表空间中的YYY数据页ZZZ偏移量的地方做了AAA更新。
3、写入方式不同:
- binlog是追加写,写满一个文件,就创建一个新的文件继续写入,不会覆盖以前的日志,保存的是全量日志。
- redo log是循环写,日志空间大小固定,全部写满就从头开始,保存未被刷入磁盘的脏页数据。
4、用途不同:
- binlog用于备份恢复、主从复制
- redo log用于崩溃等故障的数据恢复
主从复制如何实现?
MySQL的主从复制依赖于binlog,也就是记录MySQL上的所有变化并以二进制形式保存在磁盘上。复制过程就是将binlog中的数据从主库传输到从库上,一般是异步操作。
三个阶段:
- 写入binlog:主库写binlog日志,提交事务,并更新本地存储数据
- 同步binlog:将binlog复制到所有从库上,每个从库将binlog写到暂存日志中
- 回放binlog:恢复binlog,更新存储引擎中的数据
binlog什么时候刷盘?
- 事务执行过程中,日志先写到binlog cache(Server层的缓存),事务提交时再将缓存中的日志写到binlog文件中。
- 一个事务的binlog是不能拆开的,需要保证一次性写入,避免破坏原子性,也对应一个线程只能同时执行一个事务的设定。
- MySQL为每个线程分配了一片内存用于缓冲binlog,该内存叫binlog cache,参数binlog_cache_size用于控制单个线程内binlog cache所占内存的大小
- 事务提交时,执行器将binlog cache里的完整事务写入到binlog文件中,并清空缓存
write操作即为写到binlog文件中,但并不是持久化,而是在内核缓存中存储,不涉及磁盘io
fsync才是真正的刷盘操作,涉及磁盘io
MySQL提供一个sync_binlog参数(默认为0)来控制数据库的binlog刷到磁盘上的频率:
- sync_binlog=0时,表示每次提交事务都只write,不fsync,后续交由操作系统决定何时将数据持久化到磁盘。
- sync_binlog=1时,表示每次提交事务都会write,并马上执行fsync
- sync_binlog=N时,表示每次提交事务都会write,但积累N个事务后才fsync
update语句的执行过程:
优化器分析出成本最小的执行计划后,执行器按照执行计划开始更新:
1、执行器调用存储引擎接口,通过索引查询记录
- 若该条记录所在的页存在于Buffer Pool中,直接返回给执行器使用
- 若该条记录在Buffer Pool中,则从磁盘读取对应数据页到Buffer Pool中,并返回给执行器
2、执行器得到聚簇索引记录后,会比较更新前和更新后的记录是否一样
- 若一样则直接跳过后续的日志写入、事务提交等操作。
- 若不一样则将修改前和修改后的记录都作为参数传入InnoDB层,执行真正的更新记录操作。
3、开启事务,记录undo log,写入Buffer Pool的Undo页面,Undo页面变化时记录对应的redo log
4、InnoDB层开始更新记录,并更新内存(同时标记为脏页),然后将记录写入redo log里面,此时更新操作完成。
- 为减少磁盘IO并不直接将脏页写入磁盘中,而是先记录redo日志,后寻找合适时机刷盘脏页数据。也就是WAL技术。
5、至此,更新语句结束。
- 语句完成后,记录该语句对应的binlog,此时记录的binlog会被保存到binlog cache,并没有刷新到磁盘上的binlog文件,事务提交时才会统一将该事务运行过程中的所有的binlog文件刷新到磁盘(中途会先write到内核的页缓存中)中。
6、两阶段提交日志。
为什么需要两阶段提交?
事务提交后,redo log和binlog都需要持久化到磁盘中,但这两个是独立逻辑,可能出现半成功的情况,造成数据不一致,需要确保它们提交的原子性。
redo log提交成功,binlog提交失败。主库数据更新成功但binlog丢失记录,进而导致主从复制时从库数据是旧的,以及备份的数据不一致。
binlog提交成功,redo log提交失败。主库数据更新丢失,但从库和备份有记录,导致主库数据是旧的,数据不一致。
两阶段提交则是将单个事务拆分为了两个阶段:prepare阶段和Commit阶段。每个阶段由协调者和参与者共同完成。这里的Commit阶段与commit语句不同,commit语句包含了这里的Commit阶段。