逻辑架构
MySQL的逻辑架构可分为四层,包括连接层、服务层、引擎层和存储层,各层的接口交互及作用如下图所示。需要注意的是,由于本文将主要讲解事务的实现原理,因此下文针对的都是InnoDB引擎下的情况。
连接层: 负责处理客户端的连接以及权限的认证。
服务层: 定义有许多不同的模块,包括权限判断,SQL接口,SQL解析,SQL分析优化, 缓存查询的处理以及部分内置函数执行等。MySQL的查询语句在服务层内进行解析、优化、缓存以及内置函数的实现和存储。
引擎层: 负责MySQL中数据的存储和提取。MySQL中的服务器层不管理事务,事务是由存储引擎实现的。其中使用最为广泛的存储引擎为InnoDB,其它的引擎都不支持事务。
存储层: 负责将数据存储于设备的文件系统中。
undo log
回滚日志同样也是InnoDB引擎提供的日志,顾名思义,回滚日志的作用就是对数据进行回滚。当事务对数据库进行修改,InnoDB引擎不仅会记录redo log,还会生成对应的undo log日志;如果事务执行失败或调用了rollback,导致事务需要回滚,就可以利用undo log中的信息将数据回滚到修改之前的样子。
undo log由两个作用,一是提供回滚,二是实现MVCC。
redo log
重做日志(redo log)是InnoDB引擎层的日志,用来记录事务操作引起数据的变化,记录的是数据页的物理修改。
如果每一次的更新操作都需要写进磁盘,那么磁盘需要通过寻道找到对应的那条记录,然后再更新磁盘保存的内容,整个过程 IO 成本、查找成本都很高。
为了解决这个问题,MySQL 的设计者使用了 预写式技术(Write Ahead logging),它的关键点就是先写日志,再写磁盘;
缓冲池buffer pool:Mysql体系中最重要的一个概念,这是在内存中分配的一个区域,包含了磁盘中部分数据页的映射,作为访问数据库的缓冲,可以大大减少IO操作的频率,提升数据刷新的效率。
当请求读取数据时,会先判断是否在缓冲池命中,如果未命中会从磁盘上进行检索读取后放入内存中的缓冲池;
当请求写入数据时,会先在redo log中记录这次操作,并且更新缓冲池中的数据;缓冲池中修改的数据会定期更新到磁盘中。这一过程也被称之为刷脏数据 。
具体来说,当有一条记录需要更新的时候,InnoDB 引擎就会先把更新记录写到 redo log 里面,并更新内存 buffer pool 中的数据,这个时候更新就算完成了。
因此,当数据修改时,除了修改内存 buffer pool中的数据,还会在redo log中记录这次操作;如果MySQL宕机,重启时可以读取redo log中的数据,根据这个redo log 日志中的修改记录 重新执行相应操作,以此来对数据库进行恢复,从而保证了事务的持久性,使得数据库获得crash-safe能力。
有了 redo log,InnoDB 就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为 crash-safe。
InnoDB 的 redo log 是固定大小的,比如可以配置为一组 4 个文件,每个文件的大小是 1GB,那么这块“粉板”总共就可以记录 4GB 的操作。
为了能够持续不断的对更新记录进行写入,在redo log日志中设置了两个标志位置,checkpoint和write_pos,分别表示记录擦除的位置和记录写入的位置。从头开始写,写到末尾就又回到开头循环写,如上面这个图所示。
write pos 是当前记录的位置,一边写一边后移,写到第 3 号文件末尾后就回到 0 号文件开头。checkpoint 是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件。
write pos 和 checkpoint 之间的是“粉板”上还空着的部分,可以用来记录新的操作。如果 write pos 追上 checkpoint,表示“粉板”满了,这时候不能再执行新的更新,得停下来先擦掉一些记录,把 checkpoint 推进一下。
当write_pos追上checkpoint时,表示redo log日志已经写满。这时不能继续执行新的数据库更新语句,需要停下来先删除一些记录,执行checkpoint规则腾出可写空间。
checkpoint规则:checkpoint触发后,将 内存缓冲区buffer 中 的脏数据页 刷到磁盘 。
脏数据页:指内存缓冲区 buffer 中的数据页被修改以后,跟磁盘的数据页不一致,称为脏页
redo log日志 写满后,将已提交事务所修改的脏数据页 写入到磁盘数据文件中,并删除该事务所涉及的 redo log 日志内容
binlog
前面我们讲过,MySQL 整体来看,其实就有两块:一块是 Server 层,它主要做的是 MySQL 功能层面的事情;还有一块是引擎层,负责存储相关的具体事宜。上面我们聊到的 redo log 是 InnoDB 引擎特有的日志,而 Server 层也有自己的日志,称为 binlog(归档日志)。
binlog主要记录数据库的变化情况,内容包括数据库所有的更新操作。所有涉及数据变动的操作,都要记录进二进制日志中。因此有了binlog可以很方便的对数据进行复制和备份,因而也常用作主从库的同步。
- redo log是一种物理日志,记录的是实际上对某个数据进行了怎么样的修改;
- 而binlog是逻辑日志,记录的是SQL语句的原始逻辑,比如 ”给 id =2 这一行的 a 字段值加 1 "。
binlog日志中的内容是二进制的,根据日志格式参数的不同设置,可能基于SQL语句、基于数据本身或者二者的混合。一般常用记录的都是SQL语句。
小结
- redo log 是 InnoDB 存储引擎特有的;binlog 是 MySQL 的 Server 层实现的,所有存储引擎都可以使用。
- redo log 是物理日志,记录的是“在某个数据页上做了什么修改”;binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如:“给 ID=2 这一行的 c 字段加 1 ”。
- redo log 是循环写的,空间固定会用完;binlog 是可以追加写入的。“追加写”是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。
内部执行流程
有了对这两个日志的概念性理解,我们再来看执行器和 InnoDB 引擎在执行这个简单的 update 语句时的内部流程。
比如我们执行 Update Test set a=1 Where id=2 这一更新语句
-
执行器先找引擎取 id=2 这一行。id 是主键,引擎直接用树搜索找到这一行。如果 id=2 这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。 -
执行器拿到引擎给的行数据,把该行字段a 的值更改为 1,比如原来是 0,现在就是 1,得到新的一行数据,再调用引擎接口写入这行新数据。 -
引擎将这行新数据更新到内存中,同时将这个更新操作记录到 redo log 里面,此时 redo log 处于 prepare 状态。然后告知执行器执行完成了,随时可以提交事务。 -
执行器生成这个操作的 binlog,并把 binlog 写入磁盘。 -
执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交(commit)状态,更新完成。
这个 update 语句的执行流程图如下
从上图可以看出,MySQL在执行更新语句的时候,在服务层进行语句的解析和执行,在引擎层进行数据的提取和存储;同时在服务层对binlog进行写入,在InnoDB内进行redo log的写入。
你可能注意到了,最后三步看上去有点“绕”,将 redo log 的写入拆成了两个步骤:
一是binlog写入之前prepare状态的写入,二是binlog写入之后commit状态的写入。这就是"两阶段提交"。
只有reldo log 和 bin log 都写入成功了,事务才算提交成功,如果redolog只是完成了prepare, 而binlog又写入失败,那么事务本身会回滚;
两阶段提交
为什么必须有“两阶段提交”呢?这是为了让两份日志之间的逻辑一致。
由于 redo log 和 binlog 是两个独立的逻辑,如果不用两阶段提交,要么就是先写完 redo log 再写 binlog,或者采用反过来的顺序。我们看看这两种方式会有什么问题。
仍用前面的 update 语句来做例子。假设当前 id=2 的行,字段 a 的值是 0,我们将这行数据的字段 a 的值更改为1,再假设执行 update 语句过程中在写完第一个日志后,第二个日志还没有写完期间发生了 crash,会出现什么情况呢?
-
先写 redo log 后写 binlog。假设在 redo log 写完,binlog 还没有写完的时候,MySQL 进程异常重启。由于我们前面说过的,redo log 写完之后,系统即使崩溃,仍然能够把数据恢复回来,redo log 里面已经记录了“把 id=2 的行 字段c 从 0 改成 1”这个日志,所以恢复后这一行 c 的值是 1。但是由于 binlog 没写完就 crash 了,这时候 binlog 里面就没有记录这个语句。 因此,之后备份日志的时候,存起来的 binlog 里面就没有这条语句。然后你会发现,如果需要用这个 binlog 来恢复临时库的话,由于这个语句的 binlog 丢失,这个临时库就会少了这一次更新,恢复出来的 id=2 的这一行 c 的值就是 0,与原库的值不同。 -
先写 binlog 后写 redo log。如果在 binlog 写完之后 crash,由于 redo log 还没写,崩溃恢复以后这个事务无效,所以这一行 c 的值是 0。但是 binlog 里面已经记录了“给 ID=2 这一行的 c 字段值加 1 ”这个日志。所以,在之后用 binlog 来恢复的时候就多了一个事务出来,恢复出来的这一行 c 的值就是 1,与原库的值不同。
可以看到,如果不使用“两阶段提交”,那么数据库crash 后进行恢复时 数据库的状态就有可能和用它的binglog日志恢复出来的库的状态 不一致。
小结
MySQL将 redo log 的写入拆成了两个步骤:prepare 和 commit,这就是"两阶段提交"。
只有reldo log 和 bin log 都写入成功了,事务才算提交成功,如果redolog只是完成了prepare, 而binlog又写入失败,那么事务本身会回滚;
日志刷盘
除了上面提到的对于脏数据的刷盘,实际上redo log日志在记录时,为了提升日志写入的性能,也是先写入内存中的buffer 缓冲区,然后再将 buffer 缓冲区中的日志 刷到磁盘日志文件中;
为了保证日志文件的持久化,也需要经历将日志记录从内存写入到磁盘的过程。redo log日志可分为两个部分,一是存在内存中的日志缓冲区 redo log buff,二是保存在磁盘上的redo log日志文件。
日志刷盘参数
- innodb_flush_log_at_trx_commit 这个参数设置成 1 的时候,表示每次事务的 redo log 都直接持久化到磁盘。这个参数建议设置成 1,这样可以保证 MySQL 异常重启之后数据不丢失。
- sync_binlog 这个参数设置成 1 的时候,表示每次事务的 binlog 都持久化到磁盘。这个参数同样建议设置成 1,这样可以保证 MySQL 异常重启之后 binlog 不丢失。
innodb_flush_log_at_trx_commit={0|1|2} # 指定何时将事务日志 刷到磁盘,默认为1。
0表示每次事务提交都只是写入到"redo log buffer"中。
1表示每次事务提交都将"redo log buffer"中的数据同步到"os buffer",并且从"os buffer"持久化到磁盘文件中。
2表示每次事务提交都只是把"redo log buffer"中的数据同步到"os buffer"
InnoDB 有一个后台线程,每隔 1 秒,就会把 redo log buffer 中的日志,调用 write 写到文件系统的 page cache,然后调用 fsync 持久化到磁盘。
实际上,除了后台线程每秒一次的轮询操作外,还有两种场景会让一个没有提交的事务的 redo log 写入到磁盘上的redo log日志文件。
- 一种是,redo log buffer 占用的空间即将达到 innodb_log_buffer_size 一半的时候,后台线程会主动写盘。注意,由于这个事务并没有提交,所以这个写盘动作只是 write,而没有调用 fsync,也就是只留在了文件系统的 page cache。
- 另一种是,并行的事务提交的时候,顺带将这个事务的 redo log buffer 持久化到磁盘。 假设一个事务 A 执行到一半,已经写了一些 redo log 到 buffer 中,这时候有另外一个线程的事务 B 提交,如果innodb_flush_log_at_trx_commit 设置的是 1,那么按照这个参数的逻辑,事务 B 要把 redo log buffer 里的日志全部持久化到磁盘。这时候,就会带上事务 A 在 redo log buffer 里的日志一起持久化到磁盘。
为了确保每次记录都能够写入到磁盘的文件中,每次将redo log buffer中的日志缓存写入redo log file的过程中都会调用一次操作系统的fsync操作。
数据的最终落盘写入
数据最终写入磁盘的过程分为以下两种情况
- 如果是正常运行的实例的话,内存数据页被修改以后,跟磁盘的数据页不一致,称为脏页。最终数据落盘,就是把内存中的数据页写入磁盘的数据文件。
- 在崩溃恢复场景中,InnoDB 如果判断到一个数据页可能在崩溃恢复的时候丢失了更新,就会将它读到内存中的buffer pool ,然后根据 redo log 对 内存中的数据页内容进行更新。更新完成后,内存中的数据页变成脏页,就回到了第一种情况的状态。
redolog只在数据恢复时使用,数据恢复时,redo log只会更新内存中的数据页内容,产生脏页,将脏数据页写入磁盘是单独的机制;
参考:
MySQL实战45讲
|