15.Update语句执行流程之undolog

35 阅读6分钟

回顾Update语句执行流程

以一条更新语句: update t set c=c+1 where id=2 为例说明update语句的执行流程,图中浅色框表示是在InnoDB内部执行的,深色框表示是在执行器中执行的。建立连接、解析器、优化器等执行过程略。

image.png

1.执行器先找引擎取id=2这一行。id是主键,引擎直接用B+树搜索找到这一行。如果id=2这一行所在的数据页本来就在内存bufferpool中,就直接返回给执行器;否则,需要先从磁盘读入内存bufferpool,然后再返回,此时会对这行记录加独占锁。

2.写入undolog,保证数据可以回滚。

3.执行器拿到引擎给的行数据,把这个值加上1,比如原来是N,现在就是N+1,得到新的一行数据,引擎将这行新数据更新到内存bufferpool中(脏页)。

4.同时将这个更新操作记录到redo log buffer里面,此时redo log处于prepare状态。然后告知执行器执行完成了,随时可以提交事务。

4.执行器生成这个操作的binlog,并把binlog写入磁盘。

5.执行器调用引擎的提交事务接口,引擎把刚刚写入的redo log改成提交(commit)状态,更新完成。此时会把本次更新对应的 binlog 文件名称和这次更新的 binlog 文件里的位置,写入到 redo log 文件中,同时在 redo log 文件里写入一个 commit 标记。

6.如果触发刷新脏页的操作,则将内存更新后的脏数据刷回磁盘。

在之前的文章中我们根据update的执行流程分别介绍了两阶段提交和组提交以及BufferPool,接下来我们继续按照Update语句执行流程继续往下讲Undolog。

事务回滚的需求

我们说过事务需要保证原子性,也就是事务中的操作要么全部完成,要么什么也不做。但是偏偏有时候事务执行到一半会出现一些情况,比如:

  • 情况一:事务执行过程中可能遇到各种错误,比如服务器本身的错误,操作系统错误,甚至是突然断电导致的错误。
  • 情况二:程序员可以在事务执行过程中手动输入ROLLBACK语句结束当前的事务的执行。

这两种情况都会导致事务执行到一半就结束,但是事务执行过程中可能已经修改了很多东西,为了保证事务的原子性,我们需要把东西改回原先的样子,这个过程就称之为回滚(英文名:rollback),这样就可以造成一个假象:这个事务看起来什么都没做,所以符合原子性要求。

小时候我非常痴迷于象棋,总是想找厉害的大人下棋,赢棋是不可能赢棋的,这辈子都不可能赢棋的,又不想认输,只能偷偷的悔棋才能勉强玩的下去。悔棋就是一种非常典型的回滚操作,比如棋子往前走两步,悔棋对应的操作就是向后走两步;比如棋子往左走一步,悔棋对应的操作就是向右走一步。数据库中的回滚跟悔棋差不多,你插入了一条记录,回滚操作对应的就是把这条记录删除掉;你更新了一条记录,回滚操作对应的就是把该记录更新为旧值;你删除了一条记录,回滚操作对应的自然就是把该记录再插进去。

从上边的描述中我们已经能隐约感觉到,每当我们要对一条记录做改动时(这里的改动可以指INSERTDELETEUPDATE),都需要留一手,把回滚时所需的东西都给记下来。比方说:

  • 插入一条记录时,至少要把这条记录的主键值记下来,之后回滚的时候只需要把这个主键值对应的记录删掉就好了。
  • 删除了一条记录,至少要把这条记录中的内容都记下来,这样之后回滚时再把由这些内容组成的记录插入到表中就好了。
  • 修改了一条记录,至少要把修改这条记录前的旧值都记录下来,这样之后回滚时再把这条记录更新为旧值就好了。

这些为了回滚而记录的日志之为撤销日志即undo日志。这里需要注意的一点是,由于查询操作(SELECT)并不会修改任何用户记录,所以在查询操作执行时,并不需要记录相应的undo日志。在真实的InnoDB中,undo日志其实并不像我们上边所说的那么简单,不同类型的操作产生的undo日志的格式也是不同的。

事务回滚的场景

那么首先大家先思考一个问题,假设现在我们一个事务里要执行一些增删改的操作,那么必然是先把对应的数据页从磁盘加载出来放buffer pool的缓存页里,然后在缓存页里执行一通增删改,同时记录redo log日志,对吧?如下图。

但是现在问题来了,万一要是一个事务里的一通增删改操作执行到了一半,结果就回滚事务了呢?

比如一个事务里有4个增删改操作,结果目前为止已经执行了2个增删改SQL了,已经更新了一些buffer pool里的数据了,但是还有2个增删改SQL的逻辑还没执行,此时事务要回滚了怎么办?看图

image.png 这个时候就很尴尬了,如果你要回滚事务的话,那么必须要把已经在buffer pool的缓存页里执行的增删改操作给回滚了。

但是怎么回滚呢?毕竟无论是插入,还是更新,还是删除,该做的都已经做了啊!

所以在执行事务的时候,才必须引入另外一种日志,就是undo log回滚日志

这个回滚日志,他记录的东西其实非常简单,比如你要是在缓存页里执行了一个insert语句,那么此时你在undo log日志里,对这个操作记录的回滚日志就必须是有一个主键和一个对应的delete操作,要能让你把这次insert操作给回退了。

那么比如说你要是执行的是delete语句,那么起码你要把你删除的那条数据记录下来,如果要回滚,就应该执行一个insert操作把那条数据插入回去。

如果你要是执行的是update语句,那么起码你要把你更新之前的那个值记录下来,回滚的时候重新update一下,把你之前更新前的旧值给他更新回去。

如果你要是执行的是select语句呢?不好意思,select语句压根儿没有在buffer pool里执行任何修改,所以根本不需要undo log!

好,所以我们来看下图,其实你在执行事务期间,之前我们最开始的几篇文章就讲过,你除了写redo log日志还必须要写undo log日志,这个undo log日志是至关重要的,没有他,你根本都没办法回滚事务!

image.png

参考:blog.csdn.net/zht24564812…