Mysql - 图1

一条更新语句是怎么执行的?

Mysql - 图2
大体来说,MySQL 可以分为 Server 层和存储引擎层两部分。
Server 层包括连接器、查询缓存、分析器、优化器、执行器等,涵盖 MySQL 的大多数核 心服务功能,以及所有的内置函数(如日期、时间、数学和加密函数等),所有跨存储引擎 的功能都在这一层实现,比如存储过程、触发器、视图等。而存储引擎层负责数据的存储和提取。其架构模式是插件式的,支持 InnoDB、MyISAM、Memory 等多个存储引擎。现在最常用的存储引擎是 InnoDB,它从 MySQL 5.5.5 版本开始成为了默认存储引擎。

当在一个表上有更新的时候,跟这个表有关的查询缓存会失效,所以这条语句就会把表 T 上所有缓存结果都清空。所以一般不建议使用查询缓存。
连接数据库之后,分析器会通过词法和语法解析知道这是一条更新语句。优化器决定要使用哪个索引。然后,执行器负责具体执行,找到这一行,然后更新。
与查询流程不一样的是,更新流程还涉及两个重要的日志模块:redo log(重做日志)和 binlog(归档日志)。

幻读是什么?

image.png

  1. 在可重复读隔离级别下,普通的查询是快照读,是不会看到别的事务插入的数据的。因
    此,幻读在“当前读”下才会出现。
  2. 幻读仅专指“新插入的行”。

由于 session A 把所有的行都加了写锁,所以 session B 在执行第一个 update 语句的时
候就被锁住了。需要等到 T6 时刻 session A 提交以后,session B 才能继续执行。
这样对于 id=0 这一行,在数据库里的最终结果还是 (0,5,5)。在 binlog 里面,执行序列是
这样的:

  1. insert into t values(1, 1, 5); /*(1, 1, 5)*/
  2. update t set c = 5 where id = 1; /*(1, 5, 5)*/
  3. update t set d = 100 where d = 5; /*所有的d=5的行,d改成100*/
  4. update t set d = 5 where id = 0; /*(0, 0, 5)*/
  5. update t set c = 5 where id = 0; /*(0, 5, 5)*/

对于id=1 这一行,在数据库里面的结果是 (1,5,5),而根据 binlog 的执行结果是 (1,5,100),也就是说幻读的问题还是没有解决。为什么我们已经这么“凶残”地,把所有的记录都上了锁,还是阻止不了 id=1 这一行的插入和更新呢?
原因很简单。在 T3 时刻,我们给所有行加锁的时候,id=1 这一行还不存在,不存在也就加不上锁。
也就是说,即使把所有的记录都加上锁,还是阻止不了新插入的记录,这也是为什么“幻
读”会被单独拿出来解决的原因。

接下来,我们再看看 InnoDB 怎么解决幻读的问题

怎么解决幻读?

产生幻读的原因是,行锁只能锁住行,但是新插入记录这个动作,要更新的是记录之间的“间隙”。因此,为了解决幻读问题,InnoDB 只好引入新的锁,也就是间隙锁 (Gap Lock)。

t 主键索引上的行锁和间隙锁
image.png

这样当我们执行select * from t where d = 5 for update的时候,就不只是给数据库中的6个记录加入了行锁,还同时加入了7个间歇锁。这样就确保了无法再插入新的记录。

另外,间歇锁与间歇锁之间是不存在冲突关系的,因为我们都知道行锁与另一个行锁之间是存在冲突的,而跟间歇锁存在冲突关系的,是“往这个间歇中插入一个记录”。
image.png
这里 session B 并不会被堵住。因为表 t 里并没有 c=7 这个记录,因此 session A 加的是间隙锁 (5,10)。而 session B 也是在这个间隙加的间隙锁。它们有共同的目标,即:保护这个间隙,不允许插入值。但,它们之间是不冲突的。

next-key lock

间隙锁和行锁合称 next-key lock,每个 next-key lock 是前开后闭区间。也就是说,我们的表 t 初始化以后,如果用select * from t for update 要把整个表所有记录锁起来,就形成了 7 个 next-key lock,分别是 (-∞,0]、(0,5]、 (5,10]、 (10,15]、 (15,20]、(20, 25]、(25,+supremum]。

间隙锁和 next-key lock 的引入,帮我们解决了幻读的问题,但同时也带来了一些“困扰”。
比如并发可能会出现死锁问题
我们用两个session来模拟并发
image.png

  1. session A 执行 select … for update 语句,由于 id=9 这一行并不存在,因此会加上间
    隙锁 (5,10);
  2. session B 执行 select … for update 语句,同样会加上间隙锁 (5,10),间隙锁之间不会
    冲突,因此这个语句可以执行成功;
  3. session B 试图插入一行 (9,9,9),被 session A 的间隙锁挡住了,只好进入等待;
  4. session A 试图插入一行 (9,9,9),被 session B 的间隙锁挡住了。

至此,两个 session 进入互相等待状态,形成死锁。当然,InnoDB 的死锁检测马上就发现了这对死锁关系,让 session A 的 insert 语句报错返回了。