Mysql—Mysql的日志系统以及一条更新sql的执行流程

Posted L-Java

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Mysql—Mysql的日志系统以及一条更新sql的执行流程相关的知识,希望对你有一定的参考价值。

  详细介绍了mysql的日志系统中的三种日志:重做日志(redo log)、归档日志(binlog)、回滚日志(undo log )以及一条更新sql的执行流程。

  我正在参与CSDN《新程序员》有奖征文,活动地址:https://marketing.csdn.net/p/52c37904f6e1b69dc392234fff425442

  我们的sql可以分为两种,一种是查询,一种是更新(插入,更新,删除),此前学习了一条查询sql语句的执行流程,而如果是更新语句,则除了上面的流程之外(更新还会导致相关表的查询缓存全部失效),还会涉及到三个重要的日志模块(基于InnoDB引擎),分别是重做日志(redo log)、归档日志(binlog)、回滚日志(undo log )。

  除了上面三种日志之外,常见MySQL日志还有:

  1. 慢查询日志(slow log):记录所有执行时间超过long_query_time的所有查询或不使用索引的查询。
  2. 中继日志(relay log):MySQL主从复制时从服务器使用的日志。

1 redo log重做日志

  redo log日志模块又名重做日志、归档日志、事务日志,它是InnoDB引擎自带的一个日志模块。redo log通常又被称为物理日志,记录的是数据页的物理修改,而不是某一行或某几行修改成怎样,它可以用来恢复提交后的物理数据页(恢复数据页,且只能恢复到最后一次提交的位置,因为修改会覆盖之前的)。redo log保证事务的持久性。
  在MySQL中,如果每一次的更新操作都需要写进磁盘,然后磁盘也要找到对应的那条记录,然后再更新,整个过程IO成本、随机查找成本(因为操作的数据散落在磁盘各处)都很高。为了解决这个问题,MySQL使用了WAL技术(Write Ahead Logging),也称为日志先行技术,它的关键点就是先写日志,再写磁盘。写日志虽然也是写磁盘,但是它是顺序写,相比随机写开销更小,能提升语句执行的性能。WAL技术保证了数据一致性和持久性,并且提升语句执行性能。
  当有一条记录需要更新的时候,InnoDB引擎就会先把记录写到redo log日志系统里面,并更新内存,这个时候更新就算完成了。同时,InnoDB引擎会在适当的时候,将这个操作记录由后台线程异步更新到磁盘里面,而这个更新往往是在系统比较空闲的时候做的。
  InnoDB的redo log是固定大小的,并且大小可以配置,比如可以配置为一组4个文件,每个文件的大小是1GB,那么总共就可以记录4GB的操作。只能从头开始写,写到末尾就将一部分redo log跟新到磁盘中,随后又回到开头循环写。如下面这个图所示:
在这里插入图片描述
  上图中,write pos是当前记录的指针,一边写一边后移,写到第3号文件末尾后就回到0号文件开头。checkpoint是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件。在redo log满了到擦除旧记录腾出新空间这段期间,是不能再接收新的更新请求,所以有可能会导致MySQL卡顿
  在writepos和checkpoint之间的还空着的部分(在上图中就是ib_logfile_2和ib_logfile_3),可以用来记录新的操作,如果write pos追上了checkpoint,表示redo log满了,这个时候不能够在执行新的更新,得停下来先擦除一些记录,把checkpoint推进一下。擦除记录前要把记录更新到数据文件。
  由于redo log的加入,保证了MySQL数据一致性和持久性的同时(即使数据刷盘之前MySQL奔溃了,重启后仍然能通过redo log里的更改记录进行重放,重新刷盘,保证之前提交的记录不会丢失,这个能力称为crash-safe),还提升了sql语句的执行性能(写redo log是顺序写,相比于更新数据文件的随机写,日志的写入开销更小,能显著提升语句的执行性能,提高并发量)。

  实际上,数据库的crash-safe保证的是:

  1. 如果客户端收到事务成功的消息,事务就一定持久化了;
  2. 如果客户端收到事务失败(比如主键冲突、回滚等)的消息,事务就一定失败了;
  3. 如果客户端收到“执行异常”的消息或者什么也没收到,应用需要重连后通过查询当前状态来继续后续的逻辑。此时数据库只需要保证内部(数据和日志之间,主库和备库之间)一致就可以了,即事务一致性。

1.1 脏页

  当内存数据页跟磁盘数据页内容不一致的时候,我们称这个内存页为“脏页”。内存数据写入到磁盘后,内存和磁盘上的数据页的内容就一致了,称为“干净页”。MySQL 偶尔慢一下的那个瞬间,可能在刷脏页(flush)。
  什么时候会触发刷脏?

  1. innodb的redo log日志写满了,需要把checkpoint往前推进。此时整个系统就不能再接受更新了,所有的更新都必须阻塞,这种情况是InnoDB要尽量避免的。
  2. 一个查询buffer pool缓冲池内存不足时,会淘汰一些数据页缓存(最近最久未使用算法,LRU),有可能会淘汰到脏页缓存,此时就要先把脏页刷到磁盘。刷脏页一定会写盘,这是为了保证了每个数据页有两种状态:1、内存里的一定是正确数据;2、内存里没有,磁盘上的一定是正确数据。
  3. MySQL 认为系统空闲时,会刷盘。当然系统繁忙时,也会见缝插针刷盘。
  4. MySQL 正常关闭时。

  根据上面四种情况分析,可知出现以下这两种情况,都是会明显影响性能的:

  1. 一个没有内存缓存的大的查询要淘汰的脏页个数太多,会导致查询的响应时间明显变长;
  2. 日志写满,更新全部堵住,写性能跌为0,这种情况对敏感业务来说,是不能接受的。

  我们可以设置InnoDB的某些参数,来尽量避免上面的这两种情况。
  首先需要告诉InnoDB所在主机的IO能力,这样InnoDB才能知道需要全力刷脏页的时候,可以刷多快。可以使用innodb_io_capacity参数,它会告诉InnoDB你的磁盘能力。这个值建议设置成磁盘的IOPS。磁盘的IOPS可以通过fio这个工具来测试,下面的语句是用来测试磁盘随机读写速度的命令:

fio -filename=$filename -direct=1 -iodepth 1 -thread -rw=randrw -ioengine=psync -bs=16k -size=500M -numjobs=10 -runtime=10 -group_reporting -name=mytest

  参数innodb_max_dirty_pages_pct是脏页比例上限,默认值是75%。脏页比例是通过Innodb_buffer_pool_pages_dirty/Innodb_buffer_pool_pages_total参数得到。
  另外Mysql还有一个策略,当刷脏页时,如果该脏页边上也是脏页,也会把边上的脏页一起刷掉,而且该逻辑会一直蔓延innodb_flush_neighbors参数就是来控制该行为的,值为1会有上述机制,0则不会。机械硬盘可能会有不错的效果,但ssd建议设置为0。并且mysql 8.0 的innodb_flush_neighbors默认为0

2 undo log回滚日志

  undo log同样是InnoDB引擎的另一个日志模块,undo log顾名思义,主要就是提供了回滚的作用,保证事务的原子性,但其还有另一个主要作用,就是多个版本控制(MVCC,全称Multi-Version Concurrency Control)。
  undo log和redo log记录物理日志不一样,它是逻辑日志。在数据更新的流程中,会记录一条与当前sql操作相反的逻辑日志到undo log中,可以认为当delete一条记录时,undo log中会记录一条对应的insert记录,反之亦然,当update一条记录时,它记录一条对应相反的update记录。undo log记录中存储的是老版本数据,当一个旧的事务需要读取数据时,为了能读取到老版本的数据,需要逆着undo链找到满足其可见性的记录,这就是MVCC的一致性视图简单原理(后面会详细讲解MVCC)。
  如果因为某些原因导致事务异常失败了,可以借助undo log中的逻辑记录读取到相应的内容进行回滚,保证事务的原子性,所以undo log也必不可少。
  为了保证事务并发操作时,在写各自的undo log时不产生冲突,undo log是采用回滚段(segment)的方式来记录的,每个undo操作在记录的时候占用一个undo log segment,每个回滚段中有1024个undo log segment。
  应用到多版本行版本控制(MVCC)的时候(读已提交和可重复读的隔离级别),有时候也是通过undo log来实现的:当读取的某一行被其他事务锁定时,它可以从undo log中分析出该行记录以前的数据是什么,从而提供该行版本信息,让用户实现非锁定的一致性读取,可以有效减少加锁操作,提升性能。
  后面将MVCC的时候,会着重讲解undo log。

3 binlog归档日志

  binlog日志模块位于Mysql架构中的Server层中,因此这是MySQL自带的日志模块,和具体的存储引擎无关,binlog一个二进制格式的日志文件,所有的存储引擎都可以识别并使用,也称为逻辑日志。
  在之前MySQL中并没有InnoDB引擎,MySQL自带的引擎是MyISAM,但是MyISAM没有crash-safe的能力,因为binlog日志只能用于归档。而InnoDB是另一个公司以插件形式引入MySQL的,既然只依赖binlog没有crash-safe能力,所以InnoDB使用另外一套日志系统----也就是redo log来实现crash-safe能力。
  之所以将binlog称为归档日志,是因为binlog不会像redo log一样擦掉之前的记录循环写,而是一直记录(超过有效期才会被清理),如果超过单个日志文件的最大值(默认1G,可以通过变量 max_binlog_size 设置),则会新起一个文件继续记录。但由于日志可能是基于事务来记录的(如InnoDB表类型),而事务是绝对不可能也不应该跨文件记录的,如果正好binlog日志文件达到了最大值但事务还没有提交则不会切换新的文件记录,而是继续增大日志,所以 max_binlog_size 指定的值和实际的binlog日志大小不一定相等。
  并且,binlog记录的内容和redo log也不一样,redo log是物理日志,记录的是“某个数据页上做了什么修改”,binlog是逻辑日志,日志中包含了所有引起或可能引起数据库改变(如delete语句但没有匹配行)的事件信息(sql语句),比如“给 ID=2 这一行的 c 字段加 1 ”,但绝不会包括select和show这样的查询sql语句,sql语句以"事件"的形式保存,每一个记录的sql包含了时间、事件开始和结束位置等信息。

  正是由于binlog有归档的作用,所以binlog主要用作:

  1. 主从同步,从库的数据同步依赖的就是binlog。MySQL Replication在Master端开启binlog,Master把它的二进制日志传递给slaves来达到master-slave数据一致的目的。这种同步可以推广的更远,即可以通过监听binlog日志,将一个普通库中操作,同步到另一个库的相同的表中,这两个表可以结构一致,但是没有主从关系,大公司应该都有监听binlog日志的组件。
  2. binlog文件是追加写入的,不会覆盖,数据库可以基于某个时间点的精确还原。首先将某个时间点的数据库全备份数据导入,然后找到对应的binlog文件,然后指定二进制日志的起始/终止时间点或者指定起始/终止pos节点,执行恢复即可。

  binlog 日志的三种格式:

format定义优点缺点
statement记录的是修改SQL语句日志文件小,节约IO,提高性能。写完之后的最后会有 COMMIT标记。准确性差,对一些系统函数不能准确复制或不能复制,如now()、uuid()等
row(推荐)记录的是每行实际数据的变更,记两条,更新前和更新后。准确性强,能准确复制数据的变更,利于数据恢复。写完之后的最后会有一个 XID event标记。日志文件大,较大的网络IO和磁盘IO
mixedstatement和row模式的混合准确性强,文件大小适中有可能发生主从不一致问题

4 一条更新sql的执行流程

  现有一个更新的sql语句为:

update T set A=1 where ID=1;

  假设Mysql开启了binlog日志,并且使用的InnoDB引擎,那么更新操作的执行流程如下:

  1. 前面的步骤同查询sql类似,连接数据库,让查询缓存失效,经过分析器的词法和语法分析,再经过优化器的优化,比如选择走id索引,最后走到执行器。
  2. 执行器首先向执行引擎查找满足ID=1的数据,首先在引擎内部的内存缓冲池中查找,如果有,就直接使用缓存的数据,如果没有,就从磁盘中先读取满足条件的数据到缓存池中,然后返回给执行器。
  3. 执行器获取到数据之后,将数据的A字段设置为1,随后调用执行引擎的接口写入新数据。
  4. 执行引擎将这行新数据先在内存数据页中进行更新,同时将这个更新操作记录到redo log buffer(日志缓存)里面,并且fsync()刷到redo log file日志文件中,生成事务ID(XID)信息,此时redo log处于prepare状态。然后告诉执行器,执行完成了,随时可以提交。(undo log日志也是在此时写入)。
  5. 执行器收到通知后,将逻辑操作记录到binlog cache中,并且fsync()刷到binlog file日志文件中(成功后会有Commit标志或者XID标志),然后调用引擎的提交事务接口
  6. 执行引擎把刚刚写入的redo log改成提交(commit)状态,这一步只是在redo log里标记commit,不会立即刷盘,因为日志在prepare状态时已被刷到日志文件中,同时将对应的undo log放入到一个删除列表中,未来通过purge来删除,不会立即删除。更新完成。

  可以看到,更新sql和查询sql的步骤相比,出了之前的步骤都是相同的之外,还涉及到一系列日志文件的写入。但是细心的我们能够发现:

  1. 仅仅是更新了内存中的数据,并没有写入磁盘中,实际上数据并不会每次都落盘,后面会讲。
  2. 这里不仅仅是简单的先写xx,后写xx,而是对于redo log写了两次,redo log有两个状态。实际上redo log和binlog本身就是两个独立的系统,要想保持数据一致性,就必须使用分布式事务的解决方案来处理,这里其实就是很典型的分布式事务场景。将redo log的写分成了两步,其实就是使用了一种分布式一致性解决方案——两阶段提交协议(Two-phase Commit,2PC)。它的内部的具体原理,我们后面在下一篇文章单独学习。

参考资料:
  《 MySQL 技术内幕: InnoDB 存储引擎》
  《高性能 MySQL》
  《MySQL实战45讲 | 极客时间 | 丁奇》
  《详细分析MySQL的日志(一)

如有需要交流,或者文章有误,请直接留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!

以上是关于Mysql—Mysql的日志系统以及一条更新sql的执行流程的主要内容,如果未能解决你的问题,请参考以下文章

讲日志系统:一条SQL更新语句是如何执行的

日志系统:一条SQL更新语句是如何执行的

日志系统:一条SQL更新语句是如何执行的

MySQL - 实战 - 基础架构和日志系统

02 | 日志系统:一条SQL更新语句是如何执行的?

mysql-2.一条SQL更新语句是如何执行的?(日志架构)