事务日志 redo和undo
事务有4种特性:原子性、一致性、隔离性和持久性。那么事务的四种特性到底是基于什么机制实现呢?
事务的隔离性由
锁机制
实现。而事务的原子性、一致性和持久性由事务的 redo 日志和undo 日志来保证。
REDO LOG 称为
重做日志
,提供再写入操作,恢复提交事务修改的页操作,用来保证事务的持久性。UNDO LOG 称为
回滚日志
,回滚行记录到某个特定版本,用来保证事务的原子性、一致性。
有的DBA或许会认为 UNDO 是 REDO 的逆过程,其实不然。REDO 和 UNDO都可以视为是一种 恢复操作
,但是:
redo log: 是存储引擎层 (innodb) 生成的日志,记录的是
"物理级别"
上的页修改操作,比如页号xxx,偏移量yyy写入了'zzz'数据。主要为了保证数据的可靠性。undo log: 是存储引擎层 (innodb) 生成的日志,记录的是
逻辑操作
日志,比如对某一行数据进行了INSERT语句操作,那么undo log就记录一条与之相反的DELETE操作。主要用于事务的回滚
(undo log 记录的是每个修改操作的逆操作
) 和一致性非锁定读
(undo log 回滚行记录到某种特定的版本——MVCC,即多版本并发控制)。
为什么需要REDO日志
一方面,缓冲池可以帮助我们消除CPU和磁盘之间的鸿沟,checkpoint机制可以保证数据的最终落盘,然 而由于checkpoint 并不是每次变更的时候就触发
的,而是master线程隔一段时间去处理的。所以最坏的情 况就是事务提交后,刚写完缓冲池,数据库宕机了,那么这段数据就是丢失的,无法恢复。另一方面,事务包含 持久性
的特性,就是说对于一个已经提交的事务,在事务提交后即使系统发生了崩溃,这个事务对数据库中所做的更改也不能丢失。那么如何保证这个持久性呢? 一个简单的做法
:在事务提交完成之前把该事务所修改的所有页面都刷新到磁盘,但是这个简单粗暴的做法有些问题:
修改量与刷新磁盘工作量严重不成比例
有时候我们仅仅修改了某个页面中的一个字节,但是我们知道在InnoDB中是以页为单位来进行磁盘IO的,也就是说我们在该事务提交时不得不将一个完整的页面从内存中刷新到磁盘,我们又知道一个默认页面时16KB大小,只修改一个字节就要刷新16KB的数据到磁盘上显然是小题大做了。即可能是为了一个字节的修改就要刷新一个16kb的页,这个性能和资源浪费太大
随机IO刷新较慢
一个事务可能包含很多语句,即使是一条语句也可能修改许多页面,假如该事务修改的这些页面可能并不相邻,这就意味着在将某个事务修改的Buffer Pool中的页面
刷新到磁盘
时,需要进行很多的随机IO
,随机IO比顺序IO要慢,尤其对于传统的机械硬盘来说。
我们只是想让已经提交了的事务对数据库中数据所做的修改永久生效,即使后来系统崩溃,在重启后也能把这种修改恢复出来。所以我们其实没有必要在每次事务提交时就把该事务在内存中修改过的全部页面刷新到磁盘,只需要把修改了哪些东西记录一下就好。比如,某个事务将系统 表空间中 第10号 页面中偏移量为 100 处的那个字节的值1改成 2 。我们只需要记录一下:将第0号表 空间的10号页面的偏移量为100处的值更新为 2
InnoDB引擎的事务采用了WAL技术 (
Write-Ahead Logging
),这种技术的思想就是先写日志,再写磁盘,只有日志写入成功,才算事务提交成功,这里的日志就是redo log。当发生宕机且数据未刷到磁盘的时候,可以通过redo log来恢复,保证ACID中的D,这就是redo log的作用。
REDO日志的好处、特点
1. 好处
redo日志降低了刷盘频率
redo日志占用的空间非常小,存储表空间ID、页号、偏移量以及需要更新的值,所需的存储空间是很小的,刷盘快。
2. 特点
redo日志是顺序写入磁盘的
在执行事务的过程中,每执行一条语句,就可能产生若干条redo日志,这些日志是按照
产生的顺序写入磁盘的
,也就是使用顺序ID,效率比随机IO快。事务执行过程中,redo log不断记录
redo log跟bin log的区别,redo log是
存储引擎层
产生的,而bin log是数据库层
产生的。假设一个事务,对表做10万行的记录插入,在这个过程中,一直不断的往redo log顺序记录,而bin log不会记录,直到这个事务提交,才会一次写入到bin log文件中。
redo的组成
Redo log可以简单分为以下两个部分:
-
重做日志的缓冲 (redo log buffer)
,保存在内存中,是易失的。
在服务器启动时就会向操作系统申请了一大片称之为 redo log buffer 的 连续内存
空间,翻译成中文就是redo日志缓冲区。这片内存空间被划分为若干个连续的redo log block
。一个redo log block占用512字节
大小。
REDO日志文件如图所示,其中ib_logfile0
和ib_logfile1
即为REDO日志。
redo的整体流程
Write-Ahead Log(预先日志持久化):在持久化一个数据页之前,先将内存中相应的日志页持久化
第1步:先将原始数据从磁盘中读入内存中来,修改数据的内存拷贝
第2步:生成一条重做日志并写入redo log buffer,记录的是数据被修改后的值
第3步:当事务commit时,将redo log buffer中的内容刷新到 redo log file,对 redo log file采用追加写的方式
第4步:定期将内存中修改的数据刷新到磁盘中
redo log的刷盘策略
redo log的写入并不是直接写入磁盘的,InnoDB引擎会在写redo log的时候先写redo log buffer,之后以一 定的频率
刷入到真正的redo log file 中。这里的一定频率怎么看待呢?这就是我们要说的刷盘策略。注意,redo log buffer刷盘到redo log file的过程并不是真正的刷到磁盘中去,只是刷入到 文件系统缓存 (page cache)
中去(这是现代操作系统为了提高文件写入效率做的一个优化),真正的写入会交给系统自己来决定(比如page cache足够大了)。那么对于InnoDB来说就存在一个问题,如果交给系统来同步,同样如果系统宕机,也会丢失数据。
针对这种情况,InnoDB给出 innodb_flush_log_at_trx_commit
参数,该参数控制 commit提交事务时,如何将 redo log buffer 中的日志刷新到 redo log file 中。它支持三种策略:
设置为0
:表示每次事务提交时不进行刷盘操作。(系统默认master thread每隔1s进行一次重做日志的同步) 第1步:先将原始数据从磁盘中读入内存中来,修改数据的内存拷贝 第2步:生成一条重做日志并写入redo log buffer,记录的是数据被修改后的值 第3步:当事务commit时,将redo log buffer中的内容刷新到 redo log file,对 redo log file采用追加 写的方式 第4步:定期将内存中修改的数据刷新到磁盘中
设置为1
:表示每次事务提交时都将进行同步,刷盘操作( 默认值 )
设置为2
:表示每次事务提交时都只把 redo log buffer 内容写入 page cache,不进行同步。由os自己决定什么时候同步到磁盘文件。
另外,InnoDB存储引擎有一个后台线程,每隔1秒
,就会把redo log buffer
中的内容写到文件系统缓存(page cache
),然后调用刷盘操作。
也就是说,一个没有提交事务的redo log
记录,也可能会刷盘。因为在事务执行过程 redo log 记录是会写入 redo log buffer
中,这些redo log 记录会被后台线程
刷盘。除了后台线程每秒1次
的轮询操作,还有一种情况,当redo log buffer
占用的空间即将达到innodb_log_buffer_size
(这个参数默认是16M)的一半的时候,后台线程会主动刷盘。
不同刷盘策略演示
每次事务提交时即刷盘(默认值)
使用这种刷盘策略 ,只要事务提交,redo_log的记录就一定持久化到磁盘中,不会有任何数据丢失,如果事务的执行期间,Mysql挂了或者宕机,但是事务并没有提交,日志丢了就丢了,没有数据损失,可以保证持久性,一般都是采用该刷盘策略,虽然效率较差,但是一般业务场景使用到了事务,那么数据的安全肯定是重要一点的
由操作系统决定真正的刷盘时机
每次事务提交时都只把 redo log buffer 内容写入 page cache,由操作系统自己调度同步到磁盘文件。这种情况要是mysql宕机,数据的安全性还可以保证,但要是此时系统也宕机,那数据的安全就保证不了了。
先将信息写入redo_log_buffer,事务提交时,刷盘到redo_file中,定时刷新修改数据到磁盘中
一种折中的做法,但还是可能会丢失事务。
写入redo log buffer 过程
Mini-Transaction
MySQL把对底层页面中的一次原子访问过程称之为一个
Mini-Transaction
,简称mtr
,比如,向某个索引对应的B+树中插入一条记录的过程就是一个Mini-Transaction
。一个所谓的mtr
可以包含一组redo日志,在进行崩溃恢复时这一组redo
日志可以作为一个不可分割的整体。一个事务可以包含若干条语句,每一条语句其实是由若干个mtr
组成,每一个mtr
又可以包含若干条 redo日志,画个图表示它们的关系就是这样:
redo 日志写入log buffer
redo log 写入log buffer 的过程是顺序的,也就是按block的顺序进行写入,当第一个block的空间用完后,再往它的后继节点写入,InnoDB有一个buf_free,指明后续写入的redo日志应该在log_buffer的哪个位置。
每个mtr产生的redo日志都会暂时存到一个地方,当运行结束时,再将日志全部复制到log_buffer中
不同的事务可能是
并发
执行的,所以 T1 、 T2 之间的 mtr 可能是交替执行
的。没当一个mtr执行完成时,伴随该mtr生成的一组redo日志就需要被复制到log_buffer中,也就是说不同事务的mtr可能是交替写入log_buffer的。有的mtr产生的redo日志量非常大,比如mtr_t1_2
产生的redo日志占用空间比较大,占用了3个block来存储。
redo_log_block的结构
一个redo log block是由
日志头、日志体、日志尾
组成。日志头占用12字节,日志尾占用8字节,所以一个block真正能存储的数据是512-12-8=492字节。
真正的redo日志都是存储到占用
496
字节大小的log block body
中,图中的log block header
和log block trailer
存储的是一些管理信息。我们来看看这些所谓管理信息
都有什么。