1. 更新语句执行流程概述
在 MySQL 中,一条更新语句(如 update T set c=c+1 where ID=2
)的执行涉及多个关键步骤,与查询语句类似,会经过连接器、分析器、优化器、执行器等 Server 层模块,最终到达存储引擎(如 InnoDB),且在执行过程中与 redo log 和 binlog 这两个重要日志模块紧密相关。
2. 各组件在更新语句执行中的作用
2.1. 连接器
连接器负责管理数据库连接与用户权限验证。在执行更新语句前,客户端需先连接数据库,此过程由连接器完成。
并且,一旦表有更新操作,连接器会清空该表相关的查询缓存,这也是不建议使用查询缓存的原因之一,因为缓存频繁失效会降低性能。
比如,在一个电商系统中,当频繁更新商品库存信息时,如果使用查询缓存,每次更新都会导致与商品表相关的缓存被清空,后续相同的查询就无法利用缓存,增加了数据库的查询压力。
2.2. 分析器
对 SQL 语句进行词法和语法解析,识别出这是一条更新语句,为后续的执行流程奠定基础。
若语句存在词法或语法错误,分析器会报错并终止执行。例如,若写成 update T set c+1 where ID=2
(少了 c
的赋值部分),分析器会检测到语法错误并提示用户修改。
2.3. 优化器
决定执行更新操作所使用的索引,其目标是选择最优的执行计划,以提高更新操作的效率。
例如,在有主键 ID
的表 T
中更新数据时,优化器会判断使用 ID
索引来定位要更新的行。
就像在一个拥有大量用户数据的表中,如果要更新特定用户的信息,优化器会根据索引情况快速定位到该用户所在的数据页,减少数据查找的时间。
2.4. 执行器
具体执行更新操作。
首先,它会根据优化器的决策,通过引擎接口查找 ID = 2
的行。若该行所在的数据页已在内存中,则直接获取;否则,需从磁盘读入内存。
接着,执行器将获取到的行数据中 c
值加 1,并调用引擎接口写入新数据。
之后,执行器会生成该操作的 binlog,并写入磁盘,最后调用引擎的提交事务接口完成更新操作。
3. redo log 详解
3.1. 引入原因
我们用一个故事来解释一下这个问题。
在古代,酒馆掌柜一般会有一个粉板,专门用来记录客人的赊账记录。如果赊账的⼈不多,那么他可以把顾客名和账⽬写在板上。但如果赊账的⼈多了,粉板总会有记不下的时候,这个时候掌柜⼀定还有⼀个专⻔记录赊账的账本。
如果有⼈要赊账或者还账的话,掌柜⼀般有两种做法:⼀种做法是直接把账本翻出来,把这次赊的账加上去或者扣除掉;另⼀种做法是先在粉板上记下这次的账,等打烊以后再把账本翻出来核算。
在⽣意红⽕柜台很忙时,掌柜⼀定会选择后者,因为前者操作实在是太麻烦了。⾸先,你得找到这个⼈的赊账总额那条记录。你想想,密密麻麻⼏⼗⻚,掌柜要找到那个名字,可能还得带上⽼花镜慢慢找,找到之后再拿出算盘计算,最后再将结果写回到账本上。
这整个过程想想都麻烦。相⽐之下,还是先在粉板上记⼀下⽅便。你想想,如果掌柜没有粉板的帮助,每次记账都得翻账本,效率是不是低得让⼈难以忍受?
在 MySQL ⾥也有这个问题,如果每⼀次的更新操作都需要写进磁盘,然后磁盘也要找到对应的那条记录,然后再更新,整个过程 IO 成本、查找成本都很⾼。为了解决这个问题,MySQL 的设计者就⽤了类似酒馆掌柜粉板的思路来提升更新效率。这个就是 MySQL ⾥经常说到的 WAL 技术。
3.2. WAL 技术说明
WAL 的全称是 Write-Ahead Logging,它的关键点就是先写⽇志,再写磁盘。
当有⼀条记录需要更新的时候,InnoDB 引擎就会先把记录写到 redo log ⾥⾯,并更新内存,这个时候更新就算完成了。
InnoDB 引擎会在适当的时候,将这个操作记录更新到磁盘⾥⾯,⽽这个更新往往是在系统⽐较空闲的时候做。
比如在一个高并发的在线交易系统中,大量的订单数据更新操作如果都直接写磁盘,会严重影响系统性能。采用 WAL 机制后,先将更新记录到 redo log,能快速响应交易请求,提升系统的吞吐量和响应速度。
3.3. redo log 结构与循环写机制
redo log 是固定大小的,可配置为一组多个文件(如 4 个文件,每个文件 1GB,共 4GB),采用循环写的方式。
有两个关键指针,write pos
表示当前记录的位置,一边写一边后移,写到末尾则回到开头循环;checkpoint
是当前要擦除的位置,也是循环推移的。
write pos
和 checkpoint
之间的空间用于记录新操作,当 write pos
追上 checkpoint
时,表示 redo log 已满,此时需先擦掉部分记录,推进 checkpoint
后才能继续执行新的更新操作。
假设一个数据库系统的 redo log 配置为上述大小,在业务高峰期,大量的更新操作可能会使 write pos
快速接近 checkpoint
。当两者接近时,系统会暂停新的更新,先将部分记录从 redo log 同步到磁盘,移动 checkpoint
,为新的更新操作腾出空间,确保系统的持续运行。
3.4. crash-safe 能力
redo log 保证了即使数据库发生异常重启,之前提交的记录也不会丢失,此能力称为 crash-safe。
因为只要记录在 redo log 或已同步到磁盘中,系统就能在重启后恢复数据,如同酒店掌柜即使停业几天,仍可通过账本和粉板上的数据明确赊账账目。
例如,在一次数据库服务器突然断电的情况下,重启后系统可以根据 redo log 中的记录恢复到断电前的状态,保证数据的完整性和一致性。
4. binlog 详解
4.1. 与 redo log 的区别
4.1.1. 所属层次与引擎通用性
redo log 是 InnoDB 引擎特有的日志,而 binlog 是 MySQL 的 Server 层实现的,所有引擎都可使用。
由于 MySQL 历史发展过程中,早期自带的 MyISAM 引擎没有 crash-safe 能力,binlog 仅用于归档。
后来引入 InnoDB 引擎后,为实现 crash-safe 能力,InnoDB 使用了 redo log,但 binlog 依然保留其在 Server 层的作用。
在一个同时使用了 MyISAM 和 InnoDB 引擎的数据库系统中,binlog 可以统一记录所有引擎的逻辑操作,而 redo log 则仅服务于 InnoDB 引擎的事务安全和数据恢复。
4.1.2. 记录内容性质
redo log 是物理日志,记录的是在某个数据页上做了什么修改。
binlog 是逻辑日志,记录的是语句的原始逻辑,例如 给 ID=2 这一行的 c 字段加 1
。
比如执行 update T set c=c+1 where ID=2
语句,redo log 会记录数据页中 ID=2
这一行数据的具体修改细节,如某个字节的变化;而 binlog 则记录 update T set c=c+1 where ID=2
这个完整的语句逻辑。
4.1.3. 写入方式
redo log 是循环写,空间固定会用完。
binlog 是可以追加写入的,即文件写到一定大小后会切换到下一个文件,不会覆盖以前的日志。
在长期运行的数据库系统中,redo log 可能会因为循环写而覆盖早期的记录,但 binlog 则会持续保存所有的历史逻辑操作,这对于数据的长期归档和审计非常重要。
4.2. 在数据恢复中的作用
binlog 用于记录所有的逻辑操作,且采用追加写形式。
在数据恢复场景中,DBA 通常会定期做全量备份(备份周期取决于系统重要性,可为一天一备或一周一备等),同时保存最近半个月的所有 binlog。
当需要恢复到指定某一秒时,先找到最近的全量备份恢复到临时库,再从备份时间点开始,依次取出 binlog 重放到误操作之前的时刻,使临时库与误删之前的线上库一致,最后可按需将数据恢复到线上库。
例如,在一个企业级的数据库应用中,如果误删除了某个重要表的数据,DBA 可以利用最近的全量备份和相应时间段的 binlog,将数据恢复到误删前的状态,最大程度减少数据损失。
5. 两阶段提交
5.1. 引入原因
有了上面针对 redo log 和 binlog 的概念性理解,我们再来看执⾏器和 InnoDB 引擎在执⾏这个简单的 update 语句 update T set c=c+1 where ID=2
时的内部流程。
- 执⾏器先找引擎取 ID=2 这⼀⾏。ID 是主键,引擎直接⽤树搜索找到这⼀⾏。如果 ID=2 这
⼀⾏所在的数据⻚本来就在内存中,就直接返回给执⾏器;否则,需要先从磁盘读⼊内存,
然后再返回。
- 执⾏器拿到引擎给的⾏数据,把这个值加上 1,⽐如原来是 N,现在就是 N+1,得到新的⼀
⾏数据,再调⽤引擎接⼝写⼊这⾏新数据。
- 引擎将这⾏新数据更新到内存中,同时将这个更新操作记录到 redo log ⾥⾯,此时 redo
log 处于 prepare 状态。然后告知执⾏器执⾏完成了,随时可以提交事务。
-
执⾏器⽣成这个操作的 binlog,并把 binlog 写⼊磁盘。
-
执⾏器调⽤引擎的提交事务接⼝,引擎把刚刚写⼊的 redo log 改成提交(commit)状态,
更新完成。
下面用图片描述一下上述流程,图中浅⾊框表示是在 InnoDB 内部执⾏的,深⾊框表示是在执⾏器中执⾏的。
从上图应该可以看出来,我们将 redo log 的写入拆成了两个步骤:prepare 和 commit,这就是“两阶段提交”。
5.2. 两阶段提交的必要性
为保证 redo log 和 binlog 之间的逻辑一致性。
若不采用两阶段提交,无论是先写 redo log 再写 binlog,还是先写 binlog 后写 redo log,在发生崩溃时都可能导致数据库状态与用日志恢复出来的库状态不一致。
例如,执行 update T set c=c+1 where ID=2
时,假设当前 c
值为 0,若先写 redo log 后写 binlog,在 redo log 写完但 binlog 未写完时崩溃,恢复后原库 c
值为 1,但 binlog 丢失该语句,用 binlog 恢复临时库时 c
值为 0。
反之,先写 binlog 后写 redo log,binlog 写完 redo log 未写时崩溃,原库 c
值为 0,而用 binlog 恢复时 c
值为 1,均出现不一致情况。
在一个分布式数据库系统中,多个节点同时进行更新操作,如果不采用两阶段提交保证日志一致性,可能会导致节点间数据不一致,影响整个系统的正常运行。
6. 最后建议
redo log ⽤于保证 crash-safe 能⼒。innodb_flush_log_at_trx_commit
这个参数设置成 1 的时候,表示每次事务的 redo log 都直接持久化到磁盘。这个参数我建议你设置成 1,这样可以保证 MySQL 异常重启之后数据不丢失。
sync_binlog
这个参数设置成 1 的时候,表示每次事务的 binlog 都持久化到磁盘。这个参数我也建议你设置成 1,这样可以保证 MySQL 异常重启之后 binlog 不丢失。