概述
事务有4种特性:原子性、一致性、隔离性和持久性
。那么事务的四种特性到底是基于什么机制实现呢?
- 事务的隔离性由
锁机制
实现。 - 而事务的原子性、一致性和持久性由事务的
redo 日志
和undo 日志
来保证。undo log(回滚日志)
:是 Innodb存储引擎层
生成的日志,实现了事务中的原子性,主要用于事务回滚和 MVCC。(回滚行记录到某个特定版本,用来保证事务的原子性、一致性。)redo log(重做日志)
:是 Innodb存储引擎层
生成的日志,实现了事务中的持久性,主要用于掉电等故障恢复;(提供再写入操作,恢复提交事务修改的页操作,用来保证事务 的持久性。)binlog (归档日志)
:是Server 层
生成的日志,主要用于数据备份和主从复制;
redo log(引擎层)
1. 为什么使用redo log
MySQL是先把磁盘上的数据加载到内存中,在内存中对数据进行修改,再写回到磁盘上。如果此时突然宕机,内存中的数据就会丢失。 怎么解决这个问题? 简单啊,事务提交前直接把数据写入磁盘就行啊。 这么做有什么问题?
- 只修改一个页面里的一个字节,就要将整个页面刷入磁盘,太浪费资源了。毕竟一个页面16kb大小,你只改其中一点点东西,就要将16kb的内容刷入磁盘,听着也不合理。
- 毕竟一个事务里的SQL可能牵涉到多个数据页的修改,而这些数据页可能不是相邻的,也就是属于随机IO。显然操作随机IO,速度会比较慢。
于是,决定采用redo log解决上面的问题。当做数据修改的时候,不仅在内存中操作,还会在redo log中记录这次操作。当事务提交的时候,会将redo log日志进行刷盘(redo log一部分在内存中,一部分在磁盘上)。当数据库宕机重启的时候,会将redo log中的内容恢复到数据库中,再根据undo log和binlog内容决定回滚数据还是提交数据。
2. redo log 优点
redo日志占用的空间非常小
,只是记录哪一页修改了啥,修改什么内容是不知道的。体积小,刷盘快,降低刷盘频率。(具体数据是回滚还是提交,再根据undo log和redo log内容决定是回滚数据还是提交数据)redo日志是顺序写入磁盘的
,它是一直往末尾进行追加,属于顺序IO。事务执行过程中,redo log不断记录
3. redo log 流程
第1步:先将原始数据从磁盘中读入内存中来,修改数据的内存拷贝
第2步:生成一条重做日志并写入redo log buffer,记录的是数据被修改后的值(此时并不会直接将redo log buffer的内容进行刷盘,而是放在了操作系统的缓冲区中,由操作系统进行系统调用fsync()
函数决定什么时候刷盘)
第3步:当事务commit时,将redo log buffer中的内容刷新到 redo log file,对 redo log file采用追加写的方式
第4步:定期将内存中修改的数据刷新到磁盘中
Write-Ahead Log(预先日志持久化):在持久化一个数据页之前,先将内存中相应的日志页持久化(MySQL 的写操作并不是立刻写到磁盘上,而是先写日志,然后在合适的时间再写到磁盘上。)。
被修改 Undo 页面,需要记录对应 redo log 吗?
- 开启事务后,InnoDB 层更新记录前,首先要记录相应的 undo log,如果是更新操作,需要把被更新的列的旧值记下来,也就是要生成一条 undo log,undo log 会写入 Buffer Pool 中的 Undo 页面。不过,在内存修改该 Undo 页面后,需要记录对应的 redo log。
4. 什么时候刷盘
刷盘是指从redo log buffer到redo log file中,只要这个过程不出问题,就不会有事
- 首先是从
redo log buffer
刷到文件系统缓存(page cache)
中去,然后由操作系统判断什么时候刷盘。
InnoDB给出 innodb_flush_log_at_trx_commit
参数,该参数控制 commit提交事务时,如何将 redo log buffer 中的日志刷新到 redo log file 中。它支持三种策略:
设置为0
:表示每次事务提交时不进行刷盘操作。(系统默认master thread每隔1s进行一次重做日
志的同步)设置为1
:表示每次事务提交时都将进行同步,刷盘操作(默认值
)设置为2
:表示每次事务提交时都只把 redo log buffer 内容写入 page cache,不进行同步。由os自
己决定什么时候同步到磁盘文件。
redo log 文件写满怎么办?
「重做日志文件组」由有 2 个 redo log 文件组成
ib_logfile0 和 ib_logfile1
。- redo log File 设置的上限是 1 GB(总共2GB)
- 重做日志文件组是以循环写的方式工作的,从头开始写,写到末尾就又回到开头
流程
:
- InnoDB 存储引擎会先写 ib_logfile0 文件,当 ib_logfile0 文件被写满的时候,会切换至 ib_logfile1 文件,当 ib_logfile1 文件也被写满时,MySQL 会被阻塞
- 会停下来将 Buffer Pool 中的脏页刷新到磁盘中,然后标记 redo log 哪些记录可以被擦除,接着对旧的 redo log 记录进行擦除,等擦除完旧记录腾出了空间,继续执行新的更新操作。
undo log(引擎层)
- redo log是事务持久性的保证,undo log是事务原子性的保证。在事务中
更新数据
的前置操作
其实是要先写入一个undo log
。
1. 为什么使用undo log
事务需要保证原子性
,也就是事务中的操作要么全部完成,要么什么也不做。但有时候事务执行到一半会出现一些情况,比如:
- 情况一:事务执行过程中可能遇到各种错误,比如
服务器本身的错误 , 操作系统错误
,甚至是突然 断电 导致的错误。 - 情况二:程序员可以在事务执行过程中手动输入
ROLLBACK
语句结束当前事务的执行。
以上情况出现,我们需要把数据改回原先的样子,这个过程称之为 回滚
,这样就可以造成一个假象:这个事务看起来什么都没做,所以符合 原子性
要求。
一条记录的每一次更新操作产生的 undo log 格式都有一个 roll_pointer 指针和一个 trx_id 事务id:
- 通过
trx_id
可以知道该记录是被哪个事务修改的; - 通过
roll_pointer
指针可以将这些 undo log 串成一个链表,这个链表就被称为版本链;
2. undo log作用
作用1:回滚数据,保证事务的原子性
- undo是
逻辑日志
,因此只是将数据库从逻辑上恢复到原来的样子,但是物理层面做了一些改变是不会恢复的(例如创建了一个页,里面插入一个数据,然后回滚是删除该条数据,但是整个页是不会删除的)
作用2:MVCC
- innodb存储引擎中MVCC的实现是通过undo来完成。当用户读取一行记录时,若该条记录已经被其他事务占用,
当前事务可以通过undo读取之前的版本信息
,以此实现非锁定读取
3. undo log 流程
- 行格式有三个隐藏的列:
row_id
:行ID,唯一标识一条记录(如果没有定义主键,也没有定义唯一索引,那么innodb会自动为表添加一个row_id的隐藏列作为主键)transaction_id
:事务ID(每个事务都会分配一个事务ID,当对某条记录发生变更时,就会将这个事务的事务ID写入trx_id中)roll_pointer
::回滚指针(本质上是指向undo log的指针)
undo log是如何删除的
针对于insert undo log
- 因为insert操作的记录,只对事务本身可见,对其他事务不可见。故该undo log可以在事务提交后直接删除,不需要进行purge操作。
针对于update undo log
- 该undo log可能需要提供MVCC机制,因此不能在事务提交时就进行删除。提交时放入undo log链表,等待purge线程进行最后的删除。
总结
:
undo log是逻辑日志
,对事务回滚时,只是将数据库逻辑地恢复到原来的样子。redo log是物理日志
,记录的是数据页的物理变化,undo log不是redo log的逆过程。
4. undo log如何刷盘
- undo log 和数据页的刷盘策略是一样的,都需要通过 redo log 保证持久化。
- buffer pool 中有 undo 页,对 undo 页的修改也都会记录到 redo log。redo log 会每秒刷盘,提交事务时也会刷盘,数据页和 undo 页都是靠这个机制保证持久化的。
bin log(数据层)
- 主要实现主从同步,主机的数据会写到bin log中,从机拿到bin log日志进行同步,实现主从同步。
binlog即binary log,二进制日志文件,也叫作变更日志(update log)。它记录了数据库所有执行的
DDL
和 DML
等数据库更新事件的语句,但是不包含没有修改任何数据的语句(如数据查询语句select、show等)。
binlog主要应用场景:
数据恢复
:如果MySQL数据库意外停止,可以通过二进制日志文件来查看用户执行了哪些操作,对数据库服务器文件做了哪些修改,然后根据二进制日志文件中的记录来恢复数据库服务器。数据复制
:由于日志的延续性和时效性,master把它的二进制传递给slaves来达到master-salve数据一致的目的。
写入日志流程
-
事务执行过程中,
先把日志写到 binlog cache ,事务提交的时候,再把binlog cache写到binlog文件中。
因为一个事务的binlog不能被拆开,无论这个事务多大,也要确保一次性写入,所以系统会给每个线程分配一个块内存作为binlog cache。 -
write
,指的就是指把日志写入到 binlog 文件,但是并没有把数据持久化到磁盘,因为数据还缓存在文件系统的 page cache 里,write 的写入速度还是比较快的,因为不涉及磁盘 I/O。 -
fsync
,才是将数据持久化到磁盘的操作,这里就会涉及磁盘 I/O,所以频繁的 fsync 会导致磁盘的 I/O 升高。 -
write和fsync的时机,可以由参数 sync_binlog 控制,默认是 0 。为0的时候,表示每次提交事务都只write,由系统自行判断什么时候执行fsync。虽然性能得到提升,但是机器宕机,page cache里面的binglog 会丢失。如下图
binlog与redolog对比
1.适用对象不同:
redo log 它是 物理日志
,记录内容是“在某个数据页上做了什么修改”,属于 InnoDB存储引擎层
产生的。(真实记录在物理磁盘页的)binlog 是 逻辑日志
,记录内容是语句的原始逻辑,类似于“给 ID=2 这一行的 c 字段加 1”,属于MySQL Server 层
,所有存储引擎都可以使用;
2.文件格式不同:
- binlog 有 3 种格式类型,分别是 STATEMENT(默认格式)、ROW、 MIXED
- redo log 是物理日志,记录的是在某个数据页做了什么修改,比如对 XXX 表空间中的 YYY 数据页 ZZZ 偏移量的地方做了AAA 更新;
3.用途不同:
- binlog 用于备份恢复、主从复制;
- redo log 用于掉电等故障恢复。
redo log在事务执行的过程中不断写入,而binlog只有在提交事务时才写入,所以redolog与binlog的写入时机不一样
4.写入方式不同
- binlog 是
追加写
,写满一个文件,就创建一个新的文件继续写,不会覆盖以前的日志,保存的是全量的日志。 - redo log 是
循环写
,日志空间大小是固定,全部写满就从头开始,保存未被刷入磁盘的脏页日志。
两阶段提交
- 在执行更新语句过程,会记录redo log与binlog两块日志,以基本的事务为单位,redo log在事务执行过程中可以不断写入,而binlog只有在提交事务时才写入,所以redo log与binlog的
写入时机
不一样。就会导致由于binlog没写完就异常,这时候binlog里面没有对应的修改记录。
- 为了解决两份日志之间的逻辑一致问题,InnoDB存储引擎使用
两阶段提交
方案。
- 使用
两阶段提交
后,写入binlog时发生异常也不会有影响
流程
-
prepare 阶段
:将 XID(内部 XA 事务的 ID) 写入到 redo log,同时将 redo log 对应的事务状态设置为 prepare,然后将 redo log 持久化到磁盘(innodb_flush_log_at_trx_commit = 1 的作用); -
commit 阶段
:把 XID 写入到 binlog,然后将 binlog 持久化到磁盘(sync_binlog = 1 的作用),接着调用引擎的提交事务接口,将 redo log 状态设置为 commit,此时该状态并不需要持久化到磁盘,只需要 write 到文件系统的 page cache 中就够了,因为只要 binlog 写磁盘成功,就算 redo log 的状态还是 prepare 也没有关系,一样会被认为事务已经执行成功;
出现异常重启会出现什么现象?
不管是时刻 A(redo log 已经写入磁盘, binlog 还没写入磁盘),还是时刻 B (redo log 和 binlog 都已经写入磁盘,还没写入 commit 标识)崩溃,此时的 redo log 都处于 prepare 状态。
如果 binlog 中没有当前内部 XA 事务的 XID,说明 redolog 完成刷盘,但是 binlog 还没有刷盘,则回滚事务。
对应时刻 A 崩溃恢复的情况。如果 binlog 中有当前内部 XA 事务的 XID,说明 redolog 和 binlog 都已经完成了刷盘,则提交事务
。对应时刻 B 崩溃恢复的情况。
注意:
- 对于处于 prepare 阶段的 redo log,即可以提交事务,也可以回滚事务,这取决于是否能在 binlog 中查找到与 redo log 相同的 XID
- 两阶段提交是以 binlog 写成功为事务提交成功的标识,因为 binlog 写成功了,就意味着能在 binlog 中查找到与 redo log 相同的 XID。
事务没提交的时候,redo log 也是可能被持久化到磁盘的。
两阶段提交带来的问题
磁盘 I/O 次数高:对于“双1”配置,每个事务提交都会进行两次 fsync(刷盘),一次是 redo log 刷盘,另一次是 binlog 刷盘。
锁竞争激烈:两阶段提交虽然能够保证「单事务」两个日志的内容一致,但在「多事务」的情况下,却不能保证两者的提交顺序一致,因此,在两阶段提交的流程基础上,还需要加一个锁来保证提交的原子性,从而保证多事务的情况下,两个日志的提交顺序一致。