MySQL之事务和redo日志
事务
事务的四个ACID特性。
Atomicity 原子性
Consistency 一致性
Isolation 隔离性
Durability 持久性
原子性
原子性即这个事务的任务要么全做了,要么全部没做,不能出现做一半这种情况。
一致性
一致性即数据库中的数据必须满足数据满足数据库的约束。
隔离性
即事务与事务之间相互不打扰,比如两个事务在实际过程中并不是原子的,两个事务中的语句是交替运行的,但是隔离性就是要保证两个事务之间状态转换不会互相影响。
持久性
就是一旦事务结束,就要将其保存到磁盘中防止丢失。
事务的状态
活跃的active:即事务正在运行其中的SQL语句。
部分提交的partially commited:事务执行完成,但是其结果还在内存中保存着,没有刷新到磁盘中。
提交的 commited : 结果成功刷新到磁盘,就从上面部分提交进入该状态。
失败的 failed : 就是事务执行过程出现数据库或操作系统自身的错误,就导致了事务提交失败。
中止 aborted : 就是事务提交失败,需要将已经修改的语句回滚到事务未执行以前。
事务开启和关闭
begin; 算打开一个事务
....
commit; 提交事务
或者
rollback; 回滚事务
begin算一种打开方式,但是它不能指定事务的打开的类型,只读、读写等
还有一种开始事务方式
start transaction; # 不加参数,默认读写事务
start transaction read only; # 只读事务
start transaction read write; # 读写事务
start transaction read only, with consistent shapshot; # 开启只读事务和一致性读。
....
commit; 提交事务
或者
rollback; 回滚事务
关闭就是上面两个commit 和 rollback 两种,一个是提交,一个是回滚。
还有就是自动提交。
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set, 1 warning (0.04 sec)
我们的自动提交是默认打开的,自动提交就是我们在没有指定start transaction 或 begin时,MySQL会为每个语句启动一个事务,每一条语句相当与都是开了一个事务然后语句结束会自动帮我们提交。
隐式提交
当我们使用begin 或 start transaction ,或则我们关闭了自动提交。事务此时就不会提交,直到我们使用commit或则rollback。但是当出现以下情况,MySQL会帮我们偷偷提交事务。
- 定义或修改数据库对象即DDL语句。
- 隐式使用或修改mysql数据库中的表
- 当我们没提交一个事务时又begin 或 start transaction 就会继续帮我们自动提交前面已执行的。
- 加载数据的语句。load data
- 关于MySQL复制的语句
- 等等....
保存点savepoint
即我们可以使用savepoint回滚到某个保存点中,但是提交保存点以前的语句,回滚保存点以后的语句。
begin;
...sql语句
savepoint s1;
...SQL语句
rollback to s1; # 此时就会提交s1前的SQL,而回滚s1以后的SQL
redo日志
如果我们对页面进行修改的话,我们会先将修改的页面保存在内存的buffer pool中,但是如果出现断电的情况,我们做的修改就会全部丢失了不是吗。
我们对事务的持久性进行保证,就是对一个提交的事务做的页面修改刷新到磁盘中,最简单粗暴的办法就是事务提交后直接将记录刷到磁盘中。
- 但是刷新到磁盘是十分慢的,而且如果我们只对页面进行一些很微小的修改,我们都需要以页为单位和磁盘进行交互,是一个十分不值当的行为。
- 需要不断进行随机IO,因为页面在磁盘上可能零零散散,我们需要不断进行随机IO,效率也是十分低下的。
redo日志的目的:就是我们对于提交事务的修改进行永久的保存,即使系统崩溃,我们重启后也能将修改恢复到原样。
简单的redo日志
简单的redo日志分为很多中类型,MLOG_1BYTE类型,MLOG_2BYTE类型,MLOG_4BYTE类型,MLOG_8BYTE类型,MLOG_WRITE_STRING类型。
就是如果我们修改只有1,2,4,8,或则连续的一小段个字节,就会使用这种简单的日志进行保存。比如我们对某个系统变量的修改。
有了简单的redo日志,我们可以根据表空间ID和页号以及偏移量,我们就可以在重启时找到这个页,将对应偏移量的数据替换上去就可以了。
复杂的redo日志
我们平时插入一条数据,可能修改了一个页面的多个地方,比如页满了进行了页分裂,那修改的地方可就大了去了,以及插入数据也对页头一些页的基本信息又影响。反正就是一个页的插入可能影响到很多页。
我们如果对于一个页面有多处修改,我们使用简单的redo日志,一个地方一个地方的写日志,那要生成好多的redo日志,在空间上可能比我们一整个页面进行刷新效率都低。所以出现了更复杂的redo日志。
复杂的页面有以下类型。
PS:紧凑行格式就是Compact、Dynamic行格式,最原始的redundant行格式就是非紧凑的
- MLOG_REC_INSERT : 创建一个插入的非紧凑行格式页面的记录的redo日志。
- MLOG_COMP_REC_INSERT:创建一个插入的紧凑行格式页面的记录的redo日志。
- MLOG_COMP_PAGE_CREATE:创建一个存储紧凑行格式的页面的redo日志。
- MLOG_COMP_REC_DELETE:创建一个删除的一个紧凑行格式页面的记录的redo日志。
- MLOG_COMP_LIST_START_DELETE:表示从某条记录给定记录开始删除页面中一系列使用紧凑行格式页面的记录的redo日志。
- MLOG_COMP_LIST_END_DELETE:表示删除停止的记录的redo日志和MLOG_COMP_LIST_START_DELETE是一套的。
- ....还有很多
我们要理解这个复杂页面,就要把简单redo页面的想法抛弃掉。这个复杂redo页面并不是存储某个偏移量修改的新值,我把它理解为它存储的是这个操作,就是我们插入一条数据,这个redo就是把这个操作存储起来了。但是它实际上并不是这样的哈。
这些redo日志可以从物理和逻辑层面看。
- 物理层面上看,这些日志指明了对哪个表空间的那个页进行修改了。
- 逻辑层面上看,在系统崩溃重启时,并不能直接加载这些类型的redo日志。而是需要进行调用函数进行对这些redo日志处理,然后才能恢复要原样。
上面写得很清楚了需要调用函数,说明这些redo只是存储一些基础数据,然后调用函数后才能根据这些基础数据对页面进行恢复。而并不是像简单redo页面那样直接存储页面的数据哦。
看了好几遍懵逼,就是一直认为它存储的就是修改页面的数据,其实不然,它存储的是进行该操作后用来复原的基本数据。
归根结底,说了redo的不同页面类型只不过就是我们需要redo页面然后将数据库恢复要出错前的模样。
Mini-Transaction
以组的形式写入redo日志
我们在写入redo日志的时候,我们会考虑到一个情况就是我们的操作是原子的,比如说我们插入一条记录,我们不仅仅要更改页的数据,还要更改页头的基本信息,有时候还要更新父索引节点的数据。这一系列操作,都是密不可分的,如果一个没有恢复,那生成的数据将会是错误的。所以MySQL将会以组的形式写入redo日志。
MySQL将redo日志分为组的形式,对于需要保证原子性的一系列操作,就会在redo日志后面加上一个特殊类型的redo日志。代表一条完整的redo日志。
但是也会出现需要保证原子性操作的redo日志只有一条redo日志。因为MySQL要保证尽量节省空间嘛。所以会在类型的最高位设置代表是否是一条单一的redo日志。
Mini-Transaction
MySQL将对页面中的一次原子操作过程称之为Mini-Transaction,简称mtr。一个mtr就代表一组redo日志。我们接下来的redo的介绍很多都会以mtr为一个单位。
redo日志的写入
MySQL以mtr的形式来存储每一组日志,但是我们redo日志是怎么个顺序写入磁盘的呢?当然呢,和磁盘打交道就是意味着慢,所以redo日志首先还是会写入内存的缓冲区中然后在慢慢地写入磁盘哈。我们先将写入内存的过程。
MySQL设计了一个redo log block的数据结构来存储mtr,大小为512字节。
- header 头部呢就存储一些基本信息
- HDR_NO 唯一标号,省略前面的英文单词
- HDR_DATA_LEN 已使用的数据长,初始为12,写满就是512.
- FIRST_REC_GROUP 该block中第一个mtr中第一条redo日志的偏移量
- CHECKPOINT_NO 就是checkpoint的序号
- body 就是存储mtr的地方
- trailer 就是尾部放检查和。验证完整性的。
然后我们有了这个数据结构,就可以引出log buffer 简言之就是redo日志缓冲区,用来缓存redo日志的,在MySQL服务器启动时会像操作系统申请的一段连续的内存空间,和buffer pool差不多。
我们通过innodb_log_buffer_size可以查看redo日志缓冲区的大小,默认为16M。
mysql> show variables like 'innodb_log_buffer_size';
+------------------------+----------+
| Variable_name | Value |
+------------------------+----------+
| innodb_log_buffer_size | 16777216 |
+------------------------+----------+
1 row in set, 1 warning (0.00 sec)
结构如上图,我们以mtr为单位将redo日志写入log buffer。
但是我们应该在哪里插入呢?所以log buffer维护了一个叫做buf_free的全局变量,用来指向空闲的值。然后我们获取buf_free就可以直接在那个位置插入。
我们还有一个问题就是在log block header 中有个属性, log_block_first_rec_group 这个属性有什么用呢?
如上图,我们插入了4个mtr分别属于两个事务,我们用来记录这个log_block_first_rec_group的这个属性呢记录了这个block中第一个mtr的第一个redo页面的偏移量。
就像上面的mtr_t1_2一样,一下占了三个block,在第二个页面中log_block_first_rec_group的记录是512,就说明了当前的block是延续之前的mtr。同一第三个页面我们就可以知道新的mtr在哪里。
所以呢这个log_block_first_rec_group属性值的作用是让我们知道当前block有没有接续之前block的部分,如果有才可以知道,不然我们无法识别这是一个新的mtr还是接续的mtr。
redo日志刷盘
redo日志从redo log buffer中存储进入磁盘中是讲究时机的,同时呢由于存储到磁盘是很慢的,所以需要缓冲区的存在,让线程阻塞在那里等跟磁盘IO的资源那也是不理智的对不对。
以下是redo刷盘的时机
- log buffer空间不足时。
- 事务提交时(要保证事务的持久性就得把redo刷到磁盘中)
- 后台线程不断刷盘,大概每秒刷一次。
- 正常关闭服务器
- 做checkpoint时
- 其他等情况。。。
redo日志文件
我们可以从根目录下的data文件夹中查看到两个文件,默认是两个。
我们可以修改系统变量,在启动时修改log文件数量
mysql> show variables like 'innodb_log_files_in_group';
+---------------------------+-------+
| Variable_name | Value |
+---------------------------+-------+
| innodb_log_files_in_group | 2 |
+---------------------------+-------+
1 row in set, 1 warning (0.00 sec)
在启动时指定log文件的大小一次来修改,默认48M。
mysql> show variables like 'innodb_log_file_size';
+----------------------+----------+
| Variable_name | Value |
+----------------------+----------+
| innodb_log_file_size | 50331648 |
+----------------------+----------+
1 row in set, 1 warning (0.00 sec)
我们将redo日志写入磁盘中,本质上就是把block从内存中复制了一份到磁盘的ib_logfile文件中。
ib_logfile是由512字节的block组成的,ib_logfile的前2048字节即4个block用来存储一些基本的管理信息。后面剩余的就是用来存储从内存中读取来的block,每个block同样也是512字节。
首先介绍前4个block块主要是存储哪些管理信息。
- log file header 的组成
- checkpoint1组成
- 第三个没用,第四个和checkpoint1一样。
Log Sequeue Number(LSN)
我们一直在前面提到的LSN值,所以它代表着什么呢?我们可以叫日志序列号,LSN的初始值默认为8704。
我们前面提到的log buffer作为redo日志的缓冲区,有两个指针我们可以回想一下,buf_free和buf_next_to_write两个全局变量,一个代表当前缓冲区空闲的地方,一个代表下一个要log buffer写入磁盘的mtr地址。我们可以知道那些mtr还没写入磁盘中。
在buffer pool中维护着一个lsn值,当系统初始化没有mtr插入时,就是8716 即8704 + 12 的block header。随着mtr的插入到block中,会不断增大。
每个mtr都有一个对应的lsn值,lsn值越小代表redo日志产生得越早。它其实就和buf_free 差不多,只不过它是代表着一个序列号。
flushed_to_disk_lsn
innodb也在buffer pool中维护了一个全局变量叫做flushed_to_disk_lsn,和这个buf_next_to_write有着异曲同工之处。它是用来维护buffer pool中已经刷新到磁盘的lsn。
当我们没有将缓冲区中的mtr刷新到磁盘中,lsn就不会发生改变,当我们将mtr刷到磁盘的redo日志文件中时,lsn就会增加相应的偏移量 (不是很懂,上面讲我们是以block的形式向磁盘刷新redo页面的)。当然如果我们又跨过了页首或者页尾,我们就还需要添加4字节的页尾长度。
思路好乱,感觉书上没讲清楚或者是我没有get到作者的点吧。
flushed_to_disk_lsn直接点说就是一个从8706开始的数字,跟着刷新到磁盘的大小增大而增大。
flush链表中的LSN
我们之前简单提到过的flush的结构,在控制块中会存放两个关于页面修改的LSN。
- oldest_modification : 如果该页面被修改,这里将保存页面的第一次修改时mtr开始时的LSN值。可以理解为mtr插入到buffer pool前的lsn值。
- newest_modification : 如果对该页面进行修改,将保存mtr插入结束后的lsn值。对于每一次修改,这个值都会改变。
我们知道flush链表是根据第一次修改的时间从大到小排序的,最新插入的会被排在链表首部。其实就是按照oldest_modification 的值进行从大到小排序的,最早进行修改,向log buffer 写入mtr的页面的LSN。
我们在这里需要知道的是我们oldest_modification 保存的是页面第一次修改的时候向buffer pool插入mtr前buffer pool中维护的LSN值是多少,newest_modification 就是最近一次修改时buffer pool在插入mtr后buffer pool的值是多少。
像上面我们在mtr1中修改了a页面,在mtr2中修改了b,c页面。他们的LSN值就是上面的所示。我们可以算一些8716就是8704 + 12 就是第一个插入的mtr之前的LSN初始的大小嘛。8916-8716 = 200就是mtr1的大小嘛。
但是我们需要注意的是,重复修改的页面不会重新进行插入控制块嘛,前面文章好像说过,就是我们怎么找链表中有没有对应页面的控制块呢?就是通过哈希表找到key为是表空间+页号组成的键,然后我们修改其newest_modification 的值就好了。
redo日志文件的LSN
我们提到了在redo日志文件中log file header 保存了一个redo文件开始的LSN,LSN就是在文件基本信息2048字节的位置LSN值为8704开始计算。
checkpoint
redo日志对于系统崩溃恢复来说是十分重要的存在,但是如果系统不崩溃的话,这样的操作是没有意义的,且耗费性能的。但是当系统崩溃重启的时候innodb是怎么知道哪些redo日志是已经刷新到磁盘了,还是没有呢?
我们将上述的mtr_1刷新到磁盘了,这是在日志文件中我们就可以将mtr1的记录覆盖掉,我们会将日志文件中头部的4个block中存储checkpoint进行+1的操作,并修改其存储的LSN。可以回过头查看redo日志文件的组成。以上这个操作就叫做服务器做了一次checkpoint。
具体步骤如下:
- 首先我们去flush链表中找到最后一个控制块,找到它的oldest_modification ,它的值就代表当前已经刷新的LSN的值。为什么呢?仔细想想,它代表着这个mtr插入前的LSN值,它又是最后一个控制块,代表着这是还没刷新到磁盘的最早的脏页 (刷新到磁盘就不会在flush链表里了)。说明这个oldest_modification 代表着还没刷新mtr的LSN。
- 将这个oldest_modification 的值赋值给checkpoint_LSN。
- 将日志文件头部中的checkpoint中维护的基本信息进行更新,包括编号、偏移量、LSN。
以上的checkpoint的信息只会保存到第一个redo日志文件的管理信息中去。
还有一点就是checkpoint有1和2,对于他们来说,就是LSN是偶数的时候就保存到2,奇数就保存到1。
innodb中的LSN值
mysql> show engine innodb status;
LOG
---
Log sequence number 118084165
Log buffer assigned up to 118084165
Log buffer completed up to 118084165
Log written up to 118084165
Log flushed up to 118084165
Added dirty pages up to 118084165
Pages flushed up to 118084165
Last checkpoint at 118084165
16 log i/o's done, 0.00 log i/o's/second
对于事务一致性的控制
我们在事务中提到过的持久性,如果我们要保证事务的持久性,就得在事务结束的时候将该事务产生的mtr刷新到磁盘上,但是在事务结束的时候立刻刷新到磁盘上是十分耗时的。
但是呢如果我们不及时刷新,选择将其先放到缓冲区里面,但是出现系统崩溃,事务的操作就没有办法恢复了,无法保证其一致性。
在性能和一致性上我们可以进行选择。对innodb_flush_log_at_trx_commit系统变量进行设置
mysql> show variables like 'innodb_flush_log_at_trx_commit';
+--------------------------------+-------+
| Variable_name | Value |
+--------------------------------+-------+
| innodb_flush_log_at_trx_commit | 1 |
+--------------------------------+-------+
1 row in set, 1 warning (0.00 sec)
- 0代表事务提交不会立刻将mtr刷新到磁盘,而是让后台线程自己去慢慢刷。
- 1即默认值,代表事务提交时必须把mtr刷新到磁盘中。
- 2代表事务提交必须将mtr刷到操作系统的缓冲区。
innodb_flush_log_at_trx_commit值为2,我们进行刷新磁盘,从数据库的缓冲区中下来调用操作系统的执行对磁盘进行操作,还会先进入操作系统的缓冲区中让操作系统去操作,如果操作系统没崩必然也可以保证事务的一致性,但是如果操作系统也崩了,那就不能保证了。我们值为1是代表必须刷新到磁盘中,即操作系统将数据真正刷到磁盘上了。
崩溃恢复
确定恢复的起点
对于已经刷新到磁盘的mtr来说,没有必要进行再次恢复,所以我们需要对于起点进行确认。
我们从checkpoint1和checkpoint2拿出LSN,因为俩个地方都存了checkpoint的LSN,所以比较哪个最大,就可以确定需要恢复redo的起点。
确定恢复的终点
对于每个block来说,都维护这一个len,我们只要读到len小于512的,就可以知道这一页是没有满的,然后根据其具体长度,就可以知道恢复的终点。
怎么恢复
我们就是从起点,慢慢扫描每一个redo日志,对其进行复原,直到终点。
加速方法:
-
使用哈希表
就是将每个页面的redo日志,放入哈希表中,根据spaceID和page Number来确定哈希表的散列值,然后根据插入的先后排序,先插入在前。然后我们就可以根据一个页面一个页面进行更新,这样避免了随机IO。
-
跳过已经刷新的页面
我们在做了一次checkpoint后,又有页面从LRU链表或者flush链表中的页面更新到磁盘中。因为checkpoint不是一直在做的。
我们怎么知道呢?在每个页面的File Header中有一个FIL_PAGE_LSN的属性,该属性记录了最近一次刷新页面的newest_modification 值。如果当前LSN小于这个FIL_PAGE_LSN的值,代表已经刷新到后面的记录了,不需要更新了,直接跳过。