前言
上一章我们讲了一条SQL是如何做查询的,其中经历了许多步骤。这次来讲讲一条SQL是如何做更新操作的。
常有大佬说他可以把MySQL恢复到半个月内任意一秒的状态,今天也来谈谈这是如何做到的呢?
一、SQL是如何做更新操作的
之前我们讲到了一条SQL的执行要经过连接器、查询缓存、分析器、优化器、执行器,最后到达存储引擎。其实更新语句也会同查询语句一样,把这些路都走一篇。不过会在此基础上更多一些步骤。还是以一条SQL为例子:
创建一个表T
mysql> create table T(ID int primary key, c int);
如果想把ID=10这行的值+1,SQL就是这样:
mysql> update T set c=c+1 where ID=10;
先通过连接器连接数据库。如果查询缓存中有值就取,没有就走下一步。,分析器会通过词法和语法解析知道这是一条更新语句。优化器决定要使用 ID 这个索引。然后,执行器负责具体执行,找到这一行,然后更新。
与查询流程不一样的是,更新流程还涉及两个重要的日志模块,它们正是我们今天要讨论的主角:redo log(重做日志)和 binlog(归档日志)。
二、MySQL中的redo log
设想一下,如果每一次的更新操作都需要写进磁盘,然后磁盘也要找到对应的那条记录,然后再更新,整个过程 IO 成本、查找成本都很高。那么MySQL是如何解决这个问题的呢?
这就不得不提到MySQL里常说的WAL技术。全称是 Write-Ahead Logging,它的关键点就是先写日志,再写磁盘,也就是先写把那些SQL都记录下来,等统一时间再来写入。
具体来说,当有一条记录要更新的时候,InnoDB引擎会把这条记录先写到redo log里,并更新内存,再等到系统比较空闲的时候把这个操作记录更新到磁盘。如果一直都很忙没有空闲,那么redo log就会先写入一部分,为后面留下空间。(InnoDB的redo log是固定大小的。比如可以配置为一组 4 个文件,每个文件的大小是 1GB,那么总共就可以记录 4GB 的操作,每次更新一部分到磁盘就可以把已更新的内容擦除)。
有了 redo log,InnoDB 就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为 crash-safe。也就是就算异常重启也能找到写在redo log中的SQL执行内容了。
三、MySQL中的binlog
redo log是InnoDB引擎特有的日志。Sever层也有自己的日志,binlog(归档日志)。
这两种日志有以下三点不同。
- redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用。
- redo log 是物理日志,记录的是“在某个数据页上做了什么修改”;binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如“给 ID=2 这一行的 c 字段加 1 ”。
- redo log 是循环写的,空间固定会用完;binlog 是可以追加写入的。“追加写”是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。
那么我们回头来看看上面的SQL在MySQL里是怎么执行的:
- Server层中的执行器先找引擎取 ID=10 这一行。根据主键ID,引擎直接找到这一行。如果 ID=10 这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。
- 执行器拿到引擎给的行数据,把这个值加上 1,得到新的一行数据,再调用引擎接口写入这行新数据。
- 引擎将这行新数据更新到内存中,同时将这个更新操作记录到 redo log 里面,此时 redo log 处于 prepare 状态。然后告知执行器执行完成了,随时可以提交事务。
- 执行器生成这个操作的 binlog,并把 binlog 写入磁盘。
- 执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交(commit)状态,更新完成。
下图浅绿色为在Server层执行,白色为引擎中执行。
看完你可能会问,写入redo log后这个prepare是啥意思,还有写完binlog又要commit提交事务。这里的prepare和commit就是将redo log拆成了两阶段提交。
四、聊聊两阶段提交
为什么必须有“两阶段提交”呢?这是为了让两份日志之间的逻辑一致。由于 redo log 和 binlog 是两个独立的逻辑,如果不用两阶段提交,要么就是先写完 redo log 再写 binlog,或者采用反过来的顺序。还是以前面的SQL举例。
1.如果先写redo log后写binlog,结果服务器故障了。redo log写完后c的值就已经+1了,但是由于binlog还没有写完就挂了,之后备份恢复的时候,binlog语句丢失,恢复的值还会是0。
2.如果先写binlog后写redo log。由于binlog写完之后挂了,redo log还没写,服务器恢复后发现事务无效,这个值还是0。但是binlog中已经记录了c从0变成1的日志。最后用binlog恢复的时候就会成为1,和原来库中的不同。
简单说,redo log 和 binlog 都可以用于表示事务的提交状态,而两阶段提交就是让这两个状态保持逻辑上的一致。
总结
MySQL的日志系统在确保数据完整性、持久性和恢复能力方面起着关键作用。
数据一致性:通过重做日志和撤销日志,即使在系统故障的情况下,也能保证数据的一致性。当系统重启时,可以使用重做日志来恢复未完成的事务。
复制和备份:二进制日志用于主从复制和数据备份。从服务器可以读取主服务器的二进制日志,以保持与主服务器相同的数据状态。这使得实现高可用性和负载均衡变得容易。
慢查询监控:查询日志和慢查询日志可以帮助我们识别和优化性能问题。通过分析这些日志,可以找到需要优化的SQL语句或配置。
审计:查询日志可以用于审计目的,跟踪对数据库的访问和修改操作。这对于安全性和合规性检查非常有用。
补充:
MySQL的日志:
二进制日志(Binary Log):记录了对数据库执行的所有修改操作,以二进制形式存储。主要用于复制和数据恢复。
重做日志(Redo Log):存在于InnoDB存储引擎中,用于保证事务的持久性。
撤销日志(Undo Log):也存在于InnoDB存储引擎中,用于支持事务的回滚操作和多版本并发控制。
查询日志(General Query Log)和慢查询日志(Slow Query Log):用于记录数据库的活动和慢查询。