本篇文章主要关注MySQL在执行UPDATE操作时的处理策略,以及MySQL选择对应处理策略的原因。
我们知道MySQL中执行增删改操作时都会记录undo log,设计undo log一方面的目的是为了保证事务的原子性,另一方面也是为了实现MVCC。
在执行UPDATE语句时,InnoDB对更新键值和不更新键值这两种情况有截然不同的处理方案。
更新非键值
在不更新主键的情况下,又可以细分为被更新的列占用的存储空间不发生变化和发生变化的情况。如果被更新的列占用的存储空间不发生变化,那就可以进行原地更新,否则就只能先删除再插入。
1.原地更新(in-place update)
更新记录时,对于被更新的每个列来说,如果更新后的列和更新前的列占用的存储空间都一样大,那么就可以进行原地更新,也就是直接在原记录的基础上修改对应列的值。
2.先删除再插入
当不满足原地更新的条件时,就只能先删除旧记录再插入新纪录。这里的删除不是简单的delete mark,而是会由当前线程真正地删除数据再插入。要注意的是,先删除再插入的这一步在InnoDB里是一个原子操作,所以不会存在并发问题。
更新键值
那更新键值记录的情况是什么样子呢?划重点了!更新键值的时候是先执行delete mark然后再insert的,不会立刻删除真实数据,而是最后由后台线程负责执行purge操作。
原地更新的情况我们暂时不讨论,主要关注另外两种策略。那么问题来了,为什么更新键值和更新非键值的情况下InnoDB选择的策略不一样呢?
其实关键在于非键值在检索的时候一定会走全表扫描,但是使用键值的时候不会。当我们通过非键值查询数据的时候,InnoDB将不得不进行全表扫描,这样的话InnoDB最终必然能找到我们新插入的那一条记录,进而就能通过版本链找到符合当前可见性的历史数据。
那为什么更新键值的时候不能直接删除数据呢?也做成原子操作不行吗?
答案是不行,因为如果我们更新键值的时候也像更新非键值一样直接删除数据再插入新数据,那么就会出现下面的情况:事务交替执行的情况
假设当前隔离级别为REPEATABLE READ,事务B第一次查询主键为1的数据的时候返回了一条数据。当事务A执行完更新键值的操作后,事务B再次执行相同的查询,就会突然发现刚才的数据不见了。显然这是不符合REPEATABLE READ的要求的,MVCC直接没法儿玩了。
以上就是InnoDB在执行UPDATE操作时“区别对待”更新键值和非键值的原因。
PS:感谢阅读,老铁们,下篇文章见!