MySQL相关(二)- 一条更新语句是如何执行的

前言

上一篇文章讲了 《一条查询语句是如何执行的》,应该很多人都注意到我在前缀 MySQL 相关后面加了个(一)吧,哈哈哈哈,有一肯定有二,认真负责的我怎么会只说查询语句的执行流程而不说更新修改删除语句的执行流程呢?说完这些流程原理之后肯定还要再说说索引方面的东西啦,至于啥时候能更新完,曦轩在这里只能说,尽情期待~

这里有一张脑图,想要完整高清图片可以到微信我的公众号下【6曦轩】下回复 MySQL 脑图获取:
在这里插入图片描述
接下来我们会以这张脑图的一些知识点展开来讲,但是由于文章篇幅有限,有些点可能只会一笔带过,有兴趣的小伙伴可以到我的公众号下与我留言讨论。

正文

正文前再絮絮叨叨

  • 为啥讲的是更新而不是删除和插入呢?

Because,更新的复杂度要比插入和删除要高,如果已经理解了更新的一整套流程,插入和删除的流程对于你来说也是信手拈来(好像不大恰当,暂时想不出更好的词就士但啦),所以这里就只讨论更新的,插入和删除的流程大同小异,不再做讨论。

  • 使用哪个数据库存储引擎?

目前市面上流行的还是 MySQL5.7 ,而且大部分系统用的是分布式微服务的架构,考虑并发事务的执行,这里选择讲解 innodb 引擎中的语句更新流程。

  • 从哪些方面讲?

更新的流程相对比较复杂,涉及到数据库 innodb 的事务,所以这里会先从数据库的磁盘结构+内存结构对其内在机理进行剖析,之后通过讲解 redo log + undo log ,数据库端的事务日志以及 binlog,服务层面的日志,还有数据库的二阶段提交保证事务的 ACID(暂不展开),来让大家对整个流程有一个比较全面的了解。

innodb 的磁盘和内存结构

按照我的思路啊,我们先来看官网提供的这张图:

从图中我们可以看到:

  1. 左边是 innodb 的内存结构,其中包含自适应的 hash 索引(adaptive hash index),Buffer Pool,Change Buffer,最下面是 Log Buffer;
  2. 在内存数据刷到磁盘中间有操作系统的缓存;
  3. 右边是 innodb 的磁盘结构,包含系统表空间,独占表空间,共享表空间,临时表空间,redo log 和 undo log,用虚线表示的是逻辑存在而非物理存在;

接下来我会挑重点来介绍这些组件。

Buffer Pool

这个是 innodb 的缓冲池空间,保存的是数据页(data page)和索引页(index page),在修改数据的时候,数据不会直接写入到磁盘中,而会先写入 Buffer Pool(如果页数据在的话则修改 Buffer Pool),再由内存空间刷入到磁盘空间。

我们可以通过如下命令查看关于 innodb 的 Buffer Pool 的参数,有想详细了解的小伙伴请移步 MySQL 官网
show variables like '%innodb_buffer_pool%';
在这里插入图片描述
show status like '%innodb_buffer_pool%';
在这里插入图片描述
有几个点我们需要明确一下:

  1. 缓冲池(buffer pool)是一种常见的降低磁盘访问的机制;

使用 Buffer Pool,可以避免每次查询数据都跟磁盘进行 IO,磁盘读写,并不是按需读取,而是按页读取,一次至少读一页数据(1M / 64page=16K),如果未来要读取的数据就在页中,就能够省去后续的磁盘IO,提高效率,所以说Buffer Pool 是一种减少磁盘 IO 的机制。

  1. 缓冲池以页(page)为单位缓存数据;
  2. 缓冲池使用的内存淘汰算法是LRU,memcache,OS,InnoDB都使用了这种算法;

InnoDB对普通LRU(链表实现)进行了优化:将缓冲池分为老生代和新生代,入缓冲池的页,优先进入老生代,页被访问,才进入新生代,以解决预读失效的问题页被访问,且在老生代停留时间超过配置阈值的,才进入新生代,以解决批量数据访问,大量热数据淘汰的问题

这里再明确几个概念:

  • 脏页:当缓冲池数据和磁盘不一致的时候;
  • 刷脏:把脏页的数据写入磁盘中的时候叫刷脏;
  • 磁盘和缓冲区都是以页为单位,而关于页的大小:在这里插入图片描述

Change Buffer

  • 更新数据页时,数据页已经加载到缓冲池中的时候,可以直接更新;
  • 那如果更新数据页时,如果数据页存在于磁盘,则需要将数据页先读取到内存,再修改内存,不在内存区域的话至少得发生一次 IO,这样不就资源浪费了?

这个时候 change buffer 就出现了,如果要更新的数据不是唯一索引,并且没有数据重复的情况,则不需要确保数据是否会重复。我们将要修改的记录写入到 change buffer 中,再通过 change buffer 一次性同步到磁盘中 ,减少 IO,5.5 之后叫 change buffer,以前叫 insert buffer (只支持 insert),这样可以减少 IO 次数,提升修改效率。

我们也可以用下面的命令看看 change_buffer 的参数:
show variables like '%change_buffer%';
在这里插入图片描述
注意:max_size 不是指 change buffer 的大小,而是指 change buffer 占整个 buffer pool 的比例,写多读少的情况下可以适当调大 change buffer的比例 。

Log Buffer + Redo Log

看到这里,相信很多人还是会有很多问号:

  • 既然是用内存空间来做缓冲区,那如果数据库服务器宕机了,那未同步完的数据不都完了?

其实很多小伙伴应该之前有接触过或者听说过innodb 是支持事务的,而它支持事务的方式是 redo log + undo log 作为事务日志,但是对 redo log 可能没有进行一个比较深入的了解或者说并不知道它实际上是以一个什么样的角色存在。
好了不卖关子了,了解过 redis 的小伙伴应该都知道 redis 也是基于内存的,它做异常恢复的方式是通过持久化到磁盘的方式,那我们MySQL 的 innodb 引擎也是通过类似的方式,在刚才官网的结构图中我们看到磁盘空间中有 redo log,而 redo log 就是通过buffer 区的另外一块区域 log buffer 进行同步的,这个过程叫crash save。

  • 那已经有Buffer pool 作为缓存了,为啥还要多此一举再多一个 log buffer呢?

我想问一下大家 kafka 快的原因有哪些知道不?其中有一点就是多这个 log buffer 的原因。没错,这位同学答对了!就是顺序读取和随机读取的区别,随机读取是需要刷盘的操作,时长是不稳定的,而顺序读取相对而言就要快很多了,在崩溃恢复的时候起到了很大的作用。buffer pool 还是作为刷入磁盘的主角

  • 那 log buffer 什么时候才会触发刷数据到 redo log 的操作呢?

我们来看下 log buffer 的参数,大小默认是 16M:
show variables like '%log_buffer%';
在这里插入图片描述
redo log 既然是存在于磁盘空间,那它是可以找到对应的文件的,在 mysql 目录中对应的文件名如下,默认是两个大小恒为 48M 的 ib_logfile 文件:
在这里插入图片描述
既然大小是恒定的,那它肯定是会有删除数据的操作的,
在这里插入图片描述
write pos 是当前记录的位置,一边写一边后移,写到第 3 号文件末尾后就回到 0 号文件开头。checkpoint 是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件。
write pos 和 checkpoint 之间的是还空着的部分,可以用来记录新的操作。如果 write pos 跟 checkpoint 重合了,表示已经没有空间,这时候不能再执行新的更新,得停下来先删除一些记录,把 checkpoint 往前移一下。
那到底什么时候刷盘呢?
我的回答是跟事务相关,先看这条命令:
show variables like '%log_at_trx_commit%';
默认值是 1,
在这里插入图片描述
默认是提交事务就写入 log buffer,值为 0 时事务提交后不会刷盘,值为 2 时由操作系统提交。
在这里插入图片描述

  • 标题上没见到 undo log,为什么不说说 undo log?

redo log 和 undo log 是一对,它们组合到一起就是 innodb 的事务日志,但是 undo log 是关于事务提交回滚的日志,这次主要讨论流程,这里就一笔带过不做深入讨论。

binlog

用过 binlog 的伙伴应该知道binlog 有这几个功能:

  1. 记录DDL 和 DML 的逻辑日志
  2. 主从复制 (slave 拿到 master 的 binlog 再执行)
  3. 数据恢复

MySQL 在 server 层面引入了 binlog, 它可以记录所有引擎中的修改操作,因而可以对所有的引擎使用复制功能,然而这种情况会导致redo log与binlog的一致性问题。在 MySQL 中是通过内部 XA 机制解决这种一致性的问题。

我们先来看看 binlog 怎么配置,在 my.cnf 中配置 binlog:
vi /etc/my.cnf

log-bin=mysql-bin #添加这一行就ok
binlog-format=ROW #选择row模式
server_id=1 #配置mysql replaction需要定义,不能和canal的slaveId重复

业内目前推荐使用的是 row 模式,准确性高,虽然说文件大,但是现在有 SSD 和万兆光纤网络,这些磁盘 IO 和网络 IO 都是可以接受的。

在 innodb 里其实又可以分为两部分,一部分在缓存中,一部分在磁盘上。这里业内有一个词叫做刷盘,就是指将缓存中的日志刷到磁盘上。跟刷盘有关的参数有两个:
sync_binlog 和binlog_cache_size。
这两个参数作用如下:

binlog_cache_size: 二进制日志缓存部分的大小,默认值32k
sync_binlog=[N]: 表示每多少个事务写入缓冲,刷一次盘,默认值为0

要注意两点:

  1. binlog_cache_size设过大,会造成内存浪费。binlog_cache_size设置过小,会频繁将缓冲日志写入临时文件。
  2. sync_binlog=0:表示刷新binlog时间点由操作系统自身来决定,操作系统自身会每隔一段时间就会刷新缓存数据到磁盘,这个性能最好。sync_binlog=1,代表每次事务提交时就会刷新binlog到磁盘,对性能有一定的影响。sync_binlog=N,代表每N个事务提交会进行一次binlog刷新。
    另外,这里存在一个一致性问题,sync_binlog=N,数据库在操作系统宕机的时候,可能数据并没有同步到磁盘,于是再次重启数据库,会带来数据丢失问题。

MySQL 的 binlog 是多文件存储,定位一个 LogEvent 需要通过 binlog filename + binlog position,进行定位。

二阶段提交

二阶段提交时指当一个事务跨越多个节点时,为了保证事务的 ACID特性,需要引入一个作为协调者的组件来统一掌控所有节点(称作参与者)的操作结果并最终指示这些节点是否要把操作结果进行真正的提交(比如将更新后的数据写入磁盘等等)。

因此,二阶段提交的算法思路可以概括为:参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作还是中止操作。

第一阶段:

  1. InnoDB prepare, write/sync redo log;
  2. binlog不作任何操作;

第二阶段:包含两步:

  1. write/sync Binlog;
  2. InnoDB commit (commit in memory);

总结

写得是有点累啊,不过有大家的支持我觉得就是值得的,也是我更新的动力,不对的地方希望大家帮忙指正,觉得写得还可以的话麻烦大家帮我点个赞哈哈哈。

最后我们还是以一张图来将流程描述清楚:
在这里插入图片描述

By the way

有问题?可以给我留言或私聊
有收获?那就顺手点个赞呗~

当然,也可以到我的公众号下「6曦轩」,
回复“学习”,即可领取一份
【Java工程师进阶架构师的视频教程】~
回复“面试”,可以获得:
【本人呕心沥血整理的 Java 面试题】
回复“MySQL脑图”,可以获得
*** 【MySQL 知识点梳理高清脑图】***
在这里插入图片描述
由于我咧,科班出身的程序员,php,Android以及硬件方面都做过,不过最后还是选择专注于做 Java,所以有啥问题可以到公众号提问讨论(技术情感倾诉都可以哈哈哈),看到的话会尽快回复,希望可以跟大家共同学习进步,关于服务端架构,Java 核心知识解析,职业生涯,面试总结等文章会不定期坚持推送输出,欢迎大家关注~~~

发布了29 篇原创文章 · 获赞 9 · 访问量 9933

猜你喜欢

转载自blog.csdn.net/weixin_42669785/article/details/104114754