一条sql更新语句是如何执行的
在理解mysql是如何做到数据不丢失之前,我们得先知道一条更新语句在mysql中是如何执行的。和查询语句一样,更新的sql也要经过:
1.连接器——管理连接,权限验证
2.分析器——词法分析,语法分析
3.优化器——生成执行计划,进行索引选择
4.执行器——操作引擎,返回结果
与查询sql不一样的是,更新流程会涉及到mysql的两个重要的日志模块——bin log(归档日志)和redo log(重做日志)。
如下图,是查询流程和更新流程:
WAL技术和redo log
WAL全称是Write-Ahead Loggin,即先写日志,再写磁盘(很多架构也都会如此设计),这里的日志就是redo log,写磁盘是将变更后的数据写到数据库文件ibdata。
WAL技术是为了提高mysql的性能,IO交互(即读写磁盘)是数据库中最耗时的操作,如果每次更新后都将数据刷到磁盘,mysql性能就会很慢(因为是随机写)。磁盘有一个特点,顺序写的速度比随机写的速度快(顺序写不需要频繁变换磁道),所以mysql先将更新的操作顺序写入redo log中,后台再慢慢将redo log中的变更刷入磁盘。
redo log是InnoDB引擎特有的,并且大小是固定的,一般是一组4个文件,每个文件大小是1GB,并且是循环写的。
所以redo log总共可以记录4GB的操作,如果记满了mysql就要放下手头的工作(即停止一切更新行为),先将redo log上的一部分操作刷到磁盘上,然后擦除那部分操作记录。
checkpoint表示当前redo log擦除的位置,write pos是目前写到了什么位置,write pos到checkpoint之间是空白的部分。如果有新的更新操作,write pos则会向前推进,如果后台刷了磁盘,checkpoint则会向前推进。当write pos推进太快(更新操作太频繁)超过了checkpoint,这个时候mysql就要停下来,先刷一些记录到磁盘,把checkpoint推进一下。
为何更加直观的表示循环写,下面是redo log的环形图:
bin log和两阶段提交
前面说过,mysql整体分为两大模块:server层和引擎层。redo log是InnoDB引擎特有的,所以其他存储引擎没有这个日志,而server层也有自己的日志——bin log是所有引擎都拥有的,所以所有的存储引擎都拥有归档日志。
redo log和bin log的区别
- redo log是InnoDB引擎特有的,binlog是MySQL的Server层实现的,所有引擎都可以使用。
- redo log是物理日志,记录了“在某个数据页上做了什么修改”,bin log是逻辑日志,记录了原始的sql语句。
- redo log大小固定,且循环写,bin log可以追加写,但有保留的时间上限。
两阶段提交
前面我们贴过了更新sql的流程,有了redo log和bin log的参与,我们以update T set c=c+1 where ID=2;为例补充一下里面的细节:
1.执行器先找引擎取ID=2这一行。如果ID=2这一行数据已经在内存中了,引擎直接返回给执行器;否则先从磁盘中读入到内存中,然后再返回。
2.执行器拿到引擎给的行数据,把这个值加上1,比如原来是N,现在就是N+1,得到新的一行数据,再调用引擎接口写入这行新数据。
3.引擎将这行新数据更新到内存中,同时将这个更新操作记录到redo log里面,此时redo log处于prepare状态。然后告知执行器执行完成了,随时可以提交事务。
4.执行器生成这个操作的binlog,并把binlog写入磁盘。
5.执行器调用引擎的提交事务接口,引擎把刚刚写入的redo log改成提交(commit)状态,更新完成。
你可能注意到了,redo log的写入拆成了两个步骤:perpare和commit,这就是两阶段提交。为什么要两阶段提交
两阶段提交是为了mysql发生异常重启时redo log和bin log日志的逻辑统一,以及mysql发生异常重启后数据的完整性。
我们仍然用前面的update语句来做例子,用反正法来说明,如果没有两阶段提交,会有什么问题。
如果没有两阶段提交:无非就是先写redo log再写bin log或者先写bin log再写redo log两种情况。
1.先写redo log再写bin log。假设redo log写完,bin log还没有写完,mysql异常重启。由于redo log已经commit了,mysql异常重启后数据可以恢复,所以恢复后这一行c的值是1。但是由于bin log没有记录这个语句,如果这天刚好要用全备+bin log的方式恢复出一个临时库,由于这个bin log的丢失,临时库上就少了一次更新,临时库中c=0,与原库的值不同。
2.先写bin log再写redo log。假设bin log写完,redo log还没有写,mysql异常重启。由于redo log没有commit,mysql异常重启后这个更新回滚了,所以回滚后这一行c的值是0。但是用于bin log已经记录了这个语句,同样的问题,如果这天刚好要用全备+bin log的方式恢复出一个临时库,临时库上就多了一次更新,临时库中c=1,还是与原库的值不同。
change buffer
由于mysql运用了WAL技术,在加上两阶段提交,如果要更新的记录恰好在内存中(InnoDB buffer pool),那么更新语句执行一次内存更新+两次顺序写日志就结束了(redo log和bin log);如果要更新的记录不在内存中,那么mysql要将记录行所在的整页数据读入到内存中,这里就成了更新时的瓶颈,因为要进行一次随机的IO读交互。
change buffer使用场景
mysql为了优化上面的这种场景,引入了change buffer。但是change buffer只能应用在非唯一索引变更的场景中,因为唯一索引变更需要进行是否唯一性的校验,这样就需要将数据读入到内存中,既然数据已经在内存中了,就没有必要应用change buffer,直接更新内存即可。
所以,change buffer是为了在更新的数据不涉及唯一索引变更时,且要更新的数据不在内存中,为了减少随机的IO读,先将更新的操作放入到change buffer中,最终是为了加快update语句的速度。
change buffer用的是buffer pool中的内存,所以不能无限增大,可以通过参数innodb_change_buffer_max_size来动态设置,比如设置成50表示change buffer的大小最多只能占用buffer pool的50%。
change buffer中的数据何时更新
将change buffer中的操作应用到旧的数据页,得到最新结果的过程称为merge。
merge操作的主要3个时机:
- 原始数据页被加载到 buffer pool 时,即所在的数据也被访问了
- 系统有后台线程会定期merge,即purge线程会每隔1秒钟时间去刷盘
- 数据库正常关闭(shutdown)的过程中,也会执行merge操作
以第一种时机举例,merge的执行流程是这样的:
1.从磁盘读入数据页到内存(老版本的数据页)
2.从change buffer里找出这个数据页的change buffer 记录(可能有多个),依次应用,得到新版数据页
3.写redo log,这个redo log包含了数据的变更和change buffer的变更
change buffer是否会因为mysql的异常重启而丢失数据。答案是不会,虽然change buffer只更新内存,但是在事务提交的时候,我们把change buffer的操作也会记录到redo log里了,所以崩溃恢复的时候,change buffer可以靠redo log找回来。change buffer写完之后mysql就当机了,这部分数据当然是丢失了,但是针对这部分数据事务是没有commit的,所以没有影响。
change buffer更适合写多读少的业务
前面说了change buffer是为了更新时,在保证数据唯一性的基础上,减少随机访问IO的操作。所以在一个数据页做merge之前,change buffer记录的变更越多(也就是这个页面上要更新的次数越多),收益就越大。
假设一个业务的更新模式是写入之后马上会做查询,那么即使满足了条件,将更新先记录在change buffer,但之后由于马上要访问这个数据页,会立即触发merge过程。这样随机访问IO的次数不会减少,反而增加了change buffer的维护代价。
小结
今天主要讲述了一个更新sql在mysql中是如何执行的,以及在更新时涉及到的两大日志模块redo log和bin log。
为了解决在更新时随机写IO的低效率问题,InnoDB引入了WAL技术(先写日志,后写磁盘),并且进一步引入了redo log来顺序记录具体的操作。
为了保证redo log和bin log逻辑上的统一,mysql在更新时执行了两阶段提交。
为了在更新时减少随机读IO的低效率问题,InnoDB又引入了change buffer,来记录非唯一主键的变更操作。