赞
踩
binlog(归档日志,又叫二进制日志):mysql 数据库自带的日志(mysql 分两层,上层是 mysql-server,下层是可替换的数据库引擎),值得一提的是,binlog 是默认关闭的
1,这个日志用来归档,也就是数据库的基于时间点的数据还原
2,MySQL 数据库的数据备份、主备、主主、主从都离不开 binlog,需要依靠 binlog 来同步数据,保证数据一致性,这也是它的主要主要——主从复制
3,被 sql 注入后,可以查找就恢复数据
binlog 存放的位置由 datadir 参数控制,在 binlog 文件夹下有两种文件,一个是 binlog0000XX,记录了修改操作的逻辑,并且有多个版本,另一个是 binlog-index,记录了所有的 binlog 历史版本名称
在每次写入操作的时候,将信息以二进制的形式保存在磁盘中,binlog 是 mysql 的逻辑日志
binlog 可以通过 binlog_format 来控制存储格式:
这里说一下逻辑日志和物理日志的区别:逻辑日志可以简单理解为记录的就是 sql 语句;mysql 数据最终是保存在数据页中的,物理日志记录的就是数据页变更
事务执行时,会给每个线程在内存中分配一块地方叫 binlog cache,binlog 文件就记录在这里,事务提交的时候,再把 binlog cache 写到 binlog 文件中。注意,一个事务的 binlog 不能被拆开提交,无论这个事务多大,也要确保一次性写入
对于 InnoDB 存储引擎而言,只有在事务提交时才会记录 binlog ,此时记录还在内存中,那么 biglog 是什么时候刷到磁盘中的呢?
mysql 通过 sync_binlog 参数控制 biglog 的刷盘时机,取值范围是 0-N:
从上面可以看出, sync_binlog 最安全的是设置是 1 ,这也是 MySQL 5.7.7 之后版本的默认值。但是设置一个大一些的值可以提升数据库性能,因此实际情况下也可以将值适当调大,牺牲一定的一致性来获取更好的性能
之前也讲过 binlog 是用来做数据一致性的,那具体的步骤是什么呢?
首先,当从库链接主库的时候,主库会发送 binlog event 并且持有该 event 的 lock,当确认从库读到 event 后会释放 lock
从库有一个 IO 线程专门用来读主库的 event,读到后会生成 relay log(中继日志),从库还有一个SQL thread,SQL thread 用来在从库上执行 relay log
由于异步复制的过程,它在主机挂掉的时候,会有一部分已经提交的事务无法复制到从库。半同步相对异步复制来说,能更少的避免数据丢失,不过性能方面较差
半同步是这个过程,在事务提交之前,将 binlog 发送到从库,从库确认接受之后主库才进行提交
redolog(重做日志):InnoDB 储存引擎自带,引擎用这个来支持事务,机械故障后根据这个日志进行事务回滚操作。这个日志是事务执行的关键
redolog 日志只记录事务对哪些数据页做了哪些修改,它是物理日志,它会存储类似以下意思的内容,就是位置与数据:
将 x 表空间的 xx 页偏移量为 xxx 的内容修改为 xxxx
这样记录的好处是方便快捷,在尽可能少的数据里表达尽可能多的信息,因此在存入磁盘时非常快速
redolog 的类型有很多,它的每种类型都可能不一样,它不完全是上面这种格式的
MySQL 为了提高的性能,对于增、删、改这种操作都是在内存中完成的,也就是将 Buffer Pool 中的数据页改成脏数据页,此外 MySQL 还有专门的后台线程等其他机制负责将脏数据页刷新同步回磁盘。这么做是因为 MySQL 以页为单位读取数据,每次修改都进行 IO 那么效率就太低了
那么当脏数据没有刷入磁盘或者正在刷入磁盘时电脑挂了怎么办,也就是事务处于失败状态的时候如何将其回滚到中止状态
redolog 就是用来恢复提交后的物理数据页 (恢复数据页,且只能恢复到最后一次提交的位置),只要在重启时解析 redo log 中的事务然后重做一遍。将 Buffer Pool 中的缓存页重做成脏页。后续再在合适的时机将该脏页刷入磁盘即可
MyIsam 为什么不支持回滚与事务,就是因为存储引擎没有实现 redolog。请想想事务需要满足什么条件,ACID 对吧,该日志的存入磁盘的方式就决定了原子性与持久性这两个功能的实现
其实除了上面这种提交日志来保证原子性与持久性,还有一种叫 **Shadow Paging(影子分页)**的方式可以实现这种功能,它的大体思路是在数据写入的时候,先将原来的数据拷贝一份(影子),对影子进行修改,在提交的时候将指向数据的指针指向影子以替换原来的数据。不过这么做在实现隔离性的时候会遇上很多问题,因此只有在小型的数据库中才用这种方式实现(redis 的 AOF 也使用这种方式)
redolog 因为非常小,不占用内存空间,落盘比较快,并且以一定规则插入磁盘,因此保证了数据的一致性
redolog 刷盘时机有以下几种情况:
1,脏页刷盘时 redolog 找时机刷盘,在多线程的情况下,脏页是顺序刷盘的,redolog 也是顺序刷盘的
mysql 每执行一条 DML 语句,会先将记录写入内存中的 redo log buffer,并且根据不同的配置,在不同的时间点再一次性将多个操作记录写到 redo log file,即磁盘中的 redo 文件组
这里的某个时间点,可以根据参数 innodb_flush_log_at_trx_commit 来设置,和 binlog 类似:
双1设置:始终设置 innodb_flush_log_at_trx_commit=1,启用二进制日志记录并设置 sync_binlog=1,这就是MySQL最安全的设置方法
2,后台线程刷盘,就是上面说的每秒一次的频率将 redolog 刷新到磁盘
3,正常关闭服务器时
4,当 redo log buffer 占用的空间即将达到 innodb_log_buffer_size 一半的时候,后台线程会主动刷盘
基于后台线程以及一半就刷盘的机制,有可能在事务未提交时就会将 redolog 刷入磁盘中
5,做 ckeckpoint 时
redolog 有很多类型,不同的类型有不同的格式,大致有以下的通用格式,以及一些相当的格式
最简单的 redolog 里面存储着表空间ID、页号、偏移量以及需要更新的值,因此所需内存是比较小的
type :该条 redo 日志的类型。
space ID :表空间 ID。
page number :页号。
data :该条 redo 日志的具体内容
这种格式用于内存对磁盘修改非常少的情况下,比如将隐藏主键到达256时写入磁盘时。往往一些增加删除操作需要对磁盘中的页进行大规模修改,比如增加一条数据,不仅仅是在页中插入一条数据行,它还可能修改以下内容:
我们在每个修改的地方都记录一条对应的 redo 日志显然是不现实的,因此实现方式是用时间换空间,我们在数据库崩了之后用日志还原数据时,在执行这条日志之前,数据库应该是一个一致性状态,我们用对应的参数,执行固定的步骤,修改对应的数据。这就类似与函数,因此我们只要记录所有的参数即可
这些比较复杂的重写日志类似下面这样
redolog 也需要保证数据的完整性,不能说 redolog 写一半系统崩溃了,我们还拿着错误的 redolog 去恢复数据库。redolog 虽然比较小,但是还是数据,并且是一段连续的数据,因此和接受 TCP 报文的保证数据完整性的方式都是一样的,在末尾多加了一条特殊形式的 redo 日志 MLOG_MULTI_REC_END,系统读到这条日志则认为这组 redo 日志完整
mysql 规定一些操作是不可分割的最小单元,对这些操作的一次原子访问称为一个 mini-transaction,简称 mtr。这些操作有:
(1)更新 Max Row Id 属性(更新一页)
(2)向聚簇索引对应的 B+ 树的页面插入一条记录(可能更新多个页面)
(3)向某个二级索引对应的 B+ 树的页面插入一条记录(可能更新多个页面)
(4)其他的对页面的访问操作
所以一个 mtr 可能会产生多条 redo 日志,这些 redo 日志可以归为一组,称为 redo 日志记录组(redo log record group)
一条 sql 语句(比如 insert)可能会操作多棵 B+ 树,所以一条sql语句会包含多个 mtr
一个事务可能包含多条 sql 语句。
所以,事务、sql 语句、mtr、redo log 的关系如下:
redolog 在内存中的存放位置叫 log buffer,我们为了了解 redolog 应该在哪个页的哪个偏移量写,提供了一个叫 buf_free 的全局变量,该变量指明后续写入的 redo 日志应该写到 log buffer 的哪个位置
那 redolog 在磁盘中又是如何存储的呢?
硬盘上存储的 redo log 日志文件不只一个,而是以一个日志文件组的形式出现的,每个的 redo 日志文件的大小都是一样的
比如可以配置为一组4个文件,每个文件的大小是 1GB,整个 redo log 日志文件组可以记录 4G 的内容
每个文件中都包含了多个页,并且这些页负责的内容可能不一样,每个文件的前2048个字节(也就是前四页)用来存储一些管理信息,之后的页就是用来存储 redolog 的普通页了
日志文件组采用的是环形数组形式,从头开始写,写到末尾又回到头循环写.为了管理这个日志文件组,我们显然需要一些全局变量,比如记录了当前写在哪里的偏移量,一共写了多少数据等等内容
lsn,也就是 log sequence number 这个全局变量是记录当前总共写入的 redo 日志量的。在 mysql 开机时,这个值为 8704(一条数据都没写入 lsn 就是8704)。每次写入 x 字节的数据,这个值就加 x。同时,他还会将遇到的 log block header 和 log block trailer 占用的字节数加上(也就是页头和页尾占用的字节数)
而 buf_free 是下一次写入的记录的偏移量,一边写一边后移,它用来记录当前事务产生的 redo log 文件
flushed_to_disk_lsn 是用来标记当前 log buffer 中已经有那些日志被刷新到磁盘中了,该值表达的是内存中该值一开始也是8704,因此 lsn >= flushed_to_disk_lsn。log buffer 就是在磁盘中存放 redolog 的地方,log buffer 比之日志文件组就类似于 buffer pool 比之磁盘中的 Innodb
checkpoint 指的是一次刷新全局变量 checkpoint_lsn 的操作,MySQL 中可以使用 lsn 来唯一确定 redolog 位置,而 checkpoint_lsn 就指向当前可以被擦除的位置。日志文件组的大小是有限的,我们不得不循环使用日志文件组中的文件,但是如果某些日志在该日志代表的数据被刷入磁盘之前就被清理掉了,日志就没有意义了。因此可以被擦除的地方就是数据已经刷入磁盘的地方,一边擦一边往后推移,MySQL 加载日志文件组恢复数据时,会清空加载过的 redo log 记录。如果 MySQL 一直不崩溃,redolog 记录满了的话,MySQL会自动刷盘并且删除一些 redo log 记录,让 checkpoint_lsn 向后推
注意,执行一次 checkpoint 与将脏页刷新到磁盘中是不同步的,因此 checkpoint_lsn 不能代表刷新到磁盘中的数据的最新位置
至此,我们对数据库有了一个完整的日志记录,那如何使用这个日志记录功能呢,我们如何将数据从这个日志文件组中取出来并且用于恢复数据库呢
由于之前的全局变量与日志文件组配合记录,在 MySQL 中已经有了充足的信息。我们可以拿到需要恢复的起点(全局变量 checkpoint_lsn),和恢复的终点(可以用 lsn 来表示)。这两个值都能唯一确定一个日志文件组中的位置,并且里面的数据都保证正确性(每一条数据的末尾都有唯一标识),不会出现没有被刷入磁盘的数据对应的日志被刷掉的情况(checkpoint)
获取到日志后,我们可以一条条读取数据并且恢复,但是 MySQL 的设计者有更加快速的方法,将每个日志的 spaceID 与 page number 计算出哈希值并且存入 hash 表中,如果有多个 spaceID 与 page number 都相同的日志,将它们放入一个槽中。这样遍历槽,就可以一次性将一个页面恢复好,从而避免很多随机 IO,加快了恢复速度
这么恢复还有一点要注意,我们需要根据时间来恢复,不然最终的数据不保证正确性。同时,由于 checkpoint_lsn 与刷入磁盘的机制不一样,因此可能出现数据已经刷入了,但是日志还需要重新操作一遍的情况。重新操作一遍不会导致任何错误,但是可以优化掉这些过程,在每个页面中的文件头有个 FIL_PAGE_LSN 的属性,该属性记录了最新一次对该页面修改的日志的 lsn。我们只需要简单的判断就可以略过重新写一遍的过程了
undo log 里面记录的是版本链历史记录,即之前每一行的历史版本并且使用链连接起来,所以它是一个物理日志,回滚会根据历史记录做逆向操作
上图主要说了数据表中的数据与 undo log 的关系
1,MySQL内部定义了一个全局变量,每当有事务需要id时,则将当前的值分配给该事务,并进行自增1
2,当这个变量值为256的倍数时,则刷新到系统表空间中页号为5的页面下的一个名为max trx id的属性中,该属性占用8字节的空间
3,当系统下次启动时,会将该值加载到内存中并再加上256(防止上次关闭时内存中的值没有及时刷到盘中)
它记录一行行物理数据。一般对一条记录进行修改,就会记录一条 undolog,因此应该事务会记录多条 undolog,这些 undolog 会被存放在类型为 FIL_PAGE_UNDO_LOG 的页中
并且不同事务或者相同事务的对同一记录行的修改,会使该记录行的 undolog 成为一条链表,链首就是最新的记录,链尾就是最早的旧记录
不同的 undolog 有不同的组成
而 undolog 的记录就是在 delete mark 过程中做的,因为需要回滚的数据只在事务执行的时候,在事务提交之后,就没必要使用 undolog 回滚了。针对以上两个前提,undolog 的设计应该可以找到更新前的链表中的位置,以及在垃圾列表中的位置
update 的过程分为两类,一类是当数据更新后的大小与更新前的大小一致时,此时直接在用来的位置上更新数据,另一类是数据更新后的大小与更新前不一致时,此时会先删除旧记录并且添加新记录,注意,此时的删除并不是 delete mark 操作,而是将数据添加到垃圾链表的操作
这三类 undolog 日志其实有相同的部分,就是前置指针与后置指针,分别用来寻找该数据的下一次操作(或者表中的数据)与上一次操作
undo 日志存储在类型为 fil_page_undo_log 的页面中。同时,他也具有和普通的页面一样的结构,Undo Page Header 是 undo 页面独有的表头,他的部分属性如下:
undo 日志类型被分为两种,第一种是 trx_undo_insert(十进制的1表示),产生的插入日志都属于这种类型,也称为 insert undo 日志,
第二种是 trx_undo_update(十进制的2表示),除了 insert 类型,其他都属于这种类型,统称 update undo 日志
一个页面只能存储一种类型的 undo 日志,不可以混合存储,之所以做出区分,是因为 insert 类型的日志可以在事务提交后直接删除,而 update 类型的由于需要为 MVCC 服务,因此要区别对待
从页面的角度上来说,需要注意的点就这些了,但是从事务的角度上说,知识点还没结束。一个事务可能产生很多日志,这些日志在一个页面中可能放不下,那么就需要放到更多的页面中,这些页面就通过 trx_undo_page_node 形成了一个链表
链表中的第一个 undo 页面称为 first undo page,其余称为 normal undo page,因为第一个页面除了 undo page header 还有一些其他的管理信息,即 undo log segment header
一个事务的执行过程中,增删改的操作都会有,因为一个 undo 页面只能存放一种类型,所以一个事务的执行过程中可能有两种链表,一个是 insert undo 链表,一个是 update undo 链表
此外,Innodb 还规定,普通表和临时表的 undo 日志也要分别记录,所以一个事务中如果同时对临时表,普通表进行增删改操作,就会有4个链表
undo log segment header 拥有的部分属性如下:
1,trx_undo_state:本 undo 页面链表处于什么状态,我们可以用该属性了解事务是否结束,可能的状态有下面几种:
2,trx_undo_fseg_header:本 Undo 页面链表对应的段的 Segment Header 信息
3,trx_undo_page_list:Undo 页面链表的基节点,用于串联起其他页面的 trx_undo_page_node 属性,形成一个链表
因此,一个事务的 undolog 不是由页为单位来管理的,而是由链表来管理的,那应该需要一个整合的地方来管理这些链表吧,我们提出了回滚段的概念。回滚段就是被称为 Rollback Segment Header 的页面,这个页面中存放了各个 Undo 页面链表的 first undo page 的页号,这些页号被称为 undo slot。这样我们就可以提供 slot 来找到对应的链表头了
一个事务在执行过程中最多分配4个undo页面链表,一个回滚段中只有1024个undo slot,意味着同时只支持1024个事务的并发,
为了支持更多的事务执行,Innodb定义了128个回滚段,因此可以支持更多的事务,这些回滚段的页面存在系统表空间的第五个页面的一个区域中
1,MySQL 就是通过 undolog 回滚日志来保证事务原子性的,准确的说是再发生错误时提供回滚服务。在异常发生时,对已经执行的操作进行回滚,回滚日志会先于数据持久化到磁盘上(因为它记录的数据比较少,所以持久化的速度快),当用户需要回滚数据时(比如服务器断电了,再次启动数据库的时候,或者事务中出现异常需要回滚时),数据库能够通过查询回滚日志来回滚将之前未完成的事务
2,支持 MVCC(多版本并发控制),提高事务读取数据的效率
**为什么有了 redolog 还需要 undolog?**这就要从磁盘性能的优化说起
Commit Logging 有一个巨大的先天缺陷:所有对数据的真实修改都必须发生在事务提交以后,即日志写入了 Commit Record 之后。在此之前,即使磁盘有足够空闲,即使某个事务修改的数据量非常庞大,占用了大量的内存缓冲区,无论何种理由,都决不允许在事务提交之前就修改磁盘上的数据,这一点是 Commit Logging 成立的前提(因为我们我们不能使用 redolog 删除错误的记录,只能用它重做正确的记录),却对提升数据库的性能十分不利。为此,ARIES 提出了“提前写入日志”(Write-Ahead Logging)的日志改进方案,所谓“提前写入”(Write-Ahead),就是允许在事务提交之前写入变动数据的意思
Write-Ahead Logging 按照事务提交时点,将何时写入变动数据划分为 FORCE 和 STEAL 两类情况
·FORCE:当事务提交后,要求变动数据必须同时完成写入则称为 FORCE,如果不强制变动数据必须同时完成写入则称为 NO-FORCE
·STEAL:在事务提交前,允许变动数据提前写入则称为 STEAL,不允许则称为 NO-STEAL
Write-Ahead Logging 允许 NO-FORCE,也允许 STEAL,它给出的解决办法是增加了另一种被称为 Undo Log 的日志类型,当变动数据写入磁盘前,必须先记录 Undo Log,注明修改了哪个位置的数据、从什么值改成什么值等,以便在事务回滚或者崩溃恢复时根据 Undo Log 对提前写入的数据变动进行擦除
由于 Undo Log 的加入,Write-Ahead Logging 在崩溃恢复时会经历以下三个阶段
事务首次修改普通表的记录时,先去系统表空间的5号页面中分配到一个回滚段,之后该事务再修改记录时,不会重复分配,多个回滚段的分配方式使用 round-robin 来分配,从第一大类中循环分配回滚段给多个事务
分配到回滚段后,查看回滚段的两个 cached 链表是否有缓存的 undo slot,不同的操作看不同的链表,insert 类的看 insert undo cached,update 类型的看 update undo cached
如果在缓存中没找到,就从回滚段中分配一个可用的 undo slot
找到可用的 undo slot,如果该 slot 是从缓存链表中获取的,其 Undo Log Segment 已经分配,否则就需要重新分配一个 Undo Log Segment,然后从该 Segment 中申请一个页面作为 Undo 页面链表的 first undo page,并把该页填入 undo slot 中
事务开始写入日志到 Undo 页面链表中
执行更新语句时的日志操作时,我们会同时更新这三条日志,如果三条日志的记录对不上,那么利用不同日志恢复数据之后的状态就会不一致,为此,我们需要来保证这三条日志在任何时间节点都处于一致的状态
重写日志是 Innodb 提供的(这也就是 innodb 存储引擎支持事务的原因),归档日志是 mysql 提供的
binlog 主要保证 MySQL 集群架构的数据一致性,而 redolog 主要保证机器崩溃后的数据恢复工作,如果两条日志不一致会出现集群与本机的数据不一致
执行更新语句时,先更新数据,同时记录 redolog(重做日志),redolog 进入准备状态,告诉执行器记好了,执行器记录 binlog(归档日志)完毕,提交事务,redolog 从准备进入完毕状态,简单来说就是以下步骤:
记录 redolog -> 记录 binlog -> binlog 提交完毕 -> redolog 提交完毕
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。