# redo log
# redo log日志是个啥
redo log是物理日志。记录了某个数据页做了什么修改,比如对某个表空间的某个数据页某个偏移量的地方做了什么更新。每当执行一个事务就会产生这样的一条或者多条物理日志。
在事务提交时,只要先将 redo log 持久化到磁盘即可,可以不需要等到将缓存在 Buffer Pool 里的脏页数据持久化到磁盘。
当系统崩溃时,虽然脏页数据没有持久化,但是 redo log 已经持久化,接着 MySQL 重启后,可以根据 redo log 的内容,将所有数据恢复到最新的状态。 Redo的作用就是系统崩溃重启时将崩溃之前已经提交了事务但还没有刷新到磁盘中的数据更新到磁盘中。
好处:
- redo日志占用的空间非常小
- redo日志是顺序写入磁盘的:使用的是顺序IO
# 为什么需要redo log
首先我们知道InnoDB存储引擎是以页为单位来管理存储空间的。我们进行的所有增删改查操作其实本质上都是在页面上进行操作的。
假如我们对某条数据进行修改时,首先是从磁盘上将这条数据所在的页都读取到Buffer Pool上。然后在buffer pool上对这条记录进行修改。这个时候这条记录所在的页就变成了脏页。更新完成以后。MySQL会在某个时机将脏页的数据刷新到磁盘上。这个时候我们才能说这条更新语句执行成功了。但是这样就有一个问题。如果在这条记录对应的页变成脏页以后,没有刷新到磁盘之前。MySQL系统崩溃了。当MySQL重启时。我们更新了这条记录就不存在了。这肯定是不行的!!因此InnoDB存储引擎引进了redo log日志。它的作用就是在在事务提交时,只要先将 redo log 持久化到磁盘即可,可以不需要等到将缓存在 Buffer Pool 里的脏页数据持久化到磁盘。redo log也叫做重做日志。
# redo log 格式
redo log本质只是记录了一下事务对数据库做了哪些修改。InnoDB针对事务对数据库不同场景定义了多种类型的redo 日志。但是大部分redo日志的格式都是通用的。
- type:该条redo 日志的类型
- space ID:表空间ID
- page number 页号
- data:该条redo日志的具体内容
# Mini-Transaction-简称mtr
# 以组的形式写入redo 日志
一条更新语句在执行过程中可能修改若干个页面,例如更新聚簇索引和二级索引对应的页面(当然也可能更新一些系统页面)。并且对这些页面的修改都在Buffer Pool中。修改完以后需要记录一下相应的redo 日志。在执行语句的过程中产生的redo日志被设计InnoDB分成了若干个不可分割的组,比如
- 更新 Max Row ID 属性时产生的 redo 日志是不可分割的。
- 向聚簇索引对应 B+ 树的页面中插入一条记录时产生的 redo 日志是不可分割的。
- 向某个二级索引对应 B+ 树的页面中插入一条记录时产生的 redo 日志是不可分割的。
- 还有其他的一些对页面的访问操作时产生的 redo 日志是不可分割的。。。
InnoDB在同一个组中的日志做了一个简单的标记。就是在该组中最后一条redo日志加上一个特殊类型的redo日志,该类型名称为MLOG_MULTI_REC_END。所以某个需要保证原子性的操作产生的一系列 redo 日志必须要以一个类型为 MLOG_MULTI_REC_END 结尾, 这样在系统奔溃重启进行恢复时,只有当解析到类型为 MLOG_MULTI_REC_END 的 redo 日志,才认为解析到了一组完整的 redo 日志,才会进行恢复。否则的话直接放弃前边解析到的 redo 日志。
# Mini-Transaction的概念
InnoDB把对底层页面中的一次原子访问的过程称之为一个Mini-Transaction,简称mtr。比如上边所说的修改一次 Max Row ID 的值算是一个 Mini-Transaction ,向某个索引对应的 B+ 树中插入一条记录的过程也算是一个 Mini-Transaction 。通过上边的叙述我们也知道,一个所谓的 mtr 可以包含一组 redo 日志,在进行奔溃恢复时这一组 redo 日志作为一个不可分割的整体。
简单总结一下一个事务可以包含若干条语句,每一条语句又可以包含若干个MTR,每一个MRT又可以包含若干条redo日志
# 日志的写入过程
# redo log block
为了更好的管理redo日志,设计InnoDB把通过MTR生成的redo日志都放在了大小为512字节的页中。为了和表空间中的页进行区别,这里把存储redo日志的页称为block。redo log block示意图如下
真正的redo log都是存储到占用496字节的 redo block body中。途中的 log block header和log block trailer存储的是一些管理信息
# redo log 缓冲区
通过上面描述InnoDB为了解决访问磁盘速度过慢的问题而引入了Buffer Pool。同理写入redo 日志时也不能直接写到磁盘上。实际上MySQL在启动时向操作系统申请了一大片连续内存空间-redo log buffer。翻译成中文就是redo 日志缓冲区。这片内存空间被划分为若干个连续的redo log back。log buffer 结构示意图如下
# redo 日志写入log buffer
向log buffer中写入redo日志的过程是顺序的,也就是先往前边的block中写。当该block的空闲空间用完以后在往下一个空间写。InnoDB设计了一个buf_free的全局变量,该变量指明后序写入的redo 日志应该写入到log buffer 那个位置。
# redo log 刷盘时机
默认在一些情况下会刷新到磁盘里,比如
- MySQL 正常关闭时;
- 当 redo log buffer 中记录的写入量大于 redo log buffer 内存空间的一半时,会触发落盘;
- 事务提交时
- 将某个脏页刷新到磁盘前
- InnoDB 的后台线程每隔 1 秒,将 redo log buffer 持久化到磁盘。
- 每次事务提交时都将缓存在 redo log buffer 里的 redo log 直接持久化到磁盘(这个策略可由 innodb_flush_log_at_trx_commit 参数控制,)。
# redo 日志文件组
MySQL 的数据目录(使用 SHOW VARIABLES LIKE 'datadir' 查看)下默认有两个名为 ib_logfile0 和ib_logfile1 的文件,但可不是只有这俩个。这些文件 以 ib_logfile[数字] ( 数字 可以是 0 、 1 、 2 ...)的形式进行命名。在将 redo 日志写入 日志文件组 时,是从 ib_logfile0 开始写,如果 ib_logfile0 写满了,就接着 ib_logfile1 写,同理, ib_logfile1 写满了就去写 ib_logfile2 ,依此类推。如果写到最后一个文件该咋办?那就重新转到 ib_logfile0 继续写, 如果我们对默认的redo 日志文件不满意,可以通过下边几个启动参数来调节:
- innodb_log_group_home_dir: 该参数指定了 redo 日志文件所在的目录,默认值就是当前的数据目录。
- innodb_log_file_size:该参数指定了每个 redo 日志文件的大小,在 MySQL 5.7.21 这个版本中的默认值为 48MB ,
- innodb_log_files_in_group:该参数指定 redo 日志文件的个数,默认值为2,最大值为100。
这里介绍几个小概念,log sequence number:简称lsn,用来记录当前已经写入的redo日志量。InnoDB规定lsb初始值是8704。在向log buffer写入redo日志时不是一条一条写入的,而是以MTR生成的一组redo日志为单位写入的。所以lsn的增长量=实际写入的日志量+log block header+log block trailer 。同时lsn也包括已经写到log buffer但没有刷新到磁盘中的。
flushed_to_disk_lsn:表示已经刷新到磁盘中的redo日志量的全局变量。
buf_next_to_wirte:用来标记当前log buffer 中已经有哪些日志刷新到磁盘中了。
# check point
首先我们redo日志文件组容量是有限的。我们不得不选择循环使用redo日志文件组中的文件。通过上面的描述我们知道早晚有一天最后写的redo日志与最开始写的redo日志追尾。但是我们都知道:redo日志只是为了系统奔溃后恢复脏页用的,如果对应的脏页已经刷新到了磁盘,也就是说即使现在系统奔溃,那么在重启后也用不着使用redo日志恢复该页面了,所以该redo日志也就没有存在的必要了,那么它占用的磁盘空间就可以被后续的redo日志所重用。也就是说:判断某些redo日志占用的磁盘空间是否可以覆盖的依据就是它对应的脏页是否已经刷新到磁盘。
因此我们可以将redo log循环写的方式看成一个环形。InnoDB用write pos表示redo log当前记录写到的位置,用checkpoint表示当前要擦除的位置,如下图:
- write pos 和 checkpoint 的移动都是顺时针方向;
- write pos ~ checkpoint 之间的部分(图中的红色部分),用来记录新的更新操作;
- check point ~ write pos 之间的部分(图中蓝色部分):待落盘的脏数据页记录;
如果 write pos 追上了 checkpoint,就意味着 redo log 文件满了,这时 MySQL 不能再执行新的更新操作,也就是说 MySQL 会被阻塞(因此所以针对并发量大的系统,适当设置 redo log 的文件大小非常重要),此时会停下来将 Buffer Pool 中的脏页刷新到磁盘中,然后标记 redo log 哪些记录可以被擦除,接着对旧的 redo log 记录进行擦除,等擦除完旧记录腾出了空间,checkpoint 就会往后移动(图中顺时针) ,然后 MySQL 恢复正常运行,继续执行新的更新操作。
# undo log
首先我们都知道事务需要保证原子性,也就是事务中的操作要么全部成功,要么什么也不错,但可能有时候事务执行到一半的时候会出现一些情况。比如事务执行一半的时候出现各种错误。可能是服务器锁错误,也可能是系统错误。或者程序员在事务执行过程中输入roll back语句结束当前的事务。
因此InnoDB为了保证事务的原子性,引入了undo log(回滚日志机制),通过undo log保证了事务ACID特性中的原子性。undo log 是一种用于撤销回退的日志。在事务没提交之前,MySQL 会先记录更新前的数据到 undo log 日志文件里面,当事务回滚时,可以利用 undo log 来进行回滚。
# 事务id
# 给事务分配id的时机
如果某个事务执行过程中对某个表执行了增、删、改操作。那么InnoDB存储引擎就会给他分配一个独一无二的事务id,分配方式如下
- 对于只读事务来说,只有在它第一次对某个用户创建的临时表执行增、删、改操作时才会为这个事务分配一个事务i。否则的话是不分配事务ID。
- 对于读写事务来说,只有它在第一次对某个表(包括用户创建的临时表),执行增、删、改操作时才会为这个事务分配一个事务id。否则不分配事务ID
# 事务id是怎么生成的
事务id本质上就是一个数字,分配策略和隐藏列row_id的分配策略大抵相同,具体策略如下:
- 服务器会在内存中维护一个全局变量,每当需要为某个事务分配一个 事务id 时,就会把该变量的值当作 事务id 分配给该事务,并且把该变量自增1。
- 每当这个变量的值为 256 的倍数时,就会将该变量的值刷新到系统表空间的页号为 5 的页面中一个称之为Max Trx ID 的属性处,这个属性占用 8 个字节的存储空间。
- 当系统下一次重新启动时,会将上边提到的 Max Trx ID 属性加载到内存中,将该值加上256之后赋值给我们前边提到的全局变量(因为在上次关机时该全局变量的值可能大于 Max Trx ID 属性值)。
这样就可以保证整个系统中分配的 事务id 值是一个递增的数字。先被分配 id 的事务得到的是较小的 事务id ,后被分配 id 的事务得到的是较大的 事务id 。
# trx_id隐藏列
聚簇索引的记录除了会保存完整的用户数据以外,而且还会自动添加名为trx_id、roll_pointer的隐藏列,如果用户没有在表中定义主键以及UNIQUE键,还会自动添加一个名为row_id的隐藏列。trx_id就是这条记录当前的事务id。
# undo 日志简介
一个事务在执行过程中可能新增、删除、更新若干条记录,也就是说需要记录很多条对应的 undo日志 ,这些 undo日志 会被从 0 开始编号,也就是说根据生成的顺序分别被称为 第0号undo日志 、 第1号undo日志 、...、 第n号undo日志 等,这个编号也被称之为 undo no 。undo日志是被记录到类型为FIL_PAGE_UNDO_LOG的页面中。 这些页面可以从系统表空间中分配,也可以存放到undo日志的表空间,也就是所谓的undo tablespace
每当 InnoDB 引擎对一条记录进行操作(修改、删除、新增)时,要把回滚时需要的信息都记录到 undo log 里,比如:
- 在插入一条记录时,要把这条记录的主键值记下来,这样之后回滚时只需要把这个主键值对应的记录删掉就好了;
- 在删除一条记录时,要把这条记录中的内容都记下来,这样之后回滚时再把由这些内容组成的记录插入到表中就好了;
- 在更新一条记录时,要把被更新的列的旧值记下来,这样之后回滚时再把这些列更新为旧值就好了。 不同的操作,需要记录的内容也是不同的,所以不同类型的操作(修改、删除、新增)产生的 undo log 的格式也是不同的,具体的每一个操作的 undo log 的格式本文就不详细介绍了。因为介绍起来太多了。
一条记录的每一次更新操作产生的 undo log 格式都有一个 roll_pointer 指针和一个 trx_id 事务id:
- 通过 trx_id 可以知道该记录是被哪个事务修改的;
- 通过 roll_pointer 指针可以将这些 undo log 串成一个链表,这个链表就被称为版本链;
# undo日志具体写入过程
# 段的概念
这里就重新简单介绍一下段。如果感兴趣的话可以看我之前的一篇关于表空间的文章。里面对段的概念进行了详细阐述。段是一个逻辑概念,本质上是由若干个零散的页面和若干个完整的区组成的.比如一个B+ 树索引被划分成两个段,一个叶子节点段,一个非叶子节点段,这样叶子节点就可以被尽可能的存到一起, 非叶子节点被尽可能的存到一起。每一个段对应一个 INODE Entry 结构,这个 INODE Entry 结构描述了这个段的各种信息,比如段的 ID ,段内的各种链表基节点,零散页面的页号有哪些等信息。但同时InnoDB为了定位一个INODE Entry,设计了一个Segment Header结构:
整个Segment Header占用10个字节大小
- Space ID of the INODE Entry:Inode Entry结构所在的表空间ID。
- Page Number of the INODE Entry:INODE Entry结构所在的页面页号。
- Byte Offset of the INODE Ent:INODE Entry结构在该页面中的偏移量。 知道了表空间ID,页号,页内偏移量,就可以定位一个INODE Entry。
# Undo Log Segment Header
在InnoDB里,每一个Undo页面链表都对应着一个段,称之为Undo Log Segment,链表中的页面都是从这个段里边申请的。所以他们在 Undo页面 链表的第一个页面中设计了一个称之为 Undo Log Segment Header 的部分,这个部分中包含了该链表对应的段的 segment header 信息以及其他的一些关于这个段的信息。看一下undo 页面链表第一个页面结构:
Undo链表的第一个页面比普通页面多了个Undo Log Segment Header。来看一下它的结构
- TRX_UNDO_STATE :本 Undo页面 链表处在什么状态。
- TRX_UNDO_ACTIVE:活跃状态,一个活跃的事务正在往这个段里面写入undo 日志
- TRX_UNDO_CACHED:被缓存的状态。等待着之后被其他事务重用
- TRX_UNDO_TOFREE :对于insert undo链表来说,如果它所在的事务被提交,该链表不能被重用,那么就会处于这种状态
- TRX_UNDO_TO_PURGE:对于update undo链表来说,如果它所在的事务被提交,该链表不能被重用,那么就会处于这种状态
- TRX_UNDO_PREPARED :包含处于 PREPARE 阶段的事务产生的 undo日志 。
- TRX_UNDO_LAST_LOG:本 Undo页面 链表中最后一个 Undo Log Header 的位置。
- TRX_UNDO_FSEG_HEADER :本 Undo页面 链表对应的段的 Segment Header 信息
- TRX_UNDO_PAGE_LIST : Undo页面 链表的基节点。
# Undo Log Header
一个事务向Undo页面中写入undo日志是十分简单暴力的,就是直接往里写,一条接着一条写入,写完一个undo页面后,在从段里申请一个新页面,然后把这个页面插入Undo页面链表,继续往这个新申请的页面中写。InnoDB认为同一个事务向一个Undo页面链表中写入的undo日志算是一个组。
Undo Log Header里面包含了很多属性本文就简单介绍一个比较重要的:
- TRX_UNDO_TRX_ID :生成本组 undo日志 的事务 id 。
- TRX_UNDO_TRX_NO :事务提交后生成的一个需要序号,使用此序号来标记事务的提交顺序(先提交的此序号小,后提交的此序号大)。
- TRX_UNDO_DICT_TRANS :标记本组 undo日志 是不是由DDL语句产生的。
- TRX_UNDO_TABLE_ID :如果 TRX_UNDO_DICT_TRANS 为真,那么本属性表示DDL语句操作的表的 table id 。
- TRX_UNDO_NEXT_LOG :下一组的 undo日志 在页面中开始的偏移量。
- TRX_UNDO_PREV_LOG :上一组的 undo日志 在页面中开始的偏移量。
对于没有被重用的 Undo页面 链表来说,链表的第一个页面,也就是 first undo page 在真正写入 undo日志前,会填充 Undo Page Header 、 Undo Log Segment Header 、 Undo Log Header 这3个部分,之后才开始正式写入 undo日志 。对于其他的页面来说,也就是 normal undo page 在真正写入 undo日志 前,只会填充 UndoPage Header 。链表的 List Base Node 存放到 first undo page 的 Undo Log Segment Header 部分, ListNode 信息存放到每一个 Undo页面 的 undo Page Header 部分。
undo log有俩大作用:
- 实现事务回滚,保障事务的原子性:事务处理过程中,如果出现了错误或者用户执 行了 ROLLBACK 语句,MySQL 可以利用 undo log 中的历史数据将数据恢复到事务开始之前的状态。
- 实现MVCC关键因素之一:MVCC是通过ReadView +undo log实现的,undo log 为每条记录保存多份历史记录,MySQL在执行快照读的时候会根据事务的Read View,顺着undo log的版本链找到满足其可见性的记录。
很多人疑问 undo log 是如何刷盘(持久化到磁盘)的?
undo log 和数据页的刷盘策略是一样的,都需要通过 redo log 保证持久化。
buffer pool 中有 undo 页,对 undo 页的修改也都会记录到 redo log。redo log 会每秒刷盘,提交事务时也会刷盘,数据页和 undo 页都是靠这个机制保证持久化的。
# Binlog
# Binlog简介
MySQL的BinLog是一个二进制日志文件,用于记录MySQL数据库中所有的DML(数据操作语言)和DDL(数据定义语言)语句。它的主要作用是实现MySQL数据库的备份、恢复、以及数据复制等功能。
在MySQL中,当一个事务被提交时,MySQL会将该事务的相关信息写入binlog中。这些信息包括事务所执行的SQL语句以及事务的元数据(如事务ID、时间戳等)。因此,通过解析binlog可以还原出数据库中所有的修改操作,从而实现数据库的备份和恢复,或者将这些修改操作应用到其他的MySQL实例上,实现数据的复制。
# redo log和binlog的区别
- 使用对象不同
- binlog是MySQL的Server层实现的日志,所有存储引擎都适用
- redo log是InnoDB存储引擎实现的日志
- 文件格式不同
- binlog 有3种格式类型,分别是STATEMENT(默认格式)、ROW、MIXED,区别如下:
- STATEMENT:每一条修改数据的SQL都会被记录到binlog中(相当于记录了逻辑操作,针对这种格式,binlog也可以称为逻辑日志),主从复制中slave端再根据SQL语句重现,但 STATEMENT 有动态函数的问题,比如你用了 uuid 或者 now 这些函数,你在主库上执行的结果并不是你在从库执行的结果,这种随时在变的函数会导致复制的数据不一致;
- ROW:记录行数据最终被修改成什么样。不会出现STATEMENT下动态函数格式,但缺点就是每行数据都会被记录。如果执行批量update语句,更新多少行就会产生多少条数据,使binlog文件过大。
- MINED:包含了STATEMENT和ROW模式,它会根据不同情况自动使用ROW模式和STATEMENT模式
- redo log是物理日志,记录的是在某个数据页做了什么修改。
- 写入方式不同:
- binlog是追加写,写满一个文件,就创建一个新的文件继续写,不会覆盖以前的日志,保存的是全量的日志。
- redo log是循环写,日志空间大小固定,全部存满就从头开始,保存未被刷入磁盘的增页日志
- 用途不同
- binlog用于备份、主从复制
- redo log用于掉电等故障恢复
# binlog 什么时候刷盘
事务执行过程中,会先把日志写到binlog cache(Server层的cache)事务提交的时候,再把binlog cache写到binlog中。一个事务的binlog是不能被拆开的。无论这个事务多大,都要保证一次性写入。这是因为一个线程只能同时有一个事务在执行。所以每当执行一个 begin/start transaction 的时候,就会默认提交上一个事务,这样如果一个事务的 binlog 被拆开的时候,在备库执行就会被当做多个事务分段执行,这样破坏了原子性,是有问题的。MySQL 给每个线程分配了一片内存用于缓冲 binlog ,该内存叫 binlog cache,参数 binlog_cache_size 用于控制单个线程内 binlog cache 所占内存的大小。如果超过了这个参数规定的大小,就要暂存到磁盘。
# binlog cache什么时候写到binlog 文件
在事务提交的时候,执行器把bin log cache里面的完整事务写入到binlog文件中,并清空bin log cache。
虽然每个线程都有自己的binlog cache。但是最终都写入到同一个binlog 文件:
- 图中的write,指的就是把日志写入到binlog文件,但是并没有把数据持久化到磁盘,因为数据还缓存在文件系统的page chche里,write的写入速度还是比较快的。因为不涉及磁盘I/O
- 图中的fsync,才是将数据持久化到磁盘的操作,这里就会设计磁盘IO。
MySQL提供一个Sync_binlog参数来控制数据库的binlog刷到磁盘上的频率
- sync_binlog=0的时候,表示每次提交事务都只wirte,不fsync,后序由操作系统决定何时将数据持久化到磁盘
- sync_binlog=1的时候,表示每次提交事务都会write,然后马上执行fsync;
- sync——binlog=N N>1的时候,表示每次提交事务都write,但是积累N个事务后才fsync。
MySQL中系统默认设置的是0,这时候性能最好但是风险也最大。一旦主机发生异常。会造成数据丢失。
当设置为1时。是最安全但是性能损耗最大的时候。
如果能容少量事务的 binlog 日志丢失的风险,为了提高写入的性能,一般会 sync_binlog 设置为 100~1000 中的某个数值。
本文主要总结一下MySQL当中的redo log、undo log、和binlog。后面会介绍一下在一个SQL当中这几个日志具体的应用过程。