MySQL 中事务的原子性是通过 undo log 来实现的,事务的持久性是通过 redo log 来实现的,事务的隔离性是通过读写锁+MVCC 来实现的。
在事务的具体实现机制上,MySQL 采用的是 WAL(Write-ahead logging,预写 式日志)机制来实现的。在使用 WAL 的系统中,所有的修改都先被写入到日志中,然后再被应用到系 统中。通常包含 redo 和 undo 两部分信息。
使用了 WAL, 在重启之后系统可以通过比较日志和系统状态来决定是继续完成操作还是 撤销操作。
redo log 称为重做日志,每当有操作时,在数据变更之前将操作写入 redo log, 这样当发生掉电之类的情况时系统可以在重启后继续操作。 undo log 称为撤销日志,当一些变更执行到一半无法完成时,可以根据撤销日 志恢复到变更之间的状态。
Commit Logging 和 Shadow Paging 事务的 日志类型的 实现除了 WAL ( Write-ahead logging ,预写式日志) 外,还 有“ Commit Logging ”( 提 交 日 志 ),这种方式 只有在日志记录全部都安全落盘, 数据库在日志中看到代表事务成功提交的“提交记录”( Commit Record )后, 才会根据日志上的信息对真正的数据进行修改,修改完成后,再在日志中加入一条“结束记录”( End Record )表示事务已完成持久化 。
两者的区别是, WAL允许在事务提交之前,提前写入变动数据 ,而 Commit Logging
则不行 ; WAL中有undo日志, Commit Logging没有 。阿里的 Oceanbase则是使用的 Commit Logging来实现事务。
redo 日志
redo 日志的作用
我们进行的增删改查操作 其实本质上都是在访问页面(包括读页面、写页面、创建新页面等操作)。在 Buffer Pool 的时候说过,在真正访问页面之前,需要把在磁盘上的页缓存到内存 中的 Buffer Pool 之后才可以访问。只在内存的 Buffer Pool 中修改了页面,假设在事务提交后突然发生 了某个故障,导致内存中的数据都失效了,那么这个已经提交了的事务对数据库 中所做的更改也就跟着丢失了,在事务提交完成之前把该事务所修改的所有页面 都刷新到磁盘,但是这个简单粗暴的做法有些问题:
1、刷新一个完整的数据页太浪费了
2、随机 IO 刷起来比较慢
这样我们在事务提交时,把上述内容刷新到磁盘中,即使之后系统崩溃了, 重启之后只要按照上述内容所记录的步骤重新更新一下数据页,那么该事务对数 据库中所做的修改又可以被恢复出来,也就意味着满足持久性的要求。因为在系 统崩溃重启时需要按照上述内容所记录的步骤重新更新数据页,所以上述内容也 被称之为重做日志
1、redo 日志占用的空间非常小 存储表空间 ID、页号、偏移量以及需要更新的值所需的存储空间是很小的。
2、redo 日志是顺序写入磁盘的 在执行事务的过程中,每执行一条语句,就可能产生若干条 redo 日志,这些 日志是按照产生的顺序写入磁盘的,也就是使用顺序 IO。
redo 日志格式
通过上边的内容我们知道,redo 日志本质上只是记录了一下事务对数据库做 了哪些修改。 InnoDB 们针对事务对数据库的不同修改场景定义了多种类型的 redo 日志,但是绝大部分类型的 redo 日志都有下边这种通用的结构:
各个部分的详细释义如下: type:该条 redo 日志的类型,redo 日志设计大约有 53 种不同的类型日志。 space ID:表空间 ID。 page number:页号。 data:该条 redo 日志的具体内容。
redo 日志中只需 要记录一下在某个页面的某个偏移量处修改了几个字节的值,具体被修改的内容 是啥就好了,InnoDB 把这种极其简单的 redo 日志称之为物理日志,并且根据在 页面中写入数据的多少划分了几种不同的 redo 日志类型:
redo 日志的写入过程
redo log block 和 日志缓冲区
InnoDB 为了更好的进行系统崩溃恢复,把 redo 日志都放在了大小为 512 字节 的块(block)中。 写 入 redo 日志时也不能直接写到磁盘上,实际上在服务器启动时就向操作系统申请了一大片称之为 redo log buffer 的连续内存空间,翻译成中文就是 redo 日志缓冲区,我们也可以简称为 log buffer。这片内存空间被划分成若干个连续的 redo log block,我们可以通过启动参数 innodb_log_buffer_size 来指定 log buffer 的大小,该启动参数的默认值为 16MB。 向log buffer中写入redo日志的过程是顺序的,也就是先往前边的block中写, 当该 block 的空闲空间用完之后再往下一个 block 中写。
redo日志刷 盘时机
我们前边说 redo 日志在内存中有个 log buffer,可是这些日志总在内存里呆着 也不是个办法,在一些情况下它们会被刷新到磁盘里,比如:
1、log buffer 空间不足时,log buffer 的大小是有限的(通过系统变量 innodb_log_buffer_size 指定),如果不停的往这个有限大小的 log buffer 里塞入日志,很快它就会被填满。InnoDB 认为如果当前写入 log buffer 的 redo 日志量已 经占满了 log buffer 总容量的大约一半左右,就需要把这些日志刷新到磁盘上。
2、事务提交时,我们前边说过之所以使用 redo 日志主要是因为它占用的空间 少,还是顺序写,在事务提交时可以不把修改过的 Buffer Pool 页面刷新到磁盘, 但是为了保证持久性,必须要把修改这些页面对应的 redo 日志刷新到磁盘。
3、后台有一个线程,大约每秒都会刷新一次 log buffer 中的 redo 日志到磁盘。
4、正常关闭服务器时等等。
redo 日志文件组
MySQL 的数据目录(使用 SHOW VARIABLES LIKE 'datadir'查看)下默认有两个 名为 ib_logfile0 和 ib_logfile1 的文件,log buffer 中的日志默认情况下就是刷新到 这两个磁盘文件中。如果我们对默认的 redo 日志文件不满意,可以通过下边几 个启动参数来调节: innodb_log_group_home_dir,该参数指定了 redo 日志文件所在的目录,默认 值就是当前的数据目录。
innodb_log_file_size, 该参数指定了每个 redo 日志文件的大小,默认值为 48MB, innodb_log_files_in_group,该参数指定 redo 日志文件的个数,默认值为 2, 最大值为 100。 所以磁盘上的 redo 日志文件可以不只一个,而是以一个日志文件组的形式出 现的。这些文件以 ib_logfile[数字](数字可以是 0、1、2...)的形式进行命名。 在将 redo 日志写入日志文件组时,是从 ib_logfile0 开始写,如果 ib_logfile0 写满 了,就接着 ib_logfile1 写,同理,ib_logfile1 写满了就去写 ib_logfile2,依此类推。 如果写到最后一个文件该咋办?那就重新转到 ib_logfile0 继续写。 既然 Redo log 文件是循环写入的,在覆盖写之前,总是要保证对应的脏页已 经刷到了磁盘。在非常大的负载下,为避免错误的覆盖,InnoDB 会强制的 flush 脏页。
redo日志文件格式
我们前边说过log buffer本质上是一片连续的内存空间,被划分成了若干个512 字节大小的 block。将 log buffer 中的 redo 日志刷新到磁盘的本质就是把 block 的 镜像写入日志文件中,所以 redo 日志文件其实也是由若干个 512 字节大小的 block 组成。 redo 日志文件组中的每个文件大小都一样,格式也一样,都是由两部分组成: 前 2048 个字节,也就是前 4 个 block 是用来存储一些管理信息的。 从第 2048 字节往后是用来存储 log buffer 中的 block 镜像的。
Log Sequence Number
自系统开始运行,就不断的在修改页面,也就意味着会不断的生成 redo 日志。 redo 日志的量在不断的递增,就像人的年龄一样,自打出生起就不断递增,永 远不可能缩减了。 InnoDB 为记录已经写入的 redo 日志量,设计了一个称之为 Log Sequence Number 的全局变量,翻译过来就是:日志序列号,简称 LSN。规定初始的 lsn 值为 8704(也就是一条 redo 日志也没写入时,LSN 的值为 8704)。 redo 日志都有一个唯一的 LSN 值与其对应,LSN 值越小,说明 redo 日志产生 的越早。
flushed_to_disk_lsn
InnoDB 也有一个表示刷新到磁盘中的 redo 日志量的全局变量,称之为 flushed_to_disk_lsn,系统第一次启动时,该变量的 值和初始的 lsn 值是相同的,都是 8704。随着系统的运行,redo 日志被不断写入 log buffer,但是并不会立即刷新到磁盘,lsn 的值就和 flushed_to_disk_lsn 的值拉 开了差距
当有新的 redo 日志写入到 log buffer 时,首先 lsn 的值会增长,但 flushed_to_disk_lsn 不变,随后随着不断有 log buffer 中的日志被刷新到磁盘上, flushed_to_disk_lsn 的值也跟着增长。如果两者的值相同时,说明 log buffer 中的 所有 redo 日志都已经刷新到磁盘中了。
innodb_flush_log_at_trx_commit 的用法
我们前边说为了保证事务的持久性,用户线程在事务提交时需要将该事务执 行过程中产生的所有 redo 日志都刷新到磁盘上。会很明显的降低数据库性能。 如果对事务的持久性要求不是那么强烈的话,可以选择修改一个称为 innodb_flush_log_at_trx_commit 的系统变量的值,该变量有 3 个可选的值:
0:当该系统变量值为0时,表示在事务提交时不立即向磁盘中同步redo日志, 这个任务是交给后台线程做的。 这样很明显会加快请求处理速度,但是如果事务提交后服务器挂了,后台线 程没有及时将 redo 日志刷新到磁盘,那么该事务对页面的修改会丢失。
1:当该系统变量值为 1 时,表示在事务提交时需要将 redo 日志同步到磁盘, 可以保证事务的持久性。1 也是 innodb_flush_log_at_trx_commit 的默认值。
2:当该系统变量值为 2 时,表示在事务提交时需要将 redo 日志写到操作系统 的缓冲区中,但并不需要保证将日志真正的刷新到磁盘。 这种情况下如果数据库挂了,操作系统没挂的话,事务的持久性还是可以保 证的,但是操作系统也挂了的话,那就不能保证持久性了。
undo log/undo
为了回滚而记录的这些东西称之为撤销日志
事务id
给事务分配 id的时机
一个事务可以是一个只读事务,或者是一个读写事务:
我们可以通过 START TRANSACTION READ ONLY 语句开启一个只读事务。 在只读事务中不可以对普通的表(其他事务也能访问到的表)进行增、删、 改操作,但可以对用户临时表做增、删、改操作。
我们可以通过 START TRANSACTION READ WRITE 语句开启一个读写事务,或者 使用 BEGIN、START TRANSACTION 语句开启的事务默认也算是读写事务。 在读写事务中可以对表执行增删改查操作。 如果某个事务执行过程中对某个表执行了增、删、改操作,那么 InnoDB 存储 引擎就会给它分配一个独一无二的事务 id,
分配方式如下: 对于只读事务来说,只有在它第一次对某个用户创建的临时表执行增、删、 改操作时才会为这个事务分配一个事务 id,否则的话是不分配事务 id 的。 我们前边说过对某个查询语句执行 EXPLAIN 分析它的查询计划时,有时候在 Extra 列会看到 Using temporary 的提示,这个表明在执行该查询语句时会用到内 部临时表。这个所谓的内部临时表和我们手动用 CREATE TEMPORARY TABLE 创建 的用户临时表并不一样,在事务回滚时并不需要把执行 SELECT 语句过程中用到 的内部临时表也回滚,在执行 SELECT 语句用到内部临时表时并不会为它分配事 务 id。 对于读写事务来说,只有在它第一次对某个表(包括用户创建的临时表)执 行增、删、改操作时才会为这个事务分配一个事务 id,否则的话也是不分配事务 id 的。 有的时候虽然我们开启了一个读写事务,但是在这个事务中全是查询语句, 并没有执行增、删、改的语句,那也就意味着这个事务并不会被分配一个事务 id。 上边描述的事务 id 分配策略是针对 MySQL 5.7 来说的,前边的版本的分配方 式可能不同。
事务 id生成机制
这个事务 id 本质上就是一个数字,它的分配策略和我们前边提到的对隐藏列 row_id(当用户没有为表创建主键和 UNIQUE 键时 InnoDB 自动创建的列)的分 配策略大抵相同,具体策略如下: 服务器会在内存中维护一个全局变量,每当需要为某个事务分配一个事务 id 时,就会把该变量的值当作事务 id 分配给该事务,并且把该变量自增 1。 每当这个变量的值为 256 的倍数时,就会将该变量的值刷新到系统表空间的 页号为 5 的页面中一个称之为 Max Trx ID 的属性处,这个属性占用 8 个字节的存 储空间。 当系统下一次重新启动时,会将上边提到的 Max Trx ID 属性加载到内存中,将 该值加上 256 之后赋值给我们前边提到的全局变量(因为在上次关机时该全局变 量的值可能大于 Max Trx ID 属性值)。 这样就可以保证整个系统中分配的事务 id 值是一个递增的数字。先被分配 id 的事务得到的是较小的事务 id,后被分配 id 的事务得到的是较大的事务 id。
trx_id 隐藏列
我们在学习 InnoDB 记录行格式的时候重点强调过:聚簇索引的记录除了会保 存完整的用户数据以外,而且还会自动添加名为 trx_id、roll_pointer 的隐藏列,
其中的 trx_id 列就是某个对这个聚簇索引记录做改动的语句所在的事务对应 的事务 id 而已(此处的改动可以是 INSERT、DELETE、UPDATE 操作)。
如果用户没有在表中定义主键以及 UNIQUE 键,还会自动添加一个名为 row_id 的隐藏列。
undo 日志的格式
为了实现事务的原子性,InnoDB 存储引擎在实际进行增、删、改一条记录时, 都需要先把对应的 undo 日志记下来。一般每对一条记录做一次改动,就对应着 一条 undo 日志,但在某些更新记录的操作中,也可能会对应着 2 条 undo 日志。 一个事务在执行过程中可能新增、删除、更新若干条记录,也就是说需要记 录很多条对应的 undo 日志,这些 undo 日志会被从 0 开始编号,也就是说根据 生成的顺序分别被称为第 0 号 undo 日志、第 1 号 undo 日志、...、第 n 号 undo 日志等,这个编号也被称之为 undo NO。 我们前边说明表空间的时候说过,表空间其实是由许许多多的页面构成的, 页面默认大小为 16KB。这些页面有不同的类型,其中有一种称之为 FIL_PAGE_UNDO_LOG 类型的页面是专门用来存储 undo 日志的。也就是说 Undo page 跟储存的数据和索引的页等是类似的。 FIL_PAGE_UNDO_LOG 页面可以从系统表空间中分配,也可以从一种专门存放 undo 日志的表空间,也就是所谓的 undo tablespace 中分配。先来看看不同操作 都会产生什么样子的 undo 日志。
INSERT操作对应的 undo日志
当我们向表中插入一条记录时最终导致的结果就是这条记录被放到了一个数 据页中。如果希望回滚这个插入操作,那么把这条记录删除就好了,也就是说在 写对应的 undo 日志时,主要是把这条记录的主键信息记上。InnoDB 的设计了一 个类型为 TRX_UNDO_INSERT_REC 的 undo 日志。 当我们向某个表中插入一条记录时,实际上需要向聚簇索引和所有的二级索 引都插入一条记录。不过记录 undo 日志时,我们只需要考虑向聚簇索引插入记 录时的情况就好了,因为其实聚簇索引记录和二级索引记录是一一对应的,我们 在回滚插入操作时,只需要知道这条记录的主键信息,然后根据主键信息做对应 的删除操作,做删除操作时就会顺带着把所有二级索引中相应的记录也删除掉。 后边说到的DELETE 操作和UPDATE 操作对应的 undo 日志也都是针对聚簇索引记 录而言的。
roll_pointer 的作用
roll_pointer 本质上就是一个指向记录对应的 undo 日志的一个指针。比方说我 们向表里插入了 2 条记录,每条记录都有与其对应的一条 undo 日志。记录被存 储到了类型为 FIL_PAGE_INDEX 的页面中(就是我们前边一直所说的数据页), undo 日志被存放到了类型为 FIL_PAGE_UNDO_LOG 的页面中。roll_pointer 本质就 是一个指针,指向记录对应的 undo 日志。
DELETE操作对应的 undo日志
我们知道插入到页面中的记录会根据记录头信息中的 next_record 属性组成一 个单向链表,我们把这个链表称之为正常记录链表;被删除的记录其实也会根据 记录头信息中的 next_record 属性组成一个链表,只不过这个链表中的记录占用 的存储空间可以被重新利用,所以也称这个链表为垃圾链表。Page Header 部分 有一个称之为 PAGE_FREE 的属性,它指向由被删除记录组成的垃圾链表中的头 节点。 假设此刻某个页面中的记录分布情况是这样的
我们只把记录的 delete_mask 标志位展示了出来。从图中可以看出,正常记录 链表中包含了 3 条正常记录,垃圾链表里包含了 2 条已删除记录。页面的 Page Header 部分的 PAGE_FREE 属性的值代表指向垃圾链表头节点的指针。 假设现在我们准备使用 DELETE 语句把正常记录链表中的最后一条记录给删除 掉,其实这个删除的过程需要经历两个阶段:
阶段一:将记录的 delete_mask 标识位设置为 1,这个阶段称之为 delete mark。
为啥会有这种奇怪的中间状态呢?其实主要是为了实现 MVCC 的功能。
阶段二:当该删除语句所在的事务提交之后,会有专门的线程后来真正的把 记录删除掉。所谓真正的删除就是把该记录从正常记录链表中移除,并且加入到垃圾链表中,然后还要调整一些页面的其他信息,比如页面中的用户记录数量 PAGE_N_RECS、上次插入记录的位置 PAGE_LAST_INSERT、垃圾链表头节点的指 针 PAGE_FREE、页面中可重用的字节数量 PAGE_GARBAGE、还有页目录的一些信 息等等。这个阶段称之为 purge。 把阶段二执行完了,这条记录就算是真正的被删除掉了。这条已删除记录占 用的存储空间也可以被重新利用了。
从上边的描述中我们也可以看出来,在删除语句所在的事务提交之前,只会经历阶段一,也就是 delete mark 阶段(提交之后我们就不用回滚了,所以只需考虑对删除操作的阶段一做的影响进行回滚)。InnoDB 中就会产生一种称之为 TRX_UNDO_DEL_MARK_REC 类型的 undo 日志。
UPDATE操作对应的 undo日志
在执行 UPDATE 语句时,InnoDB 对更新主键和不更新主键这两种情况有截然 不同的处理方案。
不更新主键的情况 在不更新主键的情况下,又可以细分为被更新的列占用的存储空间不发生变 化和发生变化的情况。
就地更新( in-place update) 更新记录时
对于被更新的每个列来说,如果更新后的列和更新前的列占用 的存储空间都一样大,那么就可以进行就地更新,也就是直接在原记录的基础上 修改对应列的值。再次强调一边,是每个列在更新前后占用的存储空间一样大, 有任何一个被更新的列更新前比更新后占用的存储空间大,或者更新前比更新后 占用的存储空间小都不能进行就地更新。
先删除掉旧记录,再插入新记录 在不更新主键的情况下,
如果有任何一个被更新的列更新前和更新后占用的 存储空间大小不一致,那么就需要先把这条旧的记录从聚簇索引页面中删除掉, 然后再根据更新后列的值创建一条新的记录插入到页面中。 请注意一下,我们这里所说的删除并不是 delete mark 操作,而是真正的删除 掉,也就是把这条记录从正常记录链表中移除并加入到垃圾链表中,并且修改页 面中相应的统计信息(比如 PAGE_FREE、PAGE_GARBAGE 等这些信息)。由用户 线程同步执行真正的删除操作,真正删除之后紧接着就要根据各个列更新后的值 创建的新记录插入。 这里如果新创建的记录占用的存储空间大小不超过旧记录占用的空间,那么 可以直接重用被加入到垃圾链表中的旧记录所占用的存储空间,否则的话需要在 页面中新申请一段空间以供新记录使用,如果本页面内已经没有可用的空间的话, 那就需要进行页面分裂操作,然后再插入新记录。 针对 UPDATE 不更新主键的情况(包括上边所说的就地更新和先删除旧记录再 插入新记录),InnoDB 设计了一种类型为 TRX_UNDO_UPD_EXIST_REC 的 undo 日 志。
更新主键的情况
在聚簇索引中,记录是按照主键值的大小连成了一个单向链表的,如果我们 更新了某条记录的主键值,意味着这条记录在聚簇索引中的位置将会发生改变, 比如你将记录的主键值从 1 更新为 10000,如果还有非常多的记录的主键值分布 在 1 ~ 10000 之间的话,那么这两条记录在聚簇索引中就有可能离得非常远,甚 至中间隔了好多个页面。针对 UPDATE 语句中更新了记录主键值的这种情况, InnoDB 在聚簇索引中分了两步处理:
将旧记录进行 delete mark操作
也就是说在 UPDATE 语句所在的事务提交前,对旧记录只做一个 delete mark 操作,在事务提交后才由专门的线程做 purge 操作,把它加入到垃圾链表中。这里一定要和我们上边所说的在不更新记录主键值时,先真正删除旧记录,再插入 新记录的方式区分开! 之所以只对旧记录做 delete mark 操作,是因为别的事务同时也可能访问这条 记录,如果把它真正的删除加入到垃圾链表后,别的事务就访问不到了。这个功 能就是所谓的 MVCC。
创建一条新记录
根据更新后各列的值创建一条新记录,并将其插入到聚簇索引中(需重新定 位插入的位置)。 由于更新后的记录主键值发生了改变,所以需要重新从聚簇索引中定位这条 记录所在的位置,然后把它插进去。 针对 UPDATE 语句更新记录主键值的这种情况,在对该记录进行 delete mark 操作前,会记录一条类型为 TRX_UNDO_DEL_MARK_REC 的 undo 日志;之后插入 新记录时,会记录一条类型为 TRX_UNDO_INSERT_REC 的 undo 日志,也就是说 每对一条记录的主键值做改动时,会记录 2 条 undo 日志。
总结事务的流程
总的来说,事务流程分为事务的执行流程和事务恢复流程
事务执行 我们已经知道了 MySQL 的事务主要主要是通过 Redo Log 和 Undo Log 实现的。
可以看出,MySQL 在事务执行的过程中,会记录相应 SQL 语句的 UndoLog 和 Redo Log,然后在内存中更新数据并形成数据脏页。接下来 RedoLog 会根据一定 规则触发刷盘操作,Undo Log 和数据脏页则通过刷盘机制刷盘。事务提交时, 会将当前事务相关的所有 Redo Log 刷盘,只有当前事务相关的所有 Redo Log 刷 盘成功,事务才算提交成功。
事务恢复
如果一切正常,则 MySQL 事务会按照上图中的顺序执行。如果 MySQL 由于某 种原因崩溃或者宕机,当然进行数据的恢复或者回滚操作。 如果事务在执行第 8 步,即事务提交之前,MySQL 崩溃或者宕机,此时会先使用 Redo Log 恢复数据,然后使用 Undo Log 回滚数据。 如果在执行第8步之后MySQL崩溃或者宕机,此时会使用Redo Log恢复数据, 大体流程如下图所示。
很明显,MySQL 崩溃恢复后,首先会获取日志检查点信息,随后根据日志检 查点信息使用 Redo Log 进行恢复。MySQL 崩溃或者宕机时事务未提交,则接下 来使用 Undo Log 回滚数据。如果在 MySQL 崩溃或者宕机时事务已经提交,则用 Redo Log 恢复数据即可。
恢复机制
在服务器不挂的情况下,redo 日志简直就是个大累赘,不仅没用,反而让性 能变得更差。但是万一数据库挂了,就可以在重启时根据 redo 日志中的记录就 可以将页面恢复到系统崩溃前的状态。 MySQL 可以根据 redo 日志中的各种 LSN 值,来确定恢复的起点和终点。然后 将 redo 日志中的数据,以哈希表的形式,将一个页面下的放到哈希表的一个槽 中。之后就可以遍历哈希表,因为对同一个页面进行修改的 redo 日志都放在了 一个槽里,所以可以一次性将一个页面修复好(避免了很多读取页面的随机IO)。
并且通过各种机制,避免无谓的页面修复,比如已经刷新的页面,进而提升崩溃 恢复的速度。
崩溃后的恢复为什么不用 binlog ?
1、这两者使用方式不一样 binlog 会记录表所有更改操作,包括更新删除数据,更改表结构等等,主要 用于人工恢复数据,而 redo log 对于我们是不可见的,它是 InnoDB 用于保证 crash-safe 能力的,也就是在事务提交后 MySQL 崩溃的话,可以保证事务的持久 性,即事务提交后其更改是永久性的。 一句话概括:binlog 是用作人工恢复数据,redo log 是 MySQL 自己使用,用 于保证在数据库崩溃时的事务持久性。
2、redo log 是 InnoDB 引擎特有的,binlog 是 MySQL 的 Server 层实现的, 所有引擎都可以使用。
3、redo log 是物理日志,记录的是“在某个数据页上做了什么修改”,恢复 的速度更快;binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如“给 ID=2 这的 c 字段加 1 ” ;
4、redo log 是“循环写”的日志文件,redo log 只会记录未刷盘的日志,已 经刷入磁盘的数据都会从 redo log 这个有限大小的日志文件里删除。binlog 是 追加日志,保存的是全量的日志。
5、最重要的是,当数据库 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日志和 Undo 日志的关系
数据库崩溃重启后,需要先从 redo log 中把未落盘的脏页数据恢复回来,重新 写入磁盘,保证用户的数据不丢失。当然,在崩溃恢复中还需要把未提交的事务 进行回滚操作。由于回滚操作需要 undo log 日志支持,undo log 日志的完整性和 可靠性需要 redo log 日志来保证,所以数据库崩溃需要先做 redo log 数据恢复, 然后做 undo log 回滚。
在事务执行过程中,除了记录 redo 一些记录,还会记录 undo log 日志。Undo log 记录了数据每个操作前的状态,如果事务执行过程中需要回滚,就可以根据 undo log 进行回滚操作。 因为 redo log 是物理日志,记录的是数据库页的物理修改操作。所以 undo log (可以看成数据库的数据)的写入也会伴随着 redo log 的产生,这是因为 undo log 也需要持久化的保护。 事务进行过程中,每次 sql 语句执行,都会记录 undo log 和 redo log,然后更 新数据形成脏页。事务执行 COMMIT 操作时,会将本事务相关的所有 redo log 进行落盘,只有所有的 redo log 落盘成功,才算 COMMIT 成功。然后内存中的 undo log 和脏页按照同样的规则进行落盘。如果此时发生崩溃,则只使用 redo log 恢复数据。
同时写Redo 和Binlog 怎么保持一致?
当我们开启了 MySQL 的 BinLog 日志,很明显需要保证 BinLog 和事务日志的一 致性,为了保证二者的一致性,使用了两阶段事务 2PC(所谓的两个阶段是指:
第一阶段:准备阶段
第二阶段:提交阶段,具体的内容请参考分布式事务的相 关的课程内容)。
步骤如下:
1)当事务提交时 InnoDB 存储引擎进行 prepare 操作。
2)MySQL 上层会将数据库、数据表和数据表中的数据的更新操作写入 BinLog 文件。
3)InnoDB 存储引擎将事务日志写入 Redo Log 文件中。



