上一篇文章讲了 《一条查询语句是如何执行的》,应该很多人都注意到我在前缀 MySQL 相关后面加了个(一)吧,哈哈哈哈,有一肯定有二,认真负责的我怎么会只说查询语句的执行流程而不说更新修改删除语句的执行流程呢?说完这些流程原理之后肯定还要再说说索引方面的东西啦,至于啥时候能更新完,曦轩在这里只能说,尽情期待~
这里有一张脑图,想要完整高清图片可以到微信我的公众号下【6曦轩】下回复 MySQL 脑图获取:
接下来我们会以这张脑图的一些知识点展开来讲,但是由于文章篇幅有限,有些点可能只会一笔带过,有兴趣的小伙伴可以到我的公众号下与我留言讨论。
Because,更新的复杂度要比插入和删除要高,如果已经理解了更新的一整套流程,插入和删除的流程对于你来说也是信手拈来(好像不大恰当,暂时想不出更好的词就士但啦),所以这里就只讨论更新的,插入和删除的流程大同小异,不再做讨论。
目前市面上流行的还是 MySQL5.7 ,而且大部分系统用的是分布式微服务的架构,考虑并发事务的执行,这里选择讲解 innodb 引擎中的语句更新流程。
更新的流程相对比较复杂,涉及到数据库 innodb 的事务,所以这里会先从数据库的磁盘结构+内存结构对其内在机理进行剖析,之后通过讲解 redo log + undo log ,数据库端的事务日志以及 binlog,服务层面的日志,还有数据库的二阶段提交保证事务的 ACID(暂不展开),来让大家对整个流程有一个比较全面的了解。
按照我的思路啊,我们先来看官网提供的这张图:
从图中我们可以看到:
接下来我会挑重点来介绍这些组件。
这个是 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%';
有几个点我们需要明确一下:
使用 Buffer Pool,可以避免每次查询数据都跟磁盘进行 IO,磁盘读写,并不是按需读取,而是按页读取,一次至少读一页数据(1M / 64page=16K),如果未来要读取的数据就在页中,就能够省去后续的磁盘IO,提高效率,所以说Buffer Pool 是一种减少磁盘 IO 的机制。
InnoDB对普通LRU(链表实现)进行了优化:将缓冲池分为老生代和新生代,入缓冲池的页,优先进入老生代,页被访问,才进入新生代,以解决预读失效的问题页被访问,且在老生代停留时间超过配置阈值的,才进入新生代,以解决批量数据访问,大量热数据淘汰的问题
这里再明确几个概念:
这个时候 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 的比例,默认为 25%。如果数据库大部分索引都是非唯一索引,并且业务是写多读少,这种情况下可以适当调大 change buffer的比例 。
看到这里,相信很多人还是会有很多问号:
其实很多小伙伴应该之前有接触过或者听说过 innodb 是支持事务的,而它支持事务的方式是 redo log + undo log 作为事务日志,但是对 redo log 可能没有进行一个比较深入的了解或者说并不知道它实际上是以一个什么样的角色存在。
好了不卖关子了,了解过 redis 的小伙伴应该都知道 redis 也是基于内存的,它做异常恢复的方式是通过持久化到磁盘的方式,那我们MySQL 的 innodb 引擎也是通过类似的方式,在刚才官网的结构图中我们看到磁盘空间中有 redo log,而 redo log 就是通过buffer 区的另外一块区域 log buffer (专门用来保存写入日志文件的数据,默认 16M,可以节省磁盘 IO)进行同步的,这个能力叫crash safe。redo log 在 innodb 中保证了事务的持久性。
redo log 不是记录数据页更新之后的状态,而是记录这个页做了什么改动,属于物理日志。
这种日志和磁盘配合的整个过程,其实就是 MySQL 里的 WAL 技 术(Write-Ahead Logging),它的关键点就是先写日志,再写磁盘。
我想问一下大家 kafka 快的原因有哪些知道不?其中有一点就是多这个 log buffer 的原因。没错,这位同学答对了!就是顺序 IO 和随机 IO 的区别,随机 IO 是刷盘的操作,相对而言顺序 IO 效率就高多了,因此先把修改写入日志,可以延迟刷盘时机,进而提升系统吞吐。当然,buffer pool 还是作为刷入磁盘的主角。
我们来看下 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 时由操作系统提交。
值 | 含义 |
---|---|
0(延迟写) | log buffer 将每秒一次地写入 log file 中,并且 log file 的 flush 操作同时进行。 该模式下,在事务提交的时候,不会主动触发写入磁盘的操作。 |
1(默认,实时 写,实时刷) | 每次事务提交时 MySQL 都会把 log buffer 的数据写入 log file,并且刷到磁盘 中去。 |
2(实时写,延 迟刷) | 每次事务提交时 MySQL 都会把 log buffer 的数据写入 log file。但是 flush 操 作并不会同时进行。该模式下,MySQL 会每秒执行一次 flush 操作。 |
redo log 和 undo log 是一对,它们组合到一起就是 innodb 的事务日志,但是 undo log 是关于事务提交回滚的日志,这次主要讨论流程,这里就简单提一下。
undo log(撤销日志或回滚日志)记录了事务发生之前的数据状态(不包括 select)。 如果修改数据时出现异常,可以用 undo log 来实现回滚操作(保持原子性)。
在执行 undo 的时候,仅仅是将数据从逻辑上恢复至事务之前的状态,而不是从物 理页面上操作实现的,属于逻辑格式的日志。
undo Log 的数据默认在系统表空间 ibdata1 文件中,因为共享表空间不会自动收 缩,也可以单独创建一个 undo 表空间。(结构图可以看出)
用过 binlog 的伙伴应该知道binlog 有这几个功能:
- 记录DDL 和 DML 的逻辑日志
- 主从复制 (slave 拿到 master 的 binlog 再执行)
- 数据恢复
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
要注意两点:
- binlog_cache_size设过大,会造成内存浪费。binlog_cache_size设置过小,会频繁将缓冲日志写入临时文件。
- sync_binlog=0:表示刷新binlog时间点由操作系统自身来决定,操作系统自身会每隔一段时间就会刷新缓存数据到磁盘,这个性能最好。sync_binlog=1,代表每次事务提交时就会刷新binlog到磁盘,对性能有一定的影响。sync_binlog=N,代表每N个事务提交会进行一次binlog刷新。
另外,这里存在一个一致性问题,sync_binlog=N,数据库在操作系统宕机的时候,可能数据并没有同步到磁盘,于是再次重启数据库,会带来数据丢失问题。
MySQL 的 binlog 是多文件存储,定位一个 LogEvent 需要通过 binlog filename + binlog position,进行定位。
二阶段提交时指当一个事务跨越多个节点时,为了保证事务的 ACID特性,需要引入一个作为协调者的组件来统一掌控所有节点(称作参与者)的操作结果并最终指示这些节点是否要把操作结果进行真正的提交(比如将更新后的数据写入磁盘等等)。
因此,二阶段提交的算法思路可以概括为:参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作还是中止操作。
第一阶段:
第二阶段:包含两步:
写得是有点累啊,不过有大家的支持我觉得就是值得的,也是我更新的动力,不对的地方希望大家帮忙指正,觉得写得还可以的话麻烦大家帮我点个赞哈哈哈。
最后我们还是以一张图来将流程描述清楚:
有问题?可以给我留言或私聊
有收获?那就顺手点个赞呗~
当然,也可以到我的公众号下「6曦轩」,
回复“学习”,即可领取一份
【Java工程师进阶架构师的视频教程】~
回复“面试”,可以获得:
【本人呕心沥血整理的 Java 面试题】
回复“MySQL脑图”,可以获得
【MySQL 知识点梳理高清脑图】
由于我咧,科班出身的程序员,php,Android以及硬件方面都做过,不过最后还是选择专注于做 Java,所以有啥问题可以到公众号提问讨论(技术情感倾诉都可以哈哈哈),看到的话会尽快回复,希望可以跟大家共同学习进步,关于服务端架构,Java 核心知识解析,职业生涯,面试总结等文章会不定期坚持推送输出,欢迎大家关注~~~