hello,大家好,我是张张,「架构精进之路」公号作者。
在 MySQL 中我们经常会接触到三个核心日志,它们分别是:binlog 、redo log、undo log。
好多同学对于它们可能并不陌生,但是具体区分起来各自的功能用途以及实现原理,那可能认知就会比较模糊了,今天就跟大家一起,来清晰明了的介绍一下这些日志的核心思想和功能原理。
1 binlog
1.1 binlog 设计目标
binlog 记录了对 MySQL 数据库执行更改的所有的写操作,包括所有对数据库的数据、表结构、索引等等变更的操作。
注意:这其中不包含 SELECT、SHOW 等,因为对数据没有修改
只要是对数据库有变更的操作都会记录到 binlog 里面来,我们可以把数据库的数据看做银行账户里的余额,而 binlog 就相当于我们银行卡的流水记录。账户余额只是一个结果,至于这个结果怎么来的,那就必须得看流水了。
在实际应用中, binlog 的主要应用场景分别是 主从复制 和 数据恢复。
- 主从复制 :在 Master 端开启 binlog ,然后将 binlog 发送到各个 Slave 端, Slave 端重放 binlog 来达到主从数据一致。
- 数据恢复 :通过使用 mysqlbinlog 工具来恢复数据。
1.2 binlog 数据格式
binlog 日志有三种格式,分别为 STATMENT 、 ROW 和 MIXED。
在 MySQL 5.7.7 之前,默认的格式是 STATEMENT , MySQL 5.7.7 之后,默认值是 ROW。日志格式通过 binlog-format 指定。
-
ROW:基于行的复制(row-based replication, RBR),不记录每条 SQL 语句的上下文信息,仅需记录哪条数据被修改了。如果一个 update 语句修改一百行数据,那么这种模式下就会记录 100 行对应的记录日志。
优点:不会出现某些特定情况下的存储过程、或 function、或 trigger 的调用和触发无法被正确复制的问题;
缺点:会产生大量的日志,尤其是 alter table 的时候会让日志暴涨。
-
STATMENT:基于 SQL 语句的复制( statement-based replication, SBR ),每一条会修改数据的 SQL 语句会记录到 binlog 中 。相对于 ROW 模式,STATEMENT 模式下只会记录这个 update 的语句,所以此模式下会非常节省日志空间,也避免着大量的 IO 操作。
优点:不需要记录每一行的变化,减少了 binlog 日志量,节约了 IO , 从而提高了性能;
缺点:在某些情况下会导致主从数据不一致,比如执行 sysdate() 、 slepp() 等 。
-
MIXED:基于 STATMENT 和 ROW 两种模式的混合复制(mixed-based replication, MBR),一般的复制使用 STATEMENT 模式保存 binlog ,对于一些函数,STATEMENT 模式无法复制的操作使用 ROW 模式保存 binlog。
基于这三种模式需要注意的是:
1)使用 row 格式的 binlog 时,在进行数据同步或恢复的时候不一致的问题更容易被发现,因为它是基于数据行记录的。
2)使用 mixed 或者 statement 格式的 binlog 时,很多事务操作都是基于 SQL 逻辑记录,我们都知道一个 SQL 在不同的时间点执行它们产生的数据变化和影响是不一样的,所以这种情况下,数据同步或恢复的时候就容易出现不一致的情况。
1.3 binlog 写入策略
对于 InnoDB 存储引擎而言,在进行事务的过程中,首先会把 binlog 写入到 binlog cache 中(因为写入到 cache 中会比较快,一个事务通常会有多个操作,避免每个操作都直接写磁盘导致性能降低),只有在事务提交时才会记录 biglog ,此时记录还在内存中,那么 biglog 是什么时候刷到磁盘中的呢?
MySQL 其实是通过 sync_binlog 参数控制 biglog 的刷盘时机,取值范围是 0-N:
- 0:每次提交事务 binlog 不会马上写入到磁盘,而是先写到 page cache。不去强制要求,由系统自行判断何时写入磁盘,在 Mysql 崩溃的时候会有丢失日志的风险;
- 1:每次提交事务都会执行 fsync 将 binlog 写入到磁盘;
- N:每次提交事务都先写到 page cach,只有等到积累了 N 个事务之后才 fsync 将 binlog 写入到磁盘,在 MySQL 崩溃的时候会有丢失 N 个事务日志的风险。
很显然三种模式下,sync_binlog=1 是强一致的选择,选择 0 或者 N 的情况下在极端情况下就会有丢失日志的风险,具体选择什么模式还是得看系统对于一致性的要求。
2、redo log
2.1 redo log 设计目标
redo log 是属于引擎层(innodb)的日志,称为重做日志 ,当 MySQL 服务器意外崩溃或者宕机后,保证已经提交的事务持久化到磁盘中(持久性)。
它能保证对于已经 COMMIT 的事务产生的数据变更,即使是系统宕机崩溃也可以通过它来进行数据重做,达到数据的持久性,一旦事务成功提交后,不会因为异常、宕机而造成数据错误或丢失。
2.2 redo log 数据格式
redo log 包括两部分:
-
内存中的日志缓冲(redo log buffer)
-
内存层面,默认 16M,通过 innodb_log_buffer_size 参数可修改
-
磁盘上的日志文件(redo logfile)
-
持久化的,磁盘层面
MySQL 每执行一条 DML 语句,先将记录写入 redo log buffer,后续某个时间点再一次性将多个操作记录写到 redo log file。
通常所说的 Write-Ahead Log(预先日志持久化)指的是在持久化一个数据页之前,先将内存中相应的日志页持久化。
在计算机操作系统中,用户空间( user space )下的缓冲区数据一般情况下是无法直接写入磁盘的,中间必须经过操作系统内核空间( kernel space )缓冲区( OS Buffer )。
因此, redo log buffer 写入 redo logfile 实际上是先写入 OS Buffer ,然后再通过系统调用 fsync() 将其刷到 redo log file 中,过程如下:
修改数据的操作流程:
- 先将原始数据从磁盘中读入内存中来,修改数据的内存拷贝,产生脏数据
- 生成一条重做日志并写入 redo log buffer,记录的是数据被修改后的值
- 默认在事务提交后将 redo log buffer 中的内容刷新到 redo log file,对 redo log file 采用追加写的方式
- 定期将内存中修改的数据刷新到磁盘中(这里说的是那些还没及时被后台线程刷盘的脏数据)
2.3 关于 redo log 的几点疑惑
读到这里,相必有同学会有如下疑问:
Q1:为什么不直接修改磁盘中的数据?
因为直接修改磁盘数据的话,它是随机 IO,修改的数据分布在磁盘中不同的位置,需要来回的查找,所以命中率低,消耗大,而且一个小小的修改就不得不将整个页刷新到磁盘,利用率低;
与之相对的是顺序 IO,磁盘的数据分布在磁盘的一块,所以省去了查找的过程,节省寻道时间。
使用后台线程以一定的频率去刷新磁盘可以降低随机 IO 的频率,增加吞吐量,这是使用 buffer pool 的根本原因。
Q2:同为操作数据变更的日志,有了 binlog 为什么还要 redo log?
我认为最核心的一点就是两者记录的数据变更粒度是不一样的。
以修改数据为例,binlog 是以表为记录主体,在 ROW 模式下,binlog 保存的表的每行变更记录。
MySQL 是以页为单位进行刷盘的,每一页的数据单位为 16K,所以在刷盘的过程中需要把数据刷新到磁盘的多个扇区中去。而把 16K 数据刷到磁盘的每个扇区里这个过程是无法保证原子性的,如果数据库宕机,那么就可能会造成一部分数据成功,而一部分数据失败的情况。而通过 binlog 这种级别的日志是无法恢复的,因为一个 update 可能更改了多个磁盘区域的数据,所以这个时候得需要通过 redo log 这种记录到磁盘数据级别的日志进行数据恢复。
由以上两者的对比可知:binlog 日志只用于归档,只依靠 binlog 是没有 crash-safe 能力的。
同样只有 redo log 也不行,因为 redo log 是 InnoDB 特有的,且日志上的记录落盘后会被覆盖掉。因此需要 binlog 和 redo log 二者同时记录,才能保证当数据库发生宕机重启时,数据不会丢失。
Q3:redo log 一定能保证事务的持久性吗?
不一定,这要根据 redo log 的刷盘策略决定,因为 redo log buffer 同样是在内存中,如果提交事务之后,redo log buffer 还没来得及将数据刷新到 redo log file 进行持久化,此时发生宕机照样会丢失数据。
那该如何解决呢?刷盘写入策略。
2.4 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 中的数据刷盘。
3、undo log
3.1 undo log 设计目标
redo log 是也属于引擎层(innodb)的日志,从上面的 redo log 介绍中我们就已经知道了,redo log 和 undo log 的核心是为了保证 innodb 事务机制中的持久性和原子性,事务提交成功由 redo log 保证数据持久性,而事务可以进行回滚从而保证事务操作原子性则是通过 undo log 来保证的。
原子性 是指对数据库的一系列操作,要么全部成功,要么全部失败,不可能出现部分成功的情况。
undo log 的主要应用场景分别:
- 事务回滚 :前面提到过,后台线程会不定时的去刷新 buffer pool 中的数据到磁盘,但是如果该事务执行期间出现各种错误(宕机)或者执行 rollback 语句,那么前面刷进去的操作都是需要回滚的,保证原子性,undo log 就是提供事务回滚的。
- MVCC:当读取的某一行被其他事务锁定时,可以从 undo log 中分析出该行记录以前的数据版本是怎样的,从而让用户能够读取到当前事务操作之前的数据——快照读。
3.2 undo log 数据格式
undo log 数据主要分两类:
- insert undo log
insert 操作的记录,只对事务本身可见,对其他事务不可见(这是事务隔离性的要求),故该 undo log 可以在事务提交后直接删除,不需要进行 purge 操作。
- update undo log
update undo log 记录的是对 delete 和 update 操作产生的 undo log。该 undo log 可能需要提供 MVCC 机制,因此不能在事务提交时就进行删除。提交时放入 undo log 链表,等待 purge 线程进行最后的删除。
在 InnoDB 存储引擎中,undo log 使用 rollback segment 回滚段进行存储,每隔回滚段包含了 1024 个 undo log segment。MySQL5.5 之后,一共有 128 个回滚段。即总共可以记录 128 * 1024 个 undo 操作。
每个事务只会使用一个回滚段,一个回滚段在同一时刻可能会服务于多个事务。
3.3 undo log 操作实例
1、首先准备一张原始原始数据表(user_info)
对于 InnoDB 引擎来说,每个行记录除了记录本身的数据之外,还有几个隐藏的列:
- DB_ROW_ID∶记录的主键 id。
- DB_TRX_ID:事务 ID,当对某条记录发生修改时,就会将这个事务的 Id 记录其中。
- DB_ROLL_PTR︰回滚指针,版本链中的指针。
2、开启一个事务 A
对 user_info 表执行如下 SQL:
update user_info set name =“李四”where id=1
复制代码
将会进行如下流程操作:
1、首先获得一个事务编号 104
2、把 user_info 表修改前的数据拷贝到 undo log
3、修改 user_info 表 id=1 的数据
4、把修改后的数据事务版本号改成 当前事务版本号,并把 DB_ROLL_PTR 地址指向 undo log 数据地址。
3、最后执行结束
结果如下所示:
可以发现每次对数据的变更都会产生一个 undo log,当一条记录被变更多次时,那么就会产生多条 undo log,undo log 记录的是变更前的日志,并且每个 undo log 的序号是递增的,那么当要回滚的时候,按照序号依次向前推,就可以找到我们的原始数据了。
总结
binlog 是 MySQL server 层的日志,而 redo log 和 undo log 都是引擎层(InnoDB)的日志,要换其他数据引擎那么就未必有 redo log 和 undo log 了。
它的设计目标是支持 innodb 的“事务”的特性,事务 ACID 特性分别是原子性、一致性、隔离性、持久性, 一致性是事务的最终追求的目标,隔离性、原子性、持久性是达成一致性目标的手段,根据的之前的介绍我们已经知道隔离性是通过锁机制来实现的,而事务的原子性和持久性则是通过 redo log 和 undo log 来保障的。
写入策略
事务执行过程中,先把日志写到 bin log cache ,事务提交的时候,再把 binlog cache 写到 binlog 文件中。因为一个事务的 binlog 不能被拆开,无论这个事务多大,也要确保一次性写入,所以系统会给每个线程分配一个块内存作为 binlog cache。
binlog vs redo log
- redo log 物理日志:记录内容是“在 xx 数据页做了 xx 修改”,属于 InnoDB 存储引擎层产生的。
- binlog 逻辑日志:记录内容是语句的原始逻辑,类似于给 ID=2 这一行的 c 字段加 1,属于服务层。
两个侧重点也不同, redo log 让 InnoDB 有了崩溃恢复的能力,binlog 保证了 MySQL 集群架构的数据一致性。
在执行更新语句过程,会记录 redo log 与 binlog 两块日志,以基本的事务为单位,redo log 在事务执行过程中可以不断写入,而 binlog 只有在提交事务时才写入,所以 redo log 与 binlog 的写入时机不一样。
💪🏻 坚持原创分享有价值的干货技术文章!
Thanks for reading!
文章来源: https://xie.infoq.cn/article/d9fc47313dd4cb582b35c7447