mysql日志主要包括错误日志、查询日志、慢查询日志、事务日志、二进制日志几大类。作为开发,我们重点需要关注的是二进制日志( binlog )和事务日志(包括redo log 和 undo log )。
二进制日志(BinLog)是记录MySQL实例数据变更的一个组件,日志中包含了一系列变更数据的操作,例如变更表结构、删除数据、更改/添加数据等DML、DDL。这些操作在binlog中统一称为事件(Events)。Binlog也会记录一些可能会发生数据变更的事件,例如没有找到对应行的Delete操作。但是,BinLog不会记录纯查询语句,例如Select和Show。如果需要排查这些语句的记录,需要查找通用语句日志。
Binlog主要用于两个场景
主从同步:在主从同步的过程中,Binlog用于记录主库的数据变更,然后这些记录被主库内的线程发送至从库。从库的工作线程再把接收到的变更事件放到从库上执行,完成数据同步。主从同步通常被视为提升数据库吞吐能力的一种方法,因此Binlog是必不可少的环节。
数据恢复: 在生产环境中,总是会有意外导致数据丢失。在一些数据恢复的场景中,Binlog是必不可少的。当数据库从备份中恢复的时候,binlog中所记录的信息会在恢复后的数据上执行,补齐备份数据中未备份的记录。
一个完整的binlog由两种文件组成
索引文件(Index File):
索引文件用于跟踪多个binlog文件,便于主库创建新的binlog文件。索引文件中的每一行记录着所有关联和它关联的binlog文件名。
日志文件(Binlog file):
日志文件是binlog的主体,如上图所示,它是由一系列事件(Binary Log Events)组成。
日志文件的开头记录的是的是 Format_description 事件,这个事件记录主库的信息和日志文件的状态。如果主库突然宕机或者重启,主库会重新创建一个日志文件然后在开头写入Format_description。
当主库记录完成变更事件后,主库会写入Rotate事件。Rotate事件会指定下一个日志文件的文件名和读取事件的起始点。
除开上述的Format_description事件和Rotate事件,日志文件都会把其他的变更事件进行分组(Group)。在MySQL中,每一个事务会被分成一组,组中包含了这个事务下执行的所有语句。一些非事务性语句会被单独分成一组,如Create和Alter语句等。
自从MySQL5.1起,日志的格式增加到了三种
Statement-based: 这是MySQL默认的日志格式。在这个格式下,binlog日志中记录的是变更数据库的执行语句和事务。
Row-based: 在这个格式下,binlog日志中记录的是发生变更的数据行。
Mixed-logging: 这个格式是前两种格式的混合版,主库会根据执行的语句来决定binlog日志中记录的内容,可以是具体的行,也可以是执行的语句。
1.主从同步(详细参考之前)
不同的binlog日志格式会影响从库的同步方式。在statement-based格式下,从库是直接执行binlog日志中读取到的语句或者事务。在row-based格式下,从库是根据日志内容直接更新对应的数据。
MySQL默认的日志格式是statement-based。在大多数主从同步的场景下,日志格式采用row-based,最主要的原因是row-based格式下,日志内容都是真实发生变更的数据,从库的数据准确性有很高的保证。
2.数据恢复
我们知道mysql会对数据库进行持久化备份,当数据意外丢失后,备份数据可以拿过来恢复。然而,即使数据做了备份,也不能完全恢复到丢失那一刻的数据。
例如每半个小时备份一次,在备份的前一刻crash了,我们可能会丢失最后半个小时的数据。
这里就用到了MySQL的Point-In-time recovery(简称PITR)功能,PITR是用于恢复某个时间点的失效数据,用于弥补备份时间点到失效时间点这段数据“真空期”,实现这个功能的核心是binlog日志文件。
binlog日志中存放着数据库所有的变更。PITR的原理很简单,就是将binlog内存储的变更数据重新执行一遍。需要恢复数据时,用mysqlbinlog命令执行binlog日志的内容。
(刷新到磁盘的行为由参数sync_binlog决定)
我们都知道,事务的四大特性里面有一个是 持久性 ,具体来说就是
只要事务提交成功,那么对数据库做的修改就被永久保存下来了,不可能因为任何原因再回到原来的状态 。那么 mysql是如何保证一致性的呢?最简单的做法是在每次事务提交的时候,将该事务涉及修改的数据页全部刷新到磁盘中。但是这么做会有严重的性能问题,主要体现在两个方面:
因为 Innodb 是以 页 为单位进行磁盘交互的,而一个事务很可能只修改一个数据页里面的几个字节,这个时候将完整的数据页刷到磁盘的话,太浪费资源了!
一个事务可能涉及修改多个数据页,并且这些数据页在物理上并不连续,使用随机IO写入性能太差!
因此 mysql 设计了 redo log , 具体来说就是只记录事务对数据页做了哪些修改,这样就能完美地解决性能问题了(相对而言文件更小并且是顺序IO)。
redo 日志记录的是每个页面(page)更改物理情况,所以 redo 日志整体来说是比较小的,存储的信息不多,简单的介绍一下存储字段的意思:
redo 日志并非这么简单,它非常的复杂,但是我们不需要对它庖丁解牛,因为它确实对我们来说没啥用,我们只要记住 redo 日志会把事务在执行过程中对数据库所做的所有修改都记录下来,在之后系统崩溃重启后可以把事务所做的任何修改都恢复出来。
WAL(Write-ahead logging,预写式日志)是数据库系统提供原子性和持久化的一系列技术。
在使用WAL的系统中,所有的修改都先被写入到日志中,然后再被应用到系统状态中。
在事务提交时将所有修改过的内存中的页面刷新到磁盘中相比,只将该事务执行过程中产生的 redo 日志刷新到磁盘的好处如下:
redo日志占用的空间非常小:存储表空间ID、页号、偏移量以及需要更新的值所需的存储空间是很小的
redo日志是顺序写入磁盘的:在执行事务的过程中,每执行一条语句,就可能产生若干条redo日志,这些日志是按照产生的顺序写入磁盘的,也就是使用顺序IO。
首先是MySQL体系结构的原因,MySQL是多存储引擎的,不管使用那种存储引擎,都会有binlog,而不一定有redo log,简单的说,binlog是MySQL Server层的,redo log是InnoDB层的。
只使用binlog行不行?
binlog 日志只用于归档,只依靠 binlog 是没有 crash-safe 能力的。
因此innodb引入了redolog
只使用redolog行不行?
redo log是恢复在内存更新后,还没来得及刷到磁盘的数据。
binlog是存储所有数据变更的情况,理论上只要记录在binlog上的数据,都可以恢复。
如不小心整个数据库的数据被删除了,能使用redo log文件恢复数据吗?
不可以使用redo log文件恢复,只能使用binlog文件恢复。因为redo log文件不会存储历史所有的数据的变更,当内存数据刷新到磁盘中,redo log的数据就失效了,也就是redo log文件内容是会被覆盖的。
redo log 是什么?
一个固定大小,“循环写”的日志文件,记录的是物理日志——“在某个数据页上做了某个修改”。
binlog 是什么?
一个无限大小,“追加写”的日志文件,记录的是逻辑日志——“给 ID=2 这一行的 c 字段加1”。
redo log 和 binlog 有一个很大的区别就是,一个是循环写,一个是追加写。也就是说 redo log 只会记录未刷盘的日志,已经刷入磁盘的数据都会从 redo log 这个有限大小的日志文件里删除。binlog 是追加日志,保存的是全量的日志。
当数据库 crash 后,想要恢复未刷盘但已经写入 redo log 和 binlog 的数据到内存时,binlog 是无法恢复的。虽然 binlog 拥有全量的日志,但没有一个标志让 innoDB 判断哪些数据已经刷盘,哪些数据还没有。
举个栗子,binlog 记录了两条日志:
给 ID=2 这一行的 c 字段加1
给 ID=2 这一行的 c 字段加1
在记录1刷盘后,记录2未刷盘时,数据库 crash。重启后,只通过 binlog 数据库无法判断这两条记录哪条已经写入磁盘,哪条没有写入磁盘,不管是两条都恢复至内存,还是都不恢复,对 ID=2 这行数据来说,都不对。
但 redo log 不一样,只要刷入磁盘的数据,都会从 redo log 中抹掉,数据库重启后,直接把 redo log 中的数据都恢复至内存就可以了。这就是为什么 redo log 具有 crash-safe 的能力,而 binlog 不具备。
数据库事务四大特性中有一个是 原子性 ,具体来说就是 原子性是指对数据库的一系列操作,要么全部成功,要么全部失败,不可能出现部分成功的情况。
实际上, 原子性 底层就是通过 undo log 实现的。 undo log 主要记录了数据的逻辑变化,比如一条 INSERT语句,对应一条 DELETE 的 undo log ,对于每个 UPDATE 语句,对应一条相反的 UPDATE 的undo log ,这样在发生错误时,就能回滚到事务之前的数据状态。
同时, undo log 也是 MVCC (多版本并发控制)实现的关键。
binlog 的写入逻辑比较简单:事务执行过程中,先把日志写到 binlog cache,事务提交的时候,再把 binlog cache 写到 binlog 文件中。
一个事务的 binlog 是不能被拆开的,因此不论这个事务多大,也要确保一次性写入。
那么 biglog是什么时候刷到磁盘中的呢? mysql 通过 sync_binlog 参数控制 biglog 的刷盘时机,取值范围是 0-N
sync_binlog | 含义 |
---|---|
0 | 不去强制要求,由系统自行判断何时写入磁盘; |
1 | 每次 commit 的时候都要将 binlog 写入磁盘; |
N | 每N个事务,才会将 binlog 写入磁盘。 |
从上面可以看出, sync_binlog 最安全的是设置是 1 ,这也是 MySQL 5.7.7之后版本的默认值。但是设置一个大一些的值可以提升数据库性能,因此实际情况下也可以将值适当调大,牺牲一定的一致性来获取更好的性能。
redo 日志同样也要先写入到缓存区,我们把这个缓冲区叫做 redo日志缓冲区。在服务器启动时就向操作系统申请了一大片称之为 redo log buffer 的连续内存空间。
如果我们想要提交一个事务了,此时就会根据一定的策略把 redo 日志从 redo log buffer 里刷入到磁盘文件里去。此时这个策略是通过 innodb_flush_log_at_trx_commit 来配置的,他有几个选项。
innodb_flush_log_at_trx_commit | 含义 |
---|---|
0 | 提交事务的时候,不立即把 redo log buffer 里的数据刷入磁盘文件的,而是依靠 InnoDB 的主线程每秒执行一次刷新到磁盘。此时可能你提交事务了,结果 mysql 宕机了,然后此时内存里的数据全部丢失。 |
1 | 提交事务的时候,就必须把 redo log 从内存刷入到磁盘文件里去,只要事务提交成功,那么 redo log 就必然在磁盘里了。注意,因为操作系统的“延迟写”特性,此时的刷入只是写到了操作系统的缓冲区中,因此执行同步操作才能保证一定持久化到了硬盘中。 |
2 | 提交事务的时候,把 redo 日志写入磁盘文件对应的 os cache 缓存里去,而不是直接进入磁盘文件,可能 1 秒后才会把 os cache 里的数据写入到磁盘文件里去。 |
可以看到,只有1才能真正地保证事务的持久性,但是由于刷新操作 fsync() 是阻塞的,直到完成后才返回,我们知道写磁盘的速度是很慢的,因此 MySQL 的性能会明显地下降。如果不在乎事务丢失,0和2能获得更高的性能。
其实undolog和redolog的写入时机是十分相似的,但是我们可以理解为undolog每次都会落盘,redolog会先写入redolog_buffer,在事务提交后落盘
其实undolog也会生成一个redolog,用于保证undolog的持久性,这里就不谈了
一个事物完成后,redolog和binlog都需要落盘,那么谁先谁后有影响么?
MySQL为了保证master和slave的数据一致性,就必须保证binlog和InnoDB redo日志的一致性。
如果在crash时redolog和binlog有一个落盘而另一个没有落盘,就会产生主从不一致的问题:
1.先写 redo log 后写 binlog。假设在 redo log 写完,binlog 还没有写完的时候,MySQL 进程异常重启。由于我们前面说过的,redo log 写完之后,系统即使崩溃,仍然能够把数据恢复回来,所以恢复后这一行 c 的值是 1。但是由于 binlog 没写完就 crash 了,这时候 binlog 里面就没有记录这个语句。因此,之后备份日志的时候,存起来的 binlog 里面就没有这条语句。然后你会发现,如果需要用这个 binlog 来恢复临时库的话,由于这个语句的 binlog 丢失,这个临时库就会少了这一次更新,恢复出来的这一行 c 的值就是 0,与原库的值不同。
2.先写 binlog 后写 redo log。如果在 binlog 写完之后 crash,由于 redo log 还没写,崩溃恢复以后这个事务无效,所以这一行 c 的值是 0。但是 binlog 里面已经记录了“把 c 从 0 改成 1”这个日志。所以,在之后用 binlog 来恢复的时候就多了一个事务出来,恢复出来的这一行 c 的值就是 1,与原库的值不同。
如果想保证主从的一致性,必须保证这两个日志,要么都落盘,要么都失败
所以在开启Binlog后,如何保证binlog和InnoDB redo日志的一致性呢?为此,MySQL引入二阶段提交(two phase commit or 2pc),MySQL内部会自动将普通事务当做一个XA事务(内部分布式事务)来处理。
在最后提交事务的时候,需要有3个步骤:
1.redo log落盘,处于prepare状态
2.binlog落盘
3.修改redo log状态为commit
ps: redo log的提交分为prepare和commit两个阶段,所以称之为两阶段提交
当数据恢复时,如果一个事务redolog处于prepare阶段,会先检查它有没有完整的binlog,如果有我们会认为该事务执行成功,没有,我们认为该事务没有执行成功
我们可以模拟一下crash时机,当步骤1之前发生crash,redolog和binlog都没有,此时需要undolog进行回滚,当步骤1和步骤3之间发生crash,那就是那就是上述红字的情况,此时有redolog,但出于prepare状态,按照binlog的存在与否决定使不使用该redolog