持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第3天,点击查看活动详情
面试官:InnoDB存储引擎是如何更新一条数据的?
MySQL最常用的就是InnoDB存储引擎。首先系统通过一个数据库连接发送到了MySQL上,然后经过SQL接口、解析器、优化器、执行器几个环节,解析SQL语句,生成执行计划,接着去由执行器负责这个计划的执行,调用InnoDB存储引擎的接口去执行。
内存结构:缓冲池
我们知道,对于使用InnoDB作为存储引擎的表来说,不管是用于存储用户数据的索引(包括聚簇索引和二级索引),还是各种系统数据,都是以页的形式存放在表空间中的,而所谓的表空间只不过是InnoDB对文件系统上一个或几个实际文件的抽象,也就是说我们的数据说到底还是存储在磁盘上的。
但是磁盘的速度慢,所以InnoDB存储引擎在处理客户端的请求时,当需要访问某个页的数据时,就会把完整的页的数据全部加载到内存中,也就是说即使我们只需要访问一个页的一条记录,那也需要先把整个页的数据加载到内存中。将整个页加载到内存中后就可以进行读写访问了,在进行完读写访问之后并不着急把该页对应的内存空间释放掉,而是将其缓存起来,这样将来有请求再次访问该页面时,就可以省去磁盘IO的开销了。
InnoDB为了缓存磁盘中的页,在MySQL服务器启动的时候就向操作系统申请了一片连续的内存,他们给这片内存起了个名,叫做Buffer Pool(中文名是缓冲池)。那它有多大呢?这个其实看我们机器的配置,默认情况下Buffer Pool只有8M大小。
undoLog
假设这行数据的name原来是“zhangsan”,现在我们要更新为“lisi”,那么此时我们得先把要更新的原来的值,写入到undo日志文件中去。如果我们执行一个更新语句,要是他是在一个事务里的话,那么事务提交之前我们都是可以对数据进行回滚的,也就是把你更新为“lisi”的值回滚到之前的“zhangsan”去。所以为了考虑到未来可能要回滚数据的需要,这里会把你更新前的值写入undo日志文件,让你更新的数据可以回滚。
更新buffer pool
当我们把要更新的那行记录从磁盘文件加载到缓冲池,同时对他加锁之后,而且还把更新前的旧值写入undo日志文件之后,我们就可以正式开始更新这行记录了,更新的时候,先是会更新缓冲池中的记录,此时这个数据就是脏数据了。这里所谓的更新内存缓冲池里的数据,意思就是把内存里的这行数据的name字段修改为“lisi”。这个时候磁盘上这行数据的name字段还是“zhangsan”,但是内存里这行数据已经被修改了,所以就会叫他是脏数据。
Redo Log Buffer
现在已经把内存里的数据进行了修改,但是磁盘上的数据还没修改。那么此时万一MySQL所在的机器宕机了,必然会导致内存里修改过的数据丢失。这个时候,就必须要把对内存所做的修改写入到一个Redo Log Buffer里去,这也是内存里的一个缓冲区,是用来存放redoLog的。所谓的redoLog,就是记录下来你对数据做了什么修改,比如对这行记录修改了name字段的值为lisi”,这就是一个日志。这个redo日志其实是用来在MySQL突然宕机的时候,用来恢复你更新过的数据的。
面试官:如果还没提交事务,MySQL宕机了怎么办?
其实在数据库中,哪怕执行一条SQL语句,其实也可以是一个独立的事务,只有当你提交事务之后,SQL语句才算执行结束。所以这里我们都知道,到目前为止,其实还没有提交事务,那么此时如果MySQL崩溃,必然导致内存里Buffer Pool中的修改过的数据都丢失,同时你写入Redo Log Buffer中的redo日志也会丢失。
那么此时数据丢失要紧吗?其实是不要紧的,因为你一条更新语句,没提交事务,就代表他没执行成功,此时MySQL宕机虽然导致内存里的数据都丢失了,但是你会发现,磁盘上的数据依然还停留在原样子。也就是说那行数据的name字段的值还是老的值,“zhangsan”,所以此时你的这个事务就是执行失败了,没能成功完成更新,你会收到一个数据库的异常。然后当mysql重启之后,你会发现你的数据并没有任何变化。
所以此时如果mysql宕机,不会有任何的问题。
提交事务
接着我们想要提交一个事务了,此时就会根据一定的策略把redo日志从redo log buffer里刷入到磁盘文件里去。 这个策略是通过innodb_flush_log_at_trx_commit来配置的,他有几个选项。
- 0:提交事务的时候,不会把redo log buffer里的数据刷入磁盘文件的,此时可能你都提交事务了,结果mysql宕机了,然后此时内存里的数据全部丢失。相当于你提交事务成功了,但是由于MySQL突然宕机,导致内存中的数据和redo日志都丢失了。
- 1:你提交事务的时候,就必须把redo log从内存刷入到磁盘文件里去,只要事务提交成功,那么redo log就必然在磁盘里了。
- 2:提交事务的时候,把redo日志写入磁盘文件对应的os cache缓存里去,而不是直接进入磁盘文件,可能1秒后才会把os cache里的数据写入到磁盘文件里去。这种模式下,你提交事务之后,redo log可能仅仅停留在os cache内存缓存里,没实际进入磁盘文件,万一此时你要是机器宕机了,那么os cache里的redo log就会丢失,同样会让你感觉提交事务了,结果数据丢了。
binlog
redo log本身是属于InnoDB存储引擎特有的一个东西。
binlog是归档日志,他里面记录的是偏向于逻辑性的日志,类似于“对xxx表中的一行数据做了更新操作,更新以后的值是什么”。binlog不是InnoDB存储引擎特有的日志文件,是属于mysql server自己的日志文件。
在我们提交事务的时候,会把redo log日志写入磁盘文件中去。然后其实在提交事务的时候,我们同时还会把这次更新对应的binlog日志写入到磁盘文件中去。
跟InnoDB存储引擎进行交互的执行器这个组件,他会负责跟InnoDB进行交互,包括从磁盘里加载数据到Buffer Pool中进行缓存,包括写入undo日志,包括更新Buffer Pool里的数据,以及写入redo log buffer,redo log刷入磁盘,写binlog,等等。实际上,执行器是非常核心的一个组件,负责跟存储引擎配合完成一个SQL语句在磁盘与内存层面的全部数据更新操作。
binlog日志的刷盘策略
面试官:那么binlog的刷盘策略你了解吗?
有一个sync_binlog参数可以控制binlog的刷盘策略。
默认值是0,此时你把binlog写入磁盘的时候,其实不是直接进入磁盘文件,而是进入os cache内存缓存。所以跟之前分析的一样,如果此时机器宕机,那么你在os cache里的binlog日志是会丢失的。
设置为1的话,那么此时会强制在提交事务的时候,把binlog直接写入到磁盘文件里去,那么这样提交事务之后,哪怕机器宕机,磁盘上的binlog是不会丢失的。
commit标记
当我们把binlog写入磁盘文件之后,接着就会完成最终的事务提交,此时会把本次更新对应的binlog文件名称和这次更新的binlog日志在文件里的位置,都写入到redo log日志文件里去,同时在redo log日志文件里写入一个commit标记。在完成这个事情之后,才算最终完成了事务的提交。保持redo log日志与binlog日志一致。
更新后的脏数据刷盘
这个时候磁盘上的数据文件里的这行数据的name字段还是等于zhangsan这个旧的值。
MySQL有一个后台的IO线程,会在之后某个时间里,随机的把内存buffer pool中的修改后的脏数据给刷回到磁盘上的数据文件里去。当IO线程把buffer pool里的修改后的脏数据刷回磁盘的之后,磁盘上的数据才会跟内存里一样,都是name=lisi这个修改以后的值了。