InnoDB存储引擎是以页
为单位来管理存储空间的。在真正访问页面之前,需要把在磁盘上
的页缓存到内存中的
Buffer Pool之后才可以访问。所有的变更都必须先更新缓冲池
中的数据,然后缓冲池中的脏页
会以一定的频
率被刷入磁盘(checkPoint机制),通过缓冲池来优化CPU和磁盘之间的鸿沟,这样就可以保证整体的性能不
会下降太快。
一方面,缓冲池可以帮助我们消除CPU和磁盘之间的鸿沟,checkpoint机制可以保证数据的最终落盘,然而由于
checkpoint并不是每次变更的时候就触发
的,而是master线程隔一段时间去处理的。所以最坏的情况就是事务提交
后,刚写完缓冲池,数据库宕机了,那么这段数据就是丢失的,无法恢复。
另一方面,事务包含持久性的特性,就是说对于一个已经提交的事务,在事务提交后即使系统发生了崩溃,这个
事务对数据库中所做的更改也不能丢失。
那么如何保证这个持久性呢?一个简单的做法:在事务提交完成之前把该事务所修改的所有页面都刷新到磁盘,
但是这个简单粗暴的做法有些问题:
有时候我们仅仅修改了某个页面中的一个字节,但是我们知道在InnoDB中是以页为单位来进行磁盘IO的,也就是说我们在该事务提交时不得不将一个完整的页面从内存中刷新到磁盘,我们又知道一个页面默认是16KB大小,只修改一个字节就要刷新16KB的数据到磁盘上显然是太小题大做了。
一个事务可能包含很多语句,即使是一条语句也可能修改许多页面,假如该事务修改的这些页面可能并不相 邻,这就意味着在将某个事务修改的BufferPool中的页面
刷新到磁盘
时,需要进行很多的随机IO
,随机Io比 顺序I0要慢,尤其对于传统的机械硬盘来说。
另一个解决的思路:我们只是想让已经提交了的事务对数据库中数据所做的修改永久生效,即使后来系统崩溃,在重启后也能把这种修改恢复出来。所以我们其实没有必要在每次事务提交时就把该事务在内存中修改过的全部页面刷新到磁盘,只需要把修改
了哪些东西记录一下
就好。比如,某个事务将系统表空间中第10号页面中偏移
量为100处的那个字节的值1改成2。我们只需要记录一下:将第0号表空间的10号页面的偏移量为100处的值
更新为2。
InnoDB引擎的事务采用了WAL
技术(Write-Ahead Logging),这种技术的思想就是先写日志,再写磁盘,只
有日志写入成功,才算事务提交成功,这里的日志就是redo log。当发生宕机且数据未刷到磁盘的时候,可以通
过redo log来恢复,保证ACID中的D,这就是redo log的作用。
在服务器启动时就向操作系统申请了一大片称之为redo log buffer的
连续内存空间
,翻译成中文就是redo日志缓冲 区。这片内存空间被划分成若干个连续的redo log block
。一个redo log block占用512字节
大小。
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中。它支持三种策略:
redo log是事务持久性的保证,undo log是事6原子性的保证。在事务中更新数据
的前置操作
其实是要先写入一
个undo log
。
事务需要保证原子性
,也就是事务中的操作要么全部完成,要么什么也不做。但有时候事务执行到一半会出现一
些情况,比如:
服务器本身的错误,操作系统错误
,甚至是突然断电导致ROLLBACK
语句结束当前事务的执行。以上情况出现,我们需要把数据改回原先的样子,这个过程称之为回滚
,这样就可以造成一个假象:这个事务看
起来什么都没做,所以符合原子性
要求。
每当我们要对一条记录做改动时(这里的改动可以指INSERT、DELETE、UPDATE
),都需要”留一手”–把回
滚时所需的东西记下来。比如:
插入一条记录时
,至少要把这条记录的主键值记下来,之后回滚的时候只需要把这个主键值对应的记录删掉
就好了。(对于每个INSERT,InnoDB存储引擎会完成一个DELETE)删除了一条记录
,至少要把这条记录中的内容都记下来,这样之后回滚时再把由这些内容组成的记录插入
到表中就好了。(对于每个DELETE,InnoDB存储引擎会执行一个INSERT)修改了一条记录
,至少要把修改这条记录前的旧值
都记录下来,这样之后回滚时再把这条记录更新为旧值就MySQL把这些为了回滚而记录的这些内容称之为撤销日志
或者回滚日志
(即undo log
)。注意,由于查询操作
(SELECT)并不会修改任何用户记录,所以在查询操作执行时,并不需要记录相应的undo日志。
此外,undo log会产生redo log
,也就是undo log的产生会伴随着redo log的产生,这是因为undo log也需要持久
性的保护。
用户对undo日志可能有
误解
:undo用于将数据库物理地恢复到执行语句或事务之前的样子。但事实并非如此。undo是逻辑日志
,因此只是将数据库逻辑地恢复到原来的样子。所有修改都被逻辑地取消了,但是数据结构和页 本身在回滚之后可能大不相同。
这是因为在多用户并发系统中,可能会有数十、数百甚至数干个并发事务。数据库的主要任务就是协调对数据记 录的并发访问。比如,一个事务在修改当前一个页中某几条记录,同时还有别的事务在对同一个页中另几条记录进行修改。因此,不能将一个页回滚到事务开始的样子,因为这样会影响其他事务正在进行的工作。
undo的另一个作用是MVCC,即在InnoDB存储引擎中MVCC的实现是通过undo来完成。当用户读取一行记录时,若该记录已经被其他事务占用,当前事务可以通过undo读取之前的行版本信息,以此实现非锁定读取。
对于InnoDB引擎来说,每个行记录除了记录本身的数据之外,还有几个隐藏的列:
begin;
insert into user(name) values ('tom');
插入的数据都会生成一条insert undo log,并且数据的回滚指针会指向它。undo log会记录undo log的序号、插入
主键的列和值.…,那么在进行rollback的时候,通过主键直接把对应的数据删除即可。
当我们执行update时:
对于更新的操作会产生update undo log,并且会分更新主键的和不更新主键的,假设现在执行:
update user set name='sun' where id = 1;
当更新主键时
update user set id=2 where id = 1;
对于更新主键的操作,会先把原来的数据deletemark标识打开,这时并没有真正的删除数据,真正的删除会交给
清理线程去判断,然后在后面插入一条新的数据,新的数据也会产生undo log,并且undo log的序号会递增。
可以发现每次对数据的变更都会产生一个undo log,当一条记录被变更多次时,那么就会产生多条undo log,
undo log记录的是变更前的日志,并且每个undo log的序号是递增的,那么当要回滚的时候,按照序号依次向前 推
,就可以找到我们的原始数据了。
以上面的例子来说,假设执行rollback,那么对应的流程应该是这样:
因为insert操作的记录,只对事务本身可见,对其他事务不可见。故该undo log可以在事务提交后直接删除,不需 要进行purge操作。
该undo log可能需要提供MVcc机制,因此不能在事务提交时就进行删除。提交时放入undo log链表,等待purge线程进行最后的删除。
undo log是逻辑日志,对事务回滚时,只是将数据库逻辑地恢复到原来的样子。
redo log是物理日志,记录的是数据页的物理变化,undo log不是redo log的逆过程。