美团面试:binlog、redolog、undo log底层原理是啥?分别实现ACID哪个特性?(尼恩图解,史上最全)
文章很长,且持续更新,建议收藏起来,慢慢读!疯狂创客圈总目录 博客园版 为您奉上珍贵的学习资源 :
免费赠送 :《尼恩Java面试宝典》 持续更新+ 史上最全 + 面试必备 2000页+ 面试必备 + 大厂必备 +涨薪必备
免费赠送 :《尼恩技术圣经+高并发系列PDF》 ,帮你 实现技术自由,完成职业升级, 薪酬猛涨!加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷1)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷2)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷3)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 资源宝库: Java 必备 百度网盘资源大合集 价值>10000元 加尼恩领取
美团面试:binlog、redolog、undo log底层原理是啥?分别实现ACID哪个特性?(尼恩图解,史上最全)
尼恩特别说明: 尼恩的文章,都会在 《技术自由圈》 公号 发布, 并且维护最新版本。 如果发现图片 不可见, 请去 《技术自由圈》 公号 查找
尼恩说在前面
在40岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如得物、阿里、滴滴、极兔、有赞、希音、百度、网易、美团的面试资格,遇到很多很重要的面试题:
- 谈谈:mysql 中 redo log 、undo log、 binlog 分别实现了事务ACID的那些特性?
- 谈谈:如何解决 bin log 与 redo log 的一致性问题?
- 谈谈:一条 SQL 更新语句是如何执行的?
- 谈谈: redo log / bin log 两阶段提交原理
- .......
最近有小伙伴在面试 美团,又遇到了 redo log 、undo log、 binlog 与事务ACID 相关的面试题。
小伙伴懵了,因为系统的回答清楚,支支吾吾的说了几句,面试官不满意,面试挂了。
所以,尼恩给大家做一下系统化、体系化的梳理,使得大家内力猛增,可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提”。
当然,这道面试题,以及参考答案,也会收入咱们的 《尼恩Java面试宝典PDF》V171版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。
最新《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请关注本公众号【技术自由圈】获取,回复:领电子书
1 基础知识: MySQL架构&SQL语句执行流程
尼恩的叙事风格:故事从最基础的地方讲起。
先简单了解一下MySQL大概的架构:
- 第1层:连接层
对来自客户端的连接进行权限验证并将相关的连接信息维护到连接池中,以便于下次连接。
- 第2层:server 服务层:
提供NoSQL,SQL的API,SQL解析,SQL语句优化,SQL语句缓存等相关组件。
- 第3层:存储引擎层:
提供了一系列可插拔的存储引擎,我们可以通过存储引擎来进行数据的写入与读取,
通过存储引擎,我们可以真正的与硬盘中的数据和日志进行交互,
我们可以根据需求来选择合适的存储引擎进行使用。
- 第4层:文件系统层:
该层包含了具体的日志文件和数据文件以及MySQL相关的程序。
上面的四个部分,MySQL 的架构核心两个部分:服务层(Server Layer)和 存储引擎层(Storage Engine Layer)。
其中,服务层负责 MySQL 的核心逻辑处理,而存储引擎层负责实际的数据存取。
1.1 服务层(Server Layer) 主要负责以下功能:
1.查询解析与优化
- 语法解析:将客户端发送的 SQL 查询转化为可理解的内部结构,检查 SQL 语句是否符合 MySQL 语法规则。
- 查询重写:服务层会将一些查询优化为等效的、执行效率更高的形式,比如将子查询改写为 JOIN,或简化复杂的表达式。
- 查询优化:MySQL 服务层有一个查询优化器,会基于数据统计信息,选择最优的执行计划。包括表的连接顺序、索引的选择等。其优化方式包括基于成本的优化器(CBO)。
2. 查询缓存
MySQL 服务器层可以将一些查询的结果缓存起来,尤其是频繁执行的查询。如果客户端请求的查询已经存在于缓存中,MySQL 可以直接从缓存中返回结果,而无需重新执行查询。
3. SQL 执行
在查询解析和优化之后,服务层负责将 SQL 语句执行。
服务层会将解析后的查询计划传递给存储引擎层,存储引擎负责实际的数据操作。
1.2 Server 服务层(Service Layer)三个核心组件
MySQL Server 服务层(Service Layer)解析 SQL 语句、优化查询以及执行操作的,分别有三个关键组件完成:
-
解析器(Parser)
-
优化器(Optimizer)
-
执行器(Executor)。
每个组件在查询执行的过程中扮演不同的角色,下面分别介绍这三者的作用:
1. 解析器(Parser)
解析器是 SQL 查询执行的第一步,它的职责是将用户发送的 SQL 语句解析为数据库能够理解的内部结构。
- SQL 词法分析:解析器首先对 SQL 语句进行词法分析,将 SQL 语句分割成多个“单词”或“标记”,如表名、列名、关键字等。
- 语法分析:接着,解析器会根据 SQL 语法规则生成对应的解析树(Parse Tree),用来描述 SQL 语句的逻辑结构。这个过程检查 SQL 语句的语法是否正确。
- 语义分析:确认 SQL 语句中涉及的数据库对象是否存在(比如表名、字段名是否有效),并且检查权限。
解析完成后,生成一个中间表示结构,交由下一步进行处理。
2. 优化器(Optimizer)
优化器负责选择最优的执行计划,使查询能够以最高效的方式运行。
- 逻辑优化:优化器会对 SQL 语句进行逻辑优化,比如 SQL 语句重写、消除冗余操作、合并重复条件、重新排列 WHERE 子句中的条件等。
- 物理优化:在物理优化阶段,优化器会选择最优的访问路径和执行顺序。例如,它会决定使用哪种索引(如果有多个索引可选),是否做全表扫描,如何连接多张表(选择嵌套循环、哈希连接或排序合并连接等)。
- 成本估算:优化器会基于数据库的统计信息(例如表的大小、索引的选择性等)来估算不同执行计划的成本,选择代价最低的执行方案。
经过优化后,优化器会生成一个查询执行计划,并交给执行器处理。
3. 执行器(Executor)
执行器的任务是按照优化器生成的执行计划,逐步执行查询,访问数据并返回结果。
- 权限检查:在执行之前,执行器会首先检查用户是否有权限执行相应的操作。如果没有权限,则返回错误信息。
- 执行执行计划:执行器根据生成的执行计划,依次调用存储引擎的接口来执行具体的操作。例如,如果是查询操作,执行器会调用存储引擎来读取相应的数据;如果是插入操作,执行器则会调用存储引擎来插入数据。
- 结果返回:执行器根据查询的结果,将数据以合适的格式返回给客户端。如果涉及多个步骤(如 JOIN 操作),执行器会协调各个步骤的执行,并组合最终的结果集。
三个核心组件之间的交互流程
- 解析器:SQL 语句转换为解析树。
- 优化器:生成最优的执行计划。
- 执行器:根据计划调用存储引擎执行操作并返回结果。
这三个组件相互协作,完成从接收到 SQL 查询到返回结果的整个过程。
具体如下图所示
1.3 SQL语句的执行过程
1.3.1 sql查询(select)语句的执行流程
sql查询语句的执行流程,参考下面的文章:
1.3.2 Mysql数据更新(update)语句的流程:
Mysql数据更新语句的流程, 大致如下图所示:
具体更新一条记录 update table set name=“塔尖技术社区 技术自由圈” where id = 1000;
的流程如下:
-
执行器负责具体执行,会调用存储引擎的接口,通过主键索引树搜索获取 id = 1 这一行记录:
- 如果 id=1000 这一行所在的数据页本来就在 buffer pool 中,就直接返回给执行器更新;
- 如果记录不在 buffer pool,将数据页从磁盘读入到 buffer pool,返回记录给执行器。
-
执行器得到聚簇索引记录后,会看一下更新前的记录和更新后的记录是否一样:
- 如果一样的话就不进行后续更新流程;
- 如果不一样的话就把更新前的记录和更新后的记录都当作参数传给 InnoDB 层,让 InnoDB 真正的执行更新记录的操作;
-
开启事务,记录 undo log
InnoDB 层更新记录前,首先要记录相应的 undo log,
因为这是更新操作,需要旧值记下来,也就是要生成一条 undo log,
undo log 不是之间写磁盘,而是会写入Buffer Pool , 只是会写入 Buffer Pool 中的 Undo 页面。
不过,这里也有WAL思想,在修改该 Undo 页面前,需要先记录对应的 redo log,
所以,这里的流程是:先记录修改 Undo 页面的 redo log ,然后再真正的修改 Undo 页面。
老架构师尼恩提示:Undo Log 的第一个目标就是为了保证事务的 原子性(Atomicity)。事务的原子性意味着事务中的所有操作要么全部成功,要么全部回滚到事务开始之前的状态,不会出现部分执行的情况。如果事务在执行过程中发生错误,或者用户明确发起了回滚操作(
ROLLBACK
),数据库会通过读取 Undo Log 中记录的旧值,恢复被修改的数据到事务开始前的状态。这种恢复操作确保了事务的原子性,即便事务中途失败,数据库也能够恢复到一致的状态。当然,Undo Log 还用于实现 MVCC(Multi-Version Concurrency Control)。当不同事务同时读取数据时,Undo Log 提供了“旧版本”的数据视图,使得即使某个事务在修改数据,其他事务仍然能够读取到事务开始时的数据版本。这不仅提升了并发性能,也间接支持了原子性和隔离性的实现。 所以,Undo Log 实现了对 事务的 原子性、隔离性的支持。
尼恩提示: 模式的时候,一般人都知道 undo log 实现了事务的原子性, 如果把undo log的 隔离性说出来,一定会惊讶到 面试官的。
-
先记录 redo log ,再更新数据,
InnoDB 层开始更新数据记录之前,根据 WAL 思想,先记录修改数据页面的 redo log ,然后再真正的修改数据页面。
修改数据页面的过程是修改 Buffer Pool 中数据所在的页,然后将其页设置为脏页,而不是之间写磁盘。
为了减少磁盘I/O,不会立即将脏页写入磁盘,后续由后台线程选择一个合适的时机将脏页写入到磁盘。
至此,一条记录更新完了。
老架构师尼恩提示:Redo Log 是实现事务 持久性(Durability) 的关键机制之一。事务的持久性意味着一旦事务提交成功,即使数据库系统发生崩溃,系统也能够在重启后保证数据不会丢失。Redo Log 通过记录事务的修改并在崩溃后进行恢复,确保数据库的一致性和数据的持久性。
-
记录 binlog
在一条更新语句执行完成后,然后开始记录该语句对应的 binlog。
此时记录的 binlog 会被保存到 binlog cache,并没有刷新到硬盘上的 binlog 文件。
在事务提交时,才会统一将该事务运行过程中的所有 binlog 刷新到硬盘。
老架构师尼恩提示:Binlog(Binary Log) 在 MySQL 中主要用于 主从复制 和 数据恢复,它通过协调 redo log 和 binlog 的写入,确保事务的一致性( 包括两个一致性:崩溃一致性&主从一致性),特别是在分布式场景下的 主从一致性。
-
事务提交,以两阶段提交 为例:
prepare 阶段:将 redo log 对应的事务状态设置为 prepare,然后将 redo log 刷新到硬盘;
commit 阶段:将 binlog 刷新到磁盘,接着调用引擎的提交事务接口,将 redo log 状态设置为 commit(将事务设置为 commit 状态后,刷入到磁盘 redo log 文件);
老架构师尼恩提示:原始的两阶段提交 的性能比较低,更高性能的提交方式是 组提交,说白了就是批量提交。但是原始的两阶段提交比较好理解,为了方便说明,这里不说组提交的过程,只说两阶段提交.
-
至此,一条更新语句执行完成。
1.4 以下面以一条简单的 SQL update 语句为例,介绍sql更新流程
以下面一条简单的 SQL 语句为例,我们来解释下执行器和 InnoDB 存储引擎在更新时做了哪些事情:
update table set name=“塔尖技术社区 技术自由圈” where id = 1000;
如下图所示:
-
执行器:找存储引擎取到 id = 1000 这一行记录
-
存储引擎:根据主键索引树找到这一行,如果 id = 1000 这一行所在的数据页本来就在内存池(Buffer Pool)中,就直接返回给执行器;否则,需要先从磁盘读入内存池,然后再返回
-
执行器:拿到存储引擎返回的行记录,把 name 字段设置为 “塔尖技术社区 技术自由圈”,得到一行新的记录,然后再调用存储引擎的接口写入这行新记录
-
存储引擎:将这行新数据更新到内存中,同时将这个更新操作记录到 redo log 里面,为 redo log 中的事务打上
prepare
标识。然后告知执行器执行完成了,随时可以提交事务注意不要把这里的提交事务和我们 sql 语句中的提交事务 commit 命令搞混了哈,我们这里说的提交事务,指的是事务提交过程中的一个小步骤,也是最后一步。当这个步骤执行完成后,commit 命令就执行成功了。
-
执行器:生成这个操作的 bin log,并把 bin log 写入磁盘
-
执行器:调用存储引擎的提交事务接口
-
存储引擎:把刚刚写入的 redo log 状态改成提交(
commit
)状态,更新完成
可以看到,所谓两阶段提交,其实就是把 redo log 的写入拆分成了两个步骤:prepare 和 commit。
所以,为什么要这样设计呢?这样设计怎么就能够实现崩溃恢复呢?
根据两阶段提交,崩溃恢复时的判断规则是这样的:
- 如果 redo log 里面的事务是完整的,也就是已经有了 commit 标识,则直接提交
- 如果 redo log 里面的事务处于 prepare 状态,则判断对应的事务 binlog 是否存在并完整
- a. 如果 binlog 存在并完整,则提交事务;
- b. 否则,回滚事务。
结合上面的两阶段提交, 实现了事务的持久性 和一致性。
实际的场景中,上图片中的写入 redo log 和写入 bin log,并不等同于写入磁盘文件,可能仅仅写入内存了
redo log 和 bin log 何时刷盘,尼恩给大家慢慢道来。
尼恩提示: 以上内容比较复杂,后面会在《尼恩Java面试宝典》配套视频中,进行详细解读。
2. undolog 实现 ACID 中的 原子性 + 隔离性
什么是undolog ?
undolog一般叫回滚日志。
事务回滚rollback功能就是通过 undolog实现的,通过undolog保证了为事务的原子性,undolog主要功能如下:
- 事务回滚
- MVCC
什么是事务回滚?
当开启一段事务还未提交时,事务中的操作可能会出现错误异常,这时候就可以通过undo log将事务中的操作进行回滚(rollback),意思是回到事务开启前那个状态。
例如:开启事务后,对表中某条记录进行修改(将该记录字段值由value1 ——> value2 ——> value3 ),如果从整个修改过程中出现异常,事务就会回滚,字段的值就回到最初的起点(值为value1 )。
事务如何通过undo log进行回滚操作呢?
这个很好理解,我们只需要在undo log日志中记录事务中的反向操作即可,发生回滚时直接通过undolog中记录的反向操作进行恢复,例如:
- 事务进行insert操作,undo log记录delete操作
- 事务进行delete操作,undo log记录insert操作
- 事务进行update操作(value1 改为value2 ),undolog记录update操作(value2 改为value3 )
undo log保存的是一个版本链,也就是使用DB_ROLL_PTR这个字段来连接的。多个事务的 undo-log 日志副本 (数据快照),组成了一个 副本链,
value1 ——> value2 ——> value3 的版本链, 如下图:
undo log版本链,数据页中的每行数据都会分配两个字段:trx_id和roll_pointer,
上图中,
- trx_id代表事务id,记录了这一系列事务操作是基于哪个事务;
- roll_pointer代表回滚指针,就是当要发生rollback回滚操作时,就通过roll_pointer进行回滚,这个链表称为版本链。
为了 提升 Undo Log 读写性能, Undo Log 也在内存中进行了缓存,所处的位置在 Buffer Pool 中,如下图所示:
Buffer Pool 中的 Undo Log 页 是 InnoDB 存储引擎中用于缓存 Undo Log 记录的内存区域。
Buffer Pool 是一个重要的内存结构,用于提高数据库的性能,通过在内存中存储频繁访问的数据页和日志记录,减少对磁盘的 I/O 操作。
以下是有关 Buffer Pool 中 Undo Log 页的详细信息:
- Buffer Pool 是 InnoDB 的内存缓存,用于存储数据页和索引页,以便快速访问。它通过减少磁盘 I/O,提高数据库的整体性能。
- 在 Buffer Pool 中,除了数据页外,还可以存储 Undo Log 页,这样可以加速事务的回滚和数据恢复。
buffer pool 中有数据页也有 undo 页,基于wal思想,不仅对buffer pool 中数据页修改操作会记录到redo log buffer,对 undo 页修改操作也会记录到 redo log buffer,这样就通过redo log保证了事务持久性。
当事务Commit之后,undo 页本身就没有利用价值了,此时通过后台线程中的Master Thread或Purge Thread进行 undo 页 的回收工作。
尼恩提示: 以上内容比较复杂,后面会在《尼恩Java面试宝典》配套视频中,进行详细解读。
什么是MVCC和事务的隔离性?
那么,如果多个事务并行的读写操作,每一个事务应该使用那个版本呢?
MVCC 实现了自己 Copy-On-Write思想提升并发能力的时候, 也需要数据的副本,这里既然undo-log 有了那么多副本,MVCC 就借鸡生蛋, 复用 这些数据副本。
所以,undo log 中的副本,可以用于实现多版本并发控制(MVCC),提升事务的并发性能,同时每一个事务操作自己的副本,实现事务的隔离性。
MVCC
机制主要通过三个组件实现:
隐藏字段
Undo-log
日志ReadView
。
MVCC和事务的隔离性,请参见尼恩团队另外一篇重要文章:
基础知识-- Buffer Pool
InnoDB中的缓存区叫innodb_buffer_pool,当读取数据时,就会先从缓存中查看是否数据的页(page)存在,不存在的话去磁盘上检索,查到后缓存到innodb_buffer_pool中。
同理,插入、修改、删除也是先操作缓存里数据,之后再以一定频率更新到磁盘上,这个刷盘机制叫做Checkpoint。
InnoDB中的数据主要有数据页、索引页、插入缓存、自适应哈希索引、锁信息和数据字典信息。尼恩提示,RedoLog不在缓存区中,有个专门的 redo log_buffer 重做日志缓冲池。
Buffer Pool是很经典的缓存池,其中又以Page为单位
- 空闲页
- 干净页
- 脏页【类似操作系统中,修改位为1】
下图是mysql官网原图,其展示了Buffer Pool在innodb引擎架构的组成
Buffer Pool(缓冲池)是MySQL数据库中InnoDB存储引擎的一个重要组成部分,它位于内存中,用于缓存数据库中经常被访问的数据页和索引页,以减少对磁盘的I/O操作,从而提高数据库的读写性能。
Buffer Pool的工作原理基于“时间局部性”和“空间局部性”原则,即最近访问过的数据在未来很可能再次被访问,且一个数据项被访问时,与其相邻的数据项也很可能被访问。
Buffer Pool通过缓存热点数据和索引,减少了磁盘I/O操作,大大提高了数据库的性能。
在Buffer Pool中,数据的加载和淘汰机制是维护和优化其性能的关键。
当数据库服务器接收到一个查询请求时,它首先会在Buffer Pool中查找对应的数据页。
-
如果数据页已经在内存中,服务器可以直接返回数据,而不需要等待磁盘IO操作。
-
如果数据页不在内存中,服务器会从磁盘读取数据,并将其存储在Buffer Pool中,以便后续查询可以直接从内存中获取数据。
所以,Buffer Pool的大小对数据库性能有很大的影响。
-
一个大的Buffer Pool可以缓存更多的数据页,从而减少对磁盘的IO操作,提高查询性能。
-
但是过大的Buffer Pool可能会占用太多的服务器内存资源导致其他服务器的性能下降。
因此需要找到一个平衡点根据服务器的整体性能和数据库的特性来合理配置Buffer Pool的大小。
Buffer Pool中的核心组件包括索引页、数据页、Undo页等。
-
索引页存储了InnoDB表的索引结构,
-
数据页存储了InnoDB表的实际数据行,
-
Undo页存储了旧版本的数据,用于支持事务的ACID属性中的原子性、隔离性(Isolation)。
为了更好地管理Buffer Pool中的缓存页,InnoDB使用了三个重要的链表:LRU链表、free链表和flush链表。
-
LRU链表是Buffer Pool中最主要的链表,用于管理缓存页的访问顺序和淘汰策略。
-
free链表管理未被使用的空闲页,
-
flush链表管理需要被刷新到磁盘的脏页。
在配置Buffer Pool时,核心的参数如下:
-
可以通过调整
innodb_buffer_pool_size
参数来配置Buffer Pool的大小。 -
此外,
innodb_buffer_pool_instances
参数用于将InnoDB Buffer Pool分割成多个实例,每个实例拥有自己的LRU算法和hash索引,这有助于提高并发性能。 -
innodb_buffer_pool_chunk_size
参数用于设置每个Buffer Pool实例的块大小,这有助于减少内存碎片。
监控Buffer Pool的使用情况对于性能调优至关重要。
可以使用SHOW STATUS
命令查看Buffer Pool的使用情况,如Innodb_buffer_pool_read_requests
表示从Buffer Pool中读取的次数,Innodb_buffer_pool_reads
表示直接从磁盘读取的次数等。
通过这些监控信息,可以了解Buffer Pool的使用情况,从而进行针对性的优化。
总结来说,Buffer Pool是MySQL中一个重要的组件,通过合理地加载和淘汰数据页,Buffer Pool能够有效地减少磁盘IO操作,提高查询效率。
同时,监控和维护Buffer Pool的运行情况也是优化数据库性能的关键步骤。
尼恩提示: 以上内容比较复杂,后面会在《尼恩Java面试宝典》配套视频中,进行详细解读。
3. redo log 实现 ACID 中的 持久性
redo log 的意思是 重做日志,
redo log 是物理日志,记录内容是“在某个数据页上做了什么修改”,属于 InnoDB 存储引擎。
什么是 redo log ?
redo log 保证了事务的持久性,当我们对缓冲池中的数据页进行了修改(修改后变成脏页),但是脏页数据是存在于Buffer Pool缓冲池,缓冲池占用的就是操作系统内存空间,所以数据页本质也是存在内存中的,内存有个特点就是断电即失。
所以当脏页数据还没有刷入磁盘,此时数据库服务发生宕机,那么脏页数据就会因为宕机而丢失,如何恢复这些没刷盘得脏页数据呢?
这时候redo log就派上了用场.
以一个更新事务为例, redo log 流转过程,如下图所示:
第 1 步:先将原始数据从磁盘中读入buffer pool 内存中来,修改数据的内存拷贝,数据变成脏页
第 2 步:生成一条重做日志并写入 redo log buffer ,记录的是数据被修改后的值
第 3 步:当事务 commit 时,将 redo log buffer 中的内容刷新到 redo log file ,对 redo log file 采用追加写的方式
第 4 步:定期将内存中修改的数据刷新到磁盘中
redo log通过WAL(Write-Ahead Logging)来进行故障恢复(crash-safe),所谓WAL简单来说就是先写日志,后写磁盘。
当缓存页被修改后(变成脏页),我们就将本次操作写入到redo log buffer中,
当事务Commit时,就先将redo log buffer中记录通过后台线程刷到磁盘中(事务提交是redo log默认刷盘时机),
此时脏页还没有刷入磁盘,但是,只要redo log成功刷盘,就可以认为本次的修改操作完成了,
因为就算发生了故障导致脏页数据丢失,也可以通过磁盘redo log恢复,
所以,redo log 和 undo log 配合起来的作用就是:
- 事务提交前崩溃,通过 undo log 回滚事务
- 事务提交后崩溃,通过 redo log 恢复事务
尼恩提示: 以上内容比较复杂,后面会在《尼恩Java面试宝典》配套视频中,进行详细解读。
redo log buffer的进行刷盘
redo log记录先写入到redo log buffer中,什么时候刷盘?
也就是说, 最后还是从 redo log buffer 同步到硬盘中,那么redo log buffer何时进行刷盘操作呢?
主要是以下几种情况,默认情况下,redo log在事务提交时,就会进行一次redo log刷盘:
- Master Thread每秒刷盘一次
- redo log buffer 剩余空间 < 1/2, 刷盘一次
- 通过innodb_flush_log_at_trx_commit参数控制
- 0:有事务提交的情况下,每秒刷盘一次- 1:每次提交事务,刷盘一次(默认值,性能差)
- 2:每次提交事务,把日志记录放到OS内核缓冲区,刷盘时机交给OS(性能好,低可靠)
再啰嗦一下 这个核心的innodb_flush_log_at_trx_commit 参数,这个超参数有3个值,支持三种策略:
- 0 :设置为 0 的时候,表示每次事务提交时不进行刷盘操作。有事务提交的情况下,每秒刷盘一次。(性能好,低可靠)
- 1 :设置为 1 的时候,表示每次事务提交时都将进行刷盘操作(默认值)。(高可靠, 低性能)
- 2 :设置为 2 的时候,表示每次事务提交时都只把 redo log buffer 内容写入 page cache,刷盘时机交给OS
也就是说, 一个没有提交事务的redo log 记录, 也可能会刷盘.
因为在事务执行过程 redo log 记录是会写入redo log buffer 中, 这些redo log 记录会被后台线程刷盘
不同刷盘策略的流程演示
innodb_flush_log_at_trx_commit=1 流程图
设置为 1 的时候,表示每次事务提交时都将进行刷盘操作(默认值),所以默认是高可靠, 低性能
innodb_flush_log_at_trx_commit=1(默认值):
- 含义:每当一个事务提交时,InnoDB 会将日志缓冲区的内容写入文件系统缓存,并立即将该日志从文件系统缓存刷新到磁盘(
fsync
)。 - 持久性:提供最强的持久性,保证每次事务提交后日志都已经持久化到磁盘。如果数据库崩溃,最多丢失未提交的事务。
- 性能:性能最差,因为每次事务提交都会执行磁盘写操作,尤其在高并发写入的场景下,可能会成为性能瓶颈。
注意,这个不仅仅写入page cache,而是写入磁盘的 redo.file
所以,为1时, 只要事务提交成功, redo log 记录就一定在硬盘里, 不会有任何数据丢失。数据绝对不会丢失, 但是效率最差的
如果事务执行期间MySQL 挂了或宕机, 这部分日志刷盘了, 但是事务并没有提交, 可以使用redo log 进行重做,所以日志丢了也不会有损失. 可以保证ACID的D(持久性),
尼恩提示:默认的 innodb_flush_log_at_trx_commit=1 时,数据绝对不会丢失, 但是性能是最差的
innodb_flush_log_at_trx_commit=2 流程图
设置为 2 的时候,表示每次事务提交时都只把 redo log buffer 内容写入 page cache,刷盘时机交给OS(性能好,低可靠)
innodb_flush_log_at_trx_commit=2:
- 含义:在每次事务提交时,InnoDB 会将日志缓冲区的内容写入操作系统的文件系统缓存page cache(不执行
fsync
),由操作系统负责将日志写入磁盘。 - 持久性:如果数据库崩溃,而如果服务器没有崩溃,数据不会丢失。 如果服务器也崩溃,page cache 默认是缓存5s的数据,最多丢失最近5 秒内的事务数据,但与
innodb_flush_log_at_trx_commit=0
不同的是,日志至少会被写入文件系统缓存,这在某些情况下可以提供更好的安全性。 - 性能:性能较高,因为事务提交时只需要将日志写入内存中的文件系统缓存,而不需要立即进行磁盘 I/O。
为2时, 只要事务提交成功, redo log buffer 中的内容只写入文件系统缓存(page cache)
如果仅仅只是MySQL挂了不会有任何数据丢失, 但是操作系统宕机可能会有5秒数据的丢失, 这种情况下无法满足ACID中的D.
但数值2 肯定是效率更高的
Page Cache 刷盘的默认周期
在操作系统中,Page Cache 是用于缓存文件系统数据的内存区域,用来减少频繁的磁盘 I/O 操作。当数据写入文件时,实际上首先被写入到 Page Cache,而不立即写入到磁盘。
刷盘(即将 Page Cache 中的数据写入磁盘)的时机受多种因素影响,操作系统会定期将 Page Cache 中的数据刷新到磁盘。
Page Cache 刷盘周期由操作系统的默认行为和配置参数控制,一般来说:
-
操作系统定期启动后台线程(如 Linux 的
pdflush
或flush
线程)将脏页(dirty pages,即修改过但尚未刷到磁盘的页)从 Page Cache 刷写到磁盘。 -
默认情况下,Linux 系统大约每 5 秒启动一次刷盘操作。该周期由内核参数 dirty_writeback_centisecs 控制,默认值为 500,即 5 秒。
innodb_flush_log_at_trx_commit=0 流程图
设置为 0 的时候,表示每次事务提交时不进行刷盘操作。
有事务提交的情况下,每秒刷盘一次
innodb_flush_log_at_trx_commit=0:
- 含义:InnoDB 每秒将日志缓存(Redo Log Buffer)刷新到磁盘,但不会在每次事务提交时刷新。即使事务提交,也只是将日志写到内存中的日志缓冲区,1 秒后由后台线程将其写入磁盘。
- 持久性:如果数据库崩溃,可能会丢失最近 1 秒内的事务数据。
- 性能:性能最高,因为写磁盘的频率最小,但持久性最弱。
如何选择 redo log 参数刷盘?
innodb_flush_log_at_trx_commit 设置为不同值时,分别是什么时候将 redo log 写入磁盘?
参数0:
事务提交时不刷盘。靠后台线程每秒刷盘一次。
后台线程把缓存在 redo log buffer 中的 redo log ,通过调用 write() 写到操作系统的 Page Cache,然后调用 fsync() 持久化到磁盘。
所以参数为 0 的策略,MySQL 进程的崩溃会导致上一秒钟所有事务数据的丢失;
参数1:
只要事务提交成功,redo log记录就一定在硬盘里,不会有任何数据丢失。
如果事务执行期间MySQL挂了或宕机,日志丢了,但是事务也不可能成功提交,所以日志丢了也不会有损失!!
参数2:
事务提交成功,redo log buffer中的内容都会写入文件系统缓存(page cache),此时没有刷盘。
操作系统负责把 Page Cache里的 redo log 持久化到磁盘。
如果仅仅只是MySQL挂了,但是服务器没挂,不会有任何数据丢失。
只有服务器崩溃的情况下,上5秒钟所有事务数据才可能丢失。
- 数据安全性:参数 1 > 参数 2 > 参数 0
- 写入性能: 参数 0 > 参数 2> 参数 1
参数 1 最安全,在一些对数据安全性要求比较高的场景中,显然 innodb_flush_log_at_trx_commit 参数需要设置为 1。
在一些可以容忍数据库崩溃时丢失 1s 数据的场景中,我们可以将该值设置为 0,这样可以明显地减少日志同步到磁盘的 I/O 操作。
安全性和性能折中的方案就是参数 2,虽然参数 2 没有参数 0 的性能高,但是数据安全性方面比参数 0 强,因为参数 2 只要操作系统不宕机,即使数据库崩溃了,也不会丢失数据,同时性能方便比参数 1 高。
redolog日志文件的写入机制:循环写入
redo log 采用大小固定,循环写入的方式,当写满后,会重新从头开始循环写,类似一个环状。
这样设计原因是 redo log 记录的是数据页上的修改,如果 Buffer Pool 中数据页已经刷到磁盘,这些记录就失效了,新日志会将这些失效的记录覆盖擦除。
硬盘上存储的 redo log 日志文件不止一个,而是以一个日志文件组的形式出现的,每个的redo日志文件大小都是一样的
比如可以配置为一组4个文件,每个文件的大小是 1GB,整个 redo log 日志文件组可以记录4G的内容。
它采用的是环形数组形式,从头开始写,写到末尾又回到头循环写,如下图所示。
按照圆环顺时针顺序写入文件,这种写入磁盘的方式也叫循环写。
除了写入,我们还需要擦除。redolog刷盘到磁盘后,就可以进行擦除了。
因此我们用两个指针来表示:
- write pos:当前记录写到的位置,或者说 当前redo log文件写到了哪个位置
- checkpoint:当前要擦除的位置,或者说 目前redo log文件哪些记录可以被覆盖
上图中,黄色是已经写入完成了的,绿色是空闲部分。
write pos 和 checkpoint 两个指针中间绿色部分,表示还剩余多少可写入空间,也就是redo log 文件的空闲/可用空间。
-
当数据页进行刷盘操作(CheckPoint)时,check point指针也会顺时针进行覆盖(黄色变成绿色);
-
当write pos追上了check point就说明 redo log 文件存满了,那就要强制CheckPoint了,将Buffer pool 缓冲池中的脏页刷盘,然后再移动check point指针,这样就可以继续向重做日志组中写入数据了。
write pos 和 checkpoint 两个指针的原理,跟循环队列的设计有些神似,两个指针类似head跟tail一样
这两个指针把整个环形划成了几部分
- write pos - checkpoint:待写入的部分
- checkpoint - write pos:还未刷入磁盘的记录
若write pos追上了checkpoint,说明没有空间写入了。这点,跟我们队列满了是一样的情况。
如果没有空间写入,这时候不能再写入新的 redo log 记录,MySQL 得停下来,清空一些记录,把 checkpoint 推进一下。
注意:redo log 满了,在擦除之前,要确保这些要被擦除记录都已经刷到磁盘中了。在擦除旧记录释放新空间期间,不能再接收新的更新请求,此时 MySQL 性能会下降。因此高并发情况下,合理调整 redo log 大小很重要。
尼恩提示: 以上内容比较复杂,后面会在《尼恩Java面试宝典》配套视频中,进行详细解读。
通过redo log实现crash-safe 能力
Crash-safe 是指数据库系统在发生崩溃(例如服务器宕机、硬件故障、操作系统崩溃等)后,能够通过适当的恢复机制,确保数据的一致性和持久性,并保证已提交的事务不会丢失,未提交的事务会被回滚。这种机制的核心是事务的原子性、一致性、隔离性和持久性(ACID)特性,尤其是持久性。
Innodb 引擎有 crash-safe 能力,即事务提交过程中任何阶段,MySQL 宕机重启后都能保证事务的完整性,已提交的数据不会丢失。
这种能力是通过redo log保证的,MySQL 宕机重启,系统将自动检查 redo log,将修改还未写入磁盘的数据从 redo log 恢复到 MySQL 中。
发生崩溃 ,如何通过redo log实现数据的恢复?
后面尼恩会写一篇文章进行详细的介绍,具体请参见 尼恩的技术自由圈 公众号。
4. bin log 实现 ACID 中的 一致性
什么是binlog ?
binlog是binary log的缩写,即二进制日志。
binlog 是作为mysql操作记录归档的日志,这个日志记录了所有对数据库的数据、表结构、索引等等变更的操作。
也就是说只要是对数据库有变更的操作都会记录到binlog里面来, 可以把数据库的数据当成我们银行账户里的余额,而binlog就相当于我们银行卡的流水。
账户余额只是一个结果,至于这个结果怎么来的,那就必须得看流水了。
而同样在mysql里我们就是通过binlog来归档、验证、恢复、同步数据。
bin log该日志主要有两个功能:
- 备份恢复 ,实现崩溃一致性(Crash Consistency)
- 主从复制 ,实现 主从复制的一致性
每次事务进行提交时,都会将增、删、改操作以追加的方式记录到bin log文件中。
为什么该日志具备备份恢复功能,根据bin log日志中记录的二进制操作记录恢复即可;
主从复制常用于MySQL主从集群搭建,MySQL从节点通过监听主节点bin log日志进行同步即可。
事务提交,以两阶段提交 为例:
prepare 阶段:将 redo log 对应的事务状态设置为 prepare,然后将 redo log 刷新到硬盘;
commit 阶段:将 binlog 刷新到磁盘,接着调用引擎的提交事务接口,将 redo log 状态设置为 commit(将事务设置为 commit 状态后,刷入到磁盘 redo log 文件);
老架构师尼恩提示:原始的两阶段提交 的性能比较低,更高性能的提交方式是 组提交,说白了就是批量提交。但是原始的两阶段提交比较好理解,为了方便说明,这里不说组提交的过程,只说两阶段提交.
bin log的二进制日志格式
binlog应该说是Mysql里最核心的日志, 它记录了除了查询语句(select、show)之外的所有的 DDL 和 DML 语句,也就意味着我们基本上所有对数据库的操作变更都会记录到binlog里面。
实际上,binlog以事件形式记录,不仅记录了操作的语句,同时还记录了语句所执行的消耗的时间。
关于bin log的二进制日志格式,有以下三种类型:
- STATEMENT:增删改SQL语句,存储空间要求小
- ROW:记录表行的更改情况,存储空间要求大
- MIXED:混合场景,默认情况下STATEMENT,少数情况下ROW
msql 5.6默认 statement 类型的bin log,语句模式原封不动的记录当前DML,可读性高,用户能看懂。
msql 5.7默认 ROW类型的bin log,记录数据行的变化(用户看不懂,需要工具分析)。
statement 与ROW 模式的对比
- STATEMENT:可读性较高,日志量少,但是不够严谨
- ROW :可读性很低,日志量大,足够严谨
binlog 刷盘时机
事务执行过程中,先把日志写到 binlog cache(Server 层的 cache),事务提交的时候,再把 binlog cache 写到 binlog 文件中。
因为一个事务的binlog不能被拆开,无论这个事务多大,也要确保一次性写入,所以系统会给每个线程分配一个块内存作为binlog cache。
尼恩提示:
参数 binlog_cache_size 用于控制单个线程内 binlog cache 所占内存的大小。
如果超过了这个参数规定的大小,就要暂存到磁盘(Swap)。
什么时候 binlog cache 会写到 binlog 文件?
在事务提交的时候,执行器把 binlog cache 里的完整事务写入到 binlog 文件中,并清空 binlog cache。
如下图:
虽然每个线程有自己 binlog cache,但是最终都写到同一个 binlog 文件:
- 图中的 write,指的就是指把日志写入到 page cache ,但是并没有把数据持久化到磁盘,因为数据还缓存在文件系统的 page cache 里,write 的写入速度还是比较快的,因为不涉及磁盘 I/O。
- 图中的 fsync,才是将数据持久化到磁盘的操作,这里就会涉及磁盘 I/O,所以频繁的 fsync 会导致磁盘的 I/O 升高。
MySQL提供一个 sync_binlog 参数来控制数据库的 binlog 刷到磁盘上的频率:
- sync_binlog = 0 的时候,表示每次提交事务都只 write,不 fsync,后续交由操作系统决定何时将数据持久化到磁盘;
- sync_binlog = 1 的时候,表示每次提交事务都会 write,然后马上执行 fsync;
- sync_binlog =N(N>1) 的时候,表示每次提交事务都 write,但累积 N 个事务后才 fsync。
在MySQL中系统默认的设置是 sync_binlog = 0,也就是不做任何强制性的磁盘刷新指令,这时候的性能是最好的,但是风险也是最大的。
因为一旦主机【指的是操作系统】发生异常重启,还没持久化到磁盘的数据就会丢失。
而当 sync_binlog 设置为 1 的时候,是最安全但是性能损耗最大的设置。
因为当设置为 1 的时候,即使主机发生异常重启,最多丢失一个事务的 binlog,而已经持久化到磁盘的数据就不会有影响,不过就是对写入性能影响太大。
如果能容许少量事务的 binlog 日志丢失的风险,为了提高写入的性能,一般会 sync_binlog 设置为 100~1000 中的某个数值。
在MySQL中,binlog(二进制日志)是在事务的提交阶段被刷入磁盘的。当一个事务执行并准备提交时,MySQL先生成binlog日志,这些日志记录了事务所做的所有修改。
注意:在事务的两阶段提交过程中,首先会将binlog日志写入到磁盘,然后才提交事务,确保了数据的一致性和持久性。
bin log属于MySQL体系架构的Server层,事务操作进行过程中,会把日志信息先记录到bin log cache中,等到事务提交后会将bin log cache中记录刷盘到bin log 文件中。
这里需要注意,无论一个事务中包含了多少个增删改操作,都要一次性写入,不可拆分。
总的来说,binlog在事务的提交阶段被刷入磁盘,这是MySQL保证数据一致性和支持数据恢复的重要机制。通过合理配置sync_binlog
参数,可以在数据安全性和性能之间取得平衡。
bin log 与 两阶段提交
当事务进行Commit操作时,redo log 和 bin log都会被刷盘持久化保存,但是可能会出现以下两种情况:
- bin log刷盘后,redo log还未来得及刷盘,数据库宕机,数据不一致。
- redo log刷盘后,bin log还未来得及刷盘,数据库宕机,数据不一致。
说到这里大概知道两阶段提交其实就是为了防止这两个日志不一致,它将事务Commit操作分为两个阶段:
双1 配置下的 Prepare、Commit 两阶段提交:
- Prepare:XID(内部 XA 事务的 ID) 写入到 redo log,同时将 redo log 对应的事务状态设置为 prepare,然后将 redo log 持久化到磁盘(默认redo log刷盘策略);
- Commit:XID 写入到 bin log,马上将 bin log 刷盘(sync_binlog = 1),接着调用引擎的提交事务接口,将 redo log 状态设置为 commit,只要 bin log 写磁盘成功,就算 redo log 的状态还是 prepare 也没有关系,一样会被认为事务已经执行成功。
双1 配置:
- sync_binlog = 1 表示每次提交事务都会 write binlog,然后马上执行 fsync;
- innodb_flush_log_at_trx_commit=1(默认值):设置为 1 的时候,表示每次事务提交时都将进行redo log刷盘操作(默认值),所以默认是高可靠, 低性能
尼恩提示: 以上内容比较复杂,后面会在《尼恩Java面试宝典》配套视频中,进行详细解读。
这个两阶段提交机制保证了 binlog 和 redo log 的协调一致。基本上会出现下面是几种情况:
- 当事务在prepare阶段crash,数据库recovery的时候,该事务未写入binlog log并且存储引擎未提交,将该事务rollback。
- 当事务在Commit 阶段 写 binlog阶段crash,此时日志还没有成功写入到磁盘中,启动时会rollback此事务。
- 当事务在Commit 阶段 写 binlog阶段 并且已经fsync()到磁盘后crash,但是InnoDB没有来得及commit,此时MySQL数据库recovery的时候将会读出binlog日志的Xid_log_event,然后告诉InnoDB提交这些XID的事务,InnoDB提交完这些事务后会回滚其它的事务,使存储引擎和二进制日志始终保持一致。
总结起来说就是如果一个事务在prepare阶段中落盘成功,并在MySQL Server层中的binlog也写入成功,那这个事务必定commit成功。
通过两阶段提交协议,MySQL 在事务提交过程中可以确保在发生崩溃时:
- 事务的一致性:当事务提交后,binlog 和 redo log 保持一致。如果事务提交成功,则 binlog 和 redo log 都会反映出相同的操作;如果失败,则事务会被回滚,binlog 也不会记录到这个事务。
- 数据恢复:当 MySQL 恢复时,系统可以根据 redo log 和 binlog 的状态,确保事务的一致性,避免部分提交或丢失。
binlog的日志结构
binlog日志文件结构
binlog日志的结构大概是长这样的,它由索引文件和binlog文件组成.
日志文件是以.NNNNNN结尾的。索引文件以.index结尾。
所有的文件有个相同的前缀。默认的binlog文件的前缀名是“HOSTNAME-bin”。
...
HOSTNAME-bin.0000101
HOSTNAME-bin.0000102
HOSTNAME-bin.0000103
...
HOSTNAME-bin.index
...
其中索引文件是一个文本文件,列出了当前的binlog文件。
所以,重点介绍binlog文件。
其中binlog事件又包含通用头、提交头和事件体3个部分组成。
binlog的三个主要作用包括将随机io转换为顺序io以进行持久化、实现主从复制和支持数据恢复。
Binlog日志由一个索引文件与很多日志文件组成,每个日志文件由魔数以及事件组成,每个日志文件都会以一个Rotate类型的事件结束。
如图所示:日志轮换事件(rotate)作为文件尾。
日志轮换事件则包含下一个binlog的文件名以及开始读取的位置,它由服务器写完binlog后添加到文件尾,轮换事件并不会每次都存在,格式如下。
if binlog-version > 1 {
8 position
}
string[p] name of the next binlog
对于每个事件,都可以分为事件头与事件体两部分:
事件头的结构如下所示:
事件体的结构包括固定大小与可变大小两部分。
binlog的主要作用是记录数据库中表的更改,它只记录改变数据的sql,不改变数据的sql不会写入,比如select语句一般不会被记录,因为他们不会对数据产生任何改动。
用一个实际的场景看下binlog产生的过程,准备sql:
create table test(text varchar(20));
insert into test values ('test_text');
select * from test;
flush logs;
查看binlog
show binlog events in 'binlog.000029';
显示的结果如下:
binlog事件包含若干个事务组成的组(group),每个组对应一个事务,每个组要么全部执行,要么都不执行。
如果你不知道binlog有哪些,可以用命令:
show binary logs; #查看binlog列表
show master status; #查看最新的binlog
Pos代表文件开始的位置。
Event_type代表事件的类型。
Server_id是创建事件的服务器ID。
End_log_pos代表事件在文件中的结束位置,以上面为例,第一次查询的结束位置是723,第二次insert之后文件的开始位置就是从723开始。
Info代表事件信息,是一段可读的文本内容。
中继日志 relay-log与主从复制一致性
主从复制的一致性 是 MySQL 等数据库系统在主从复制架构中的核心概念。
Relay Log 的作用是实现主从复制的一致性,对于确保高可用性和数据同步至关重要。
在 MySQL 主从复制中,从库(slave)会从主库(master)读取其执行的二进制日志(Binlog),并将这些日志写入自己的中继日志(Relay Log)。
Relay Log 是从库用于保存从主库传输过来的数据变更操作的日志。
mysql主从复制流程
mysql主从复制流程的过程大概是这个样子:
- master 写入binlog:master 服务器会将 SQL 记录通过多 dump 线程写入到 binary log 中。
- 连接master: slave 服务器连接master。开启一个 io thread 线程向服务器发送请求,向 master 服务器请求 binary log。
- 推送binlog:master 服务器在接收到请求之后,根据偏移量将新的 binary log 发送给 slave 服务器。
- 写入中继日志:slave 服务器收到新的 binary log 之后,写入到自身的 relay log 中,这就是所谓的中继日志。
- 执行中继日志:slave 服务器,单独开启一个 sql thread 读取 relay log 之后,写入到自身数据中。
中继日志 relay-log 的文件结构
relay-log中继日志是连接master和slave的核心,我们来深入了解一下它的结构和使用。
relay 日志文件的命名类似,只不过文件的前缀是“HOSTNAME-relay”。
HOSTNAME-relay.0000101
HOSTNAME-relay.0000102
HOSTNAME-relay.0000103
...
HOSTNAME-relay.index
relay-log的结构和binlog非常相似,只不过他多了一个master.info和relay-log.info的文件。
master.info记录了上一次读取到master同步过来的binlog的位置,以及连接master和启动复制必须的所有信息。
relay-log.info记录了文件复制的进度,下一个事件从什么位置开始,由sql线程负责更新。
中继日志 relay-log 的回放流程
知道binlog和relay-log的结构之后,我们重新梳理一下整个链路的流程,这里我们假定master.info和relay-log.info都是存在的情况:
- Master收到客户端请求语句,在语句结束之前向二进制日志写入一条记录,可能包含多个事件。
- 此时,一个Slave连接到Master,Master的dump线程从binlog读取日志并发送到Slave的IO线程。
- IO线程从master.info读取到上一次写入的最后的位置。
- IO线程写入日志到relay-log中继日志,如果超过指定的relay-log大小,写入轮换事件,创建一个新的relay-log。
- 更新master.info的最后位置
- SQL线程从relay-log.info读取进上一次读取的位置
- SQL线程读取日志事件
- 在数据库中执行sql
- 更新relay-log.info的最后位置
- Slave记录自己的binlog日志
但是在这里IO和SQL线程有会产生重复事件的问题,举一个场景:
- 先记录中继日志,然后更新master.info位置
- 此时服务器崩溃,写入master.info失败
- 服务器恢复,再次同步从master.info获取到的是上一次的位置,会导致事件重复执行
既然会有这个问题还为什么要这样做呢?假设反过来,先更新master.info再记录中继日志,这样带来的问题就是丢失数据了。而mysql认为丢失比重复更严重,所以要先刷新日志,保大还是保小mysql帮你做了决定。
Binlog 与两大数据一致性
1 崩溃一致性(Crash Consistency)
Binlog 记录事务提交的 SQL 操作,通常与 Redo Log 协同工作,实现崩溃一致性(Crash Consistency)。
MySQL 的 InnoDB 存储引擎 和 MySQL Server 层的 binlog 通过两阶段提交来保证事务的一致性,确保在系统崩溃或主从复制时,事务的逻辑和物理数据保持一致。
具体流程如下:
- 第一阶段(Prepare 阶段):
- 当一个事务执行完所有操作并准备提交时,InnoDB 首先会将修改操作写入到 redo log,并将其标记为 prepare 状态。这意味着事务的物理修改操作已经记录到了 redo log 中,但事务尚未真正提交。
- 此时,如果系统崩溃,事务可以通过 undo log 回滚,保持一致性。
- 第二阶段(Commit 阶段):
- 写入 binlog:将 binlog 刷新到磁盘。这时,binlog 确保了主从库在复制时具有相同的操作逻辑。
- 提交 redo log:InnoDB 会将 redo log 的状态修改为 commit,表示事务已经正式提交。
这个两阶段提交机制保证了 binlog 和 redo log 的协调一致。基本上会出现下面是几种情况:
- 当事务在prepare阶段crash,数据库recovery的时候,该事务未写入binlog log并且存储引擎未提交,将该事务rollback。
- 当事务在Commit 阶段 写 binlog阶段crash,此时日志还没有成功写入到磁盘中,启动时会rollback此事务。
- 当事务在Commit 阶段 写 binlog阶段 并且已经fsync()到磁盘后crash,但是InnoDB没有来得及commit,此时MySQL数据库recovery的时候将会读出binlog日志的Xid_log_event,然后告诉InnoDB提交这些XID的事务,InnoDB提交完这些事务后会回滚其它的事务,使存储引擎和二进制日志始终保持一致。
总结起来说就是如果一个事务在prepare阶段中落盘成功,并在MySQL Server层中的binlog也写入成功,那这个事务必定commit成功。
通过两阶段提交协议,MySQL 在事务提交过程中可以确保在发生崩溃时:
- 事务的一致性:当事务提交后,binlog 和 redo log 保持一致。如果事务提交成功,则 binlog 和 redo log 都会反映出相同的操作;如果失败,则事务会被回滚,binlog 也不会记录到这个事务。
- 数据恢复:当 MySQL 恢复时,系统可以根据 redo log 和 binlog 的状态,确保事务的一致性,避免部分提交或丢失。
2 中继日志与主从复制的一致性
在主从复制的场景下,binlog 的一致性尤为重要。
-
主库提交事务时会先写入 binlog
-
从库则通过读取主库的 binlog 重新执行相同的操作。
MySQL 确保:
- 只有当事务在主库提交成功(即 binlog 和 redo log 一致)后,主库才会将 binlog 传递给从库。
- 从库应用 binlog 以确保主库和从库的数据保持一致。
通过这种机制,MySQL 保证了主从复制环境中的一致性,不会出现主库和从库不同步的情况。
崩溃恢复时的判断规则
根据两阶段提交,崩溃恢复时的判断规则是这样的:
- 如果 redo log 里面的事务是完整的,也就是已经有了 commit 标识,则直接提交
- 如果 redo log 里面的事务处于 prepare 状态,则判断对应的事务 binlog 是否存在并完整
- a. 如果 binlog 存在并完整,则提交事务;
- b. 否则,回滚事务。
commit阶段写入 binlog 之前发生了崩溃,事务回滚
假设数据库在写入 redo log(prepare) 阶段之后、写入 binlog 之前,发生了崩溃,这个事务会回滚
为啥?
此时 redo log 里面的事务处于 prepare 状态,binlog 还没写(对应 2b),所以崩溃的时候,这个事务会回滚。
因为 binlog 还没有写入,之后从库进行同步的时候,slave 无法执行这个操作,违背了 主从不一致。
也就是说,如果我们主库上继续恢复这个操作,显然就会导致主从不一致,
所以在主库上需要回滚这个事务。
commit阶段写入 binlog 之后发生了崩溃,事务恢复
假设数据库在commit写入 binlog 之后,redo log 状态修改为 commit 前发生崩溃,发生了崩溃,该事务在崩溃恢复的时候会被正常提交。
为啥?
虽然此时 redo log 里面的事务仍然是 prepare 状态,binlog 存在并完整,所以如果在这个时刻数据库崩溃了,该事务在崩溃恢复的时候会被正常提交。
从主从一致性角度来说,因为 binlog 已经写入成功了,这样之后就会被从库同步过去。
即使 redolog 没有这条cmmit这个记录,所以为了主备一致,在主库上崩溃恢复的时候需要提交这个事务。
所以,其实可以看出来,处于 prepare 阶段的 redo log 加上完整的 bin log,就能保证数据库的崩溃恢复了。
问题是: MySQL 咋知道 bin log 是不是完整的?这是通过 binlog格式保证:
- statement 格式的 bin log,最后会有 COMMIT
- row 格式的 bin log,最后会有 XID event
而对于 bin log 可能会在中间出错的情况,MySQL 5.6.2 版本以后引入了 binlog-checksum
参数,用来验证 bin log 内容的正确性。
undo log、redo log、binlog的定义和对比
发现自己的知识点有点散,今天就把它们连接起来,好好总结一下。
定义和作用 | 所在架构层级 | 日志形式 | 所在文件和默认名称,组织结构 | 是否缓存,如何缓存 | 写文件方式 | |
---|---|---|---|---|---|---|
undo log | 回滚日志,在事务执行的过程中操作任何数据之前先将数据备份到undolog中。事务失败时可根据undo log进行回滚。用来保证事务的一致性。还可以用来实现多版本并发控制MVCC。 | InnoDB引擎层 | 逻辑日志,以行的形式进行记录。 | 共享表空间文件,ibdata0/ibdata1。undo只是该文件的的一部分(rollback segment),文件由页组成,每一页又由一行行的数据组成,undo相对有自己的固定页,会循环覆盖。Rollback segment,一共有128个,分别从resg slot0 - resg slot127,每一个resg slot,也就是每一个回滚段,内部由1024个undo segment组成。 | 是,innodb_buffer_pool缓冲池中有undo页 | 尽量顺序写。 回滚,实际做的是与之前相反的工作,比如INSERT需要的是DELETE,而DELETE需要的是INSERT。执行相反的UPDATE,将修改前的行放回去。回滚操作写入磁盘时为随机写。 |
redo log | 重做日志,在事务执行的过程中不断记录事务操作的变化。恢复提交后的物理数据页(恢复数据页,且只能恢复到最后一次提交的位置)。用来保证事务的原子性和持久性。 | InnoDB引擎层 | 物理日志,记录的是数据页的物理修改。 | 重做日志文件,ib_logfile0/ib_logfile1。有自己的页结构,会循环覆盖。 | 是,专门的独立的redo缓冲区 | 尽量顺序写。先写入日志缓冲中,然后按照一定的条件顺序写入redo日志文件。恢复操作写入磁盘时为随机写。 |
binlog | 二进制日志,在事务提交后进行记录。用来备份, 通过主从复制来实现数据同步和读写分离。 | MySQL数据库(上)层 | 逻辑日志,记录数据库所有增删改操作(sql语句)。 | 二进制日志文件,localhost-bin.000001~localhost-bin.00000n。有自己的文件格式和文件结构,不会循环覆盖,追加写,文件写满再新建。 | 是,每个会话有一个默认32K的缓冲。 | 顺序写。 |
尼恩架构团队的塔尖 sql 面试题
尼恩一文起底: sql查询语句的执行流程:
尼恩一文起底: 什么是回表?什么是 索引下推 ?
尼恩一文起底: mysql 索引失效
美团面试:mysql 索引失效?怎么解决? (重点知识,建议收藏,读10遍+)
尼恩一文起底: MVCC和事务的隔离性?
说在最后:有问题找老架构取经
关于什么是redo log 、undo log、 binlog ? 分别实现了事务ACID的那些特性?
借助此文,尼恩给大家梳理的这一个满分答案,已经彻底出来啦。
后面,尼恩java面试宝典回录成视频, 给大家打造一套进大厂的塔尖视频。
通过这个问题的深度回答,可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提”。
在面试之前,建议大家系统化的刷一波 5000页《尼恩Java面试宝典PDF》,里边有大量的大厂真题、面试难题、架构难题。
很多小伙伴刷完后, 吊打面试官, 大厂横着走。
在刷题过程中,如果有啥问题,大家可以来 找 40岁老架构师尼恩交流。
另外,如果没有面试机会,可以找尼恩来改简历、做帮扶。
遇到职业难题,找老架构取经, 可以省去太多的折腾,省去太多的弯路。
尼恩指导了大量的小伙伴上岸,前段时间,刚指导一个40岁+被裁小伙伴,拿到了一个年薪100W的offer。
狠狠卷,实现 “offer自由” 很容易的, 前段时间一个武汉的跟着尼恩卷了2年的小伙伴, 在极度严寒/痛苦被裁的环境下, offer拿到手软, 实现真正的 “offer自由” 。
技术自由的实现路径:
实现你的 架构自由:
《阿里二面:千万级、亿级数据,如何性能优化? 教科书级 答案来了》
《峰值21WQps、亿级DAU,小游戏《羊了个羊》是怎么架构的?》
… 更多架构文章,正在添加中
实现你的 响应式 自由:
这是老版本 《Flux、Mono、Reactor 实战(史上最全)》
实现你的 spring cloud 自由:
《Spring cloud Alibaba 学习圣经》 PDF
《分库分表 Sharding-JDBC 底层原理、核心实战(史上最全)》
《一文搞定:SpringBoot、SLF4j、Log4j、Logback、Netty之间混乱关系(史上最全)》
实现你的 linux 自由:
实现你的 网络 自由:
《网络三张表:ARP表, MAC表, 路由表,实现你的网络自由!!》
实现你的 分布式锁 自由:
实现你的 王者组件 自由:
《队列之王: Disruptor 原理、架构、源码 一文穿透》
《缓存之王:Caffeine 源码、架构、原理(史上最全,10W字 超级长文)》
《Java Agent 探针、字节码增强 ByteBuddy(史上最全)》