主要是用来保证数据的持久性
我们知道InnoDB
存储引擎是以页为单位来管理存储空间的,我们进行的增删改查操作其实本质上都是在访问页面(包括读页面、写页面、创建新页面等操作)。在真正访问页面之前,需要把在磁盘上的页缓存到内存中的Buffer Pool
之后才可以访问。对于一个已经提交的事务,在事务提交后即使系统发生了崩溃,这个事务对数据库中所做的更改也不能丢失(事务的持久性)。但是如果我们只在内存的Buffer Pool
中修改了页面,假设在事务提交后突然发生了某个故障,导致内存中的数据都失效了,那么这个已经提交了的事务对数据库中所做的更改也就跟着丢失了,这是我们所不能忍受的。那么如何保证这个持久性
呢?一个很简单的做法就是在事务提交完成之前把该事务所修改的所有页面都刷新到磁盘,但是这个简单粗暴的做法有些问题:
刷新一个完整的数据页太浪费了
有时候我们仅仅修改了某个页面中的一个字节,但是我们知道在InnoDB
中是以页为单位来进行磁盘IO的,也就是说我们在该事务提交时不得不将一个完整的页面从内存中刷新到磁盘,我们又知道一个页面默认是16KB大小,只修改一个字节就要刷新16KB的数据到磁盘上显然是太浪费了。
随机IO刷起来比较慢
一个事务可能包含很多语句,即使是一条语句也可能修改许多页面,倒霉催的是该事务修改的这些页面可能并不相邻,这就意味着在将某个事务修改的Buffer Pool
中的页面刷新到磁盘时,需要进行很多的随机IO,随机IO比顺序IO要慢,尤其对于传统的机械硬盘来说。
咋办呢?再次回到我们的初心:我们只是想让已经提交了的事务对数据库中数据所做的修改永久生效,即使后来系统崩溃,在重启后也能把这种修改恢复出来。所以我们其实没有必要在每次事务提交时就把该事务在内存中修改过的全部页面刷新到磁盘,只需要把修改了哪些东西记录一下就好,比方说某个事务将系统表空间中的第100号页面中偏移量为1000处的那个字节的值1
改成2
我们只需要记录一下:
将第0号表空间的100号页面的偏移量为1000处的值更新为
2
。
这样我们在事务提交时,把上述内容刷新到磁盘中,即使之后系统崩溃了,重启之后只要按照上述内容所记录的步骤重新更新一下数据页,那么该事务对数据库中所做的修改又可以被恢复出来,也就意味着满足持久性
的要求。因为在系统奔溃重启时需要按照上述内容所记录的步骤重新更新数据页,所以上述内容也被称之为重做日志
,英文名为redo log
,我们也可以土洋结合,称之为redo日志
。与在事务提交时将所有修改过的内存中的页面刷新到磁盘中相比,只将该事务执行过程中产生的redo
日志刷新到磁盘的好处如下:
redo
日志占用的空间非常小
存储表空间ID、页号、偏移量以及需要更新的值所需的存储空间是很小的
redo
日志是顺序写入磁盘的
在执行事务的过程中,每执行一条语句,就可能产生若干条redo
日志,这些日志是按照产生的顺序写入磁盘的,也就是使用顺序IO。
总结: redo log不是随着事务的提交才写入的,而是在事务的执行过程中,便开始写入redo 中。具体的落盘策略可以进行配置 。防止在发生故障的时间点,尚有脏页未写入磁盘,在重启mysql服务的时候,根据redo log进行重做,从而达到事务的未入磁盘数据进行持久化这一特性。
更好的进行系统奔溃恢复,InnoDB设计师把redo log
都放在了大小为512字节
的页
中。我们这里把用来存储redo
日志的页称为redo log block
(这里的页跟InnoDB基本的存储单位不是一个概念)。
InnoDB
设计师为了解决磁盘速度过慢的问题而引入了Buffer Pool
。同理,写入redo
日志时也不能直接直接写到磁盘上,实际上在服务器启动时就向操作系统申请了一大片称之为redo log buffer
的连续内存空间,翻译成中文就是redo日志缓冲区
,我们也可以简称为log buffer
。这片内存空间被划分成若干个连续的redo log block
,就像这样:
向log buffer
中写入redo
日志的过程是顺序的,也就是先往前边的redo log block
中写,当该block
的空闲空间用完之后再往下一个block
中写。
可是这些日志总在内存里呆着也不是个办法,在一些情况下它们会被刷新到磁盘里,比如:
log buffer
空间不足时
log buffer
的大小是有限的(通过系统变量innodb_log_buffer_size
指定),如果不停的往这个有限大小的log buffer
里塞入日志,很快它就会被填满。设计InnoDB
的大叔认为如果当前写入log buffer
的redo
日志量已经占满了log buffer
总容量的大约一半左右,就需要把这些日志刷新到磁盘上。
事务提交时
我们前边说过之所以使用redo
日志主要是因为它占用的空间少,还是顺序写,在事务提交时可以不把修改过的Buffer Pool
页面刷新到磁盘,但是为了保证持久性,必须要把修改这些页面对应的redo
日志刷新到磁盘。
后台线程不停的刷刷刷
后台有一个线程,大约每秒都会刷新一次log buffer
中的redo
日志到磁盘。
正常关闭服务器时
做所谓的checkpoint
时(我们现在没介绍过checkpoint
的概念,稍后会仔细唠叨,稍安勿躁)
其他的一些情况…
有一个很不幸的事实就是我们的redo log
文件组容量是有限的,我们不得不选择循环使用redo
日志文件组中的文件,但是这会造成最后写的redo
日志与最开始写的redo
日志追尾
,这时应该想到:redo日志只是为了系统奔溃后恢复脏页用的,如果对应的脏页已经刷新到了磁盘,也就是说即使现在系统奔溃,那么在重启后也用不着使用redo日志恢复该页面了,所以该redo日志也就没有存在的必要了,那么它占用的磁盘空间就可以被后续的redo日志所重用。redo log空间复用如下图
write pos
是当前记录的位置,一边写一边后移,写到第3号文件末尾后就回到0号文件开头。checkpoint
是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件。
write pos
和checkpoint
之间的是“粉板”上还空着的部分,可以用来记录新的操作。如果write pos
追上checkpoint
,表示redo log
满了,这时候不能再执行新的更新,得停下来先擦掉一些记录,把checkpoint
推进一下。
有了redo log,InnoDB就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为crash-safe。
Undo Log
实现事务原子性:
事务处理过程中如果出现了错误或者用户执行了 ROLLBACK
语句,MySQL可以利用Undo Log
中的备份将数据恢复到事务开始之前的状态
Undo log
实现多版本并发控制:
事务未提交之前,Undo保存了未提交之前的版本数据,Undo 中的数据可作为数据旧版本快照供其他并发事务进行快照读
我们前边说过,对于使用InnoDB
存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列(row_id
并不是必要的,我们创建的表中有主键或者非NULL的UNIQUE键时都不会包含row_id
列)请参考MySQL—InnoDB记录结构:
trx_id
:每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的事务id
赋值给trx_id
隐藏列。
roll_pointer
:每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到undo日志
中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。
比方说我们的表hero
现在只包含一条记录:
mysql> SELECT * FROM hero;
+--------+--------+---------+
| number | name | country |
+--------+--------+---------+
| 1 | 刘备 | 蜀 |
+--------+--------+---------+
1 row in set (0.07 sec)
假设插入该记录的事务id
为80
,那么此刻该条记录的示意图如下所示:
假设之后两个事务id
分别为100
、200
的事务对这条记录进行UPDATE
操作,操作流程如下:
每次对记录进行改动,都会记录一条undo日志
,每条undo日志
也都有一个roll_pointer
属性(INSERT
操作对应的undo日志
没有该属性,因为该记录并没有更早的版本),可以将这些undo日志
都连起来,串成一个链表,所以现在的情况就像下图一样:
对该记录每次更新后,都会将旧值放到一条undo日志
中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被roll_pointer
属性连接成一个链表,我们把这个链表称之为版本链
,版本链的头节点就是当前记录最新的值。
版本链也是实现MVCC的基础,换句话说MVCC的实现是基于Undo log的。
主要是用来数据恢复,比如删库跑路这种
binlog是记录所有数据库表结构变更(例如CREATE、ALTER TABLE…)以及表数据修改(INSERT、UPDATE、DELETE…)的二进制日志。
我们MySQL为了兼容其它非事务引擎的复制,在server层面引入了 binlog, 它可以记录所有引擎中的修改操作,因而可以对所有的引擎使用复制功能; 然而这种情况会导致redo log与binlog的一致性问题;MySQL通过二阶段提交机制(XA机制)解决这种一致性的问题。
XA(分布式事务)规范主要定义了(全局)事务管理器(TM: Transaction Manager)和(局部)资源管理器(RM: Resource Manager)之间的接口。XA为了实现分布式事务,将事务的提交分成了两个阶段:也就是2PC (tow phase commit),XA协议就是通过将事务的提交分为两个阶段来实现分布式事务。
1)prepare 阶段
事务管理器向所有涉及到的数据库服务器发出prepare"准备提交"请求,数据库收到请求后执行数据修改和日志记录等处理,处理完成后只是把事务的状态改成"可以提交",然后把结果返回给事务管理器。即:为prepare阶段,TM向RM发出prepare指令,RM进行操作,然后返回成功与否的信息给TM。
2)commit 阶段
事务管理器收到回应后进入第二阶段,如果在第一阶段内有任何一个数据库的操作发生了错误,或者事务管理器收不到某个数据库的回应,则认为事务失败,回撤所有数据库的事务。数据库服务器收不到第二阶段的确认提交请求,也会把"可以提交"的事务回撤。如果第一阶段中所有数据库都提交成功,那么事务管理器向数据库服务器发出"确认提交"请求,数据库服务器把事务的"可以提交"状态改为"提交完成"状态,然后返回应答。即:为事务提交或者回滚阶段,如果TM收到所有RM的成功消息,则TM向RM发出提交指令;不然则发出回滚指令。
我们来看执行器和InnoDB引擎在执行这个简单的update语句时的内部流程。
update T set c=c+1 where ID=2;
如下图
由于redo log和binlog是两个独立的逻辑,如果不用两阶段提交,要么就是先写完redo log再写binlog,或者采用反过来的顺序。我们看看这两种方式会有什么问题。
仍然用前面的update语句来做例子。假设当前ID=2的行,字段c的值是0,再假设执行update语句过程中在写完第一个日志后,第二个日志还没有写完期间发生了crash,会出现什么情况呢?
可以看到,如果不使用“两阶段提交”,那么数据库的状态就有可能和用它的日志恢复出来的库的状态不一致。