草庐IT

我打赌!这个 SQL 题,大部分人答不出来

小林coding 2023-03-28 原文
大家好,我是小林。

周末的时候,一个读者问了我一个很有意思的问题,是关于 MySQL 中 update 加锁的问题。

他用下面这张数据库表,做了个 MySQL 实验的时候。

发现事务 B 的 update 不会阻塞,而事务 C 的 update 会阻塞,都是对 id = 10 这条记录进行 update, 为什么一个会阻塞,一个不会阻塞?

首先,我们先来分析下,事务 A 这条 SQL 加了什么锁。

// 事务 A
select * from t_person where id < 10 for update;
我直接说结论,事务 A  加了这三个行级锁:

  • 在 id 为 1 的主键索引上,加了 X 型的 next-key 锁,范围是 (-∞,1]。意味着,其他事务无法对 id = 1 的记录进行删除和更新操作,同时无法插入 id 小于 1 的新记录。
  • 在 id 为 5 的主键索引上,加了 X 型的 next-key 锁,范围是 (1, 5]。意味着,其他事务无法对 id = 5 的记录进行删除和更新操作,同时无法插入 id 为 2、3、4 的新记录。
  • 在 id 为 10 的主键索引上,加了 X 型的间隙锁,范围是 (5, 10)。意味着,其他事务无法插入 id 为 6、7、8、9 的新纪录。
PS:如果你不清楚什么是 MySQL 这些行级锁(记录锁、间隙锁、next-key 锁),以及不清楚行级锁的加锁规则,强烈建议先看我之前写的这篇:​​MySQL 是怎么加行级锁的?​​,看完后,你回头看我这篇文章,就会有感觉的了。

事务 B 的 update 语句为什么不会阻塞?

事务 B 的 update 语句是对 id = 10 的行记录的 name 字段进行更新。

// 事务 B
update t_person set name = "小林" where id = 10;
事务 B 会在 id = 10 的主键索引上加 X 型记录锁,仅锁住这一行。因为当我们用唯一索引进行等值查询的时候,查询的记录是「存在」的,在索引树上定位到这一条记录后,该记录的索引中的 next-key 锁会退化成「记录锁」。

事务 A 并没有对 id = 10 的主键索引上加 X 型记录锁,而是对 id = 10 的主键索引上加 X 型间隙锁。间隙锁和记录锁之间是没有互斥关系的,所以事务 B 的 update 语句不会阻塞。

事务 C 的 update 语句为什么会阻塞?

事务 C 的 update 语句是将 id = 10 的行记录的 id 更新为 2。

// 事务 C
update t_person set id = 2 where id = 10;
这条 update 很特殊,特殊之处在于更新了主键索引。你以为它只是一个更新操作,实际上它在背后执行了两个操作:

  • 操作 1:delete from t_person where id = 10;
  • 操作 2:insert into t_person (2, 陈某,  30, 广州市海珠区);
也就是先删除 id = 10 的记录,然后再插入 id = 2 的新纪录。

为什么当 update 语句更新了索引值,会被拆分成删除和插入操作?

要回答这个问题,我们先要清楚 B+ 树的特点。

Innodb(MySQL 存储引擎)在实现索引的时候,采用的数据结构是 B+ 树。B+ 树是基于二分查找树演变过来的,所以 B+ 树在存储索引的时候,是按顺序存储的,因为这样才能利用二分查找快速检索到索引。

现在有一颗这样的  B+ 树,可以看到叶子节点的索引值是从小到大的顺序。

假设这时候需要将索引值为 25 更新为 3,如果直接索引值为 25 的位置上,将值改为 3 的话。

这时候你就会发现这棵 B+ 树不满足顺序性了!

所以更新索引的值,不能只是修改一个索引值就完事,而是还要保证更新后的索引值能继续满足  B+ 树的顺序性。

解决的方法就是,先删除索引值为 25 的节点,再插入索引值为 3 的节点,这样,这颗 B+ 树才能满足顺序性。

事务 C 的 update  语句具体阻塞在哪个「操作」?

现在我们知道,事务 C 的 update 特殊语句背后执行了两个操作,分别是删除和插入操作,那具体是阻塞在哪个「操作 」?

「操作 1 」是删除 id = 10 的记录,事务 C 是会在 id = 10 的主键索引上加 X 型记录锁,而事务 A 并没有对 id = 10 的主键索引上加 X 型记录锁,而是对 id = 10 的主键索引上加 X 型间隙锁。间隙锁和记录锁之间是没有互斥关系的,所以「操作 1 」不会阻塞。

根据排除法,既然 「操作 1 」不会阻塞,那事务 C 的 update 语句阻塞的原因就是因为 「操作 2」发生了阻塞。

为什么「操作2」会发生阻塞呢?

我们先要知道,插入操作什么时候会发生阻塞:插入语句在插入一条新记录之前,需要先定位到该记录在 B+树的位置,如果插入的位置的下一条记录的索引上有间隙锁,此时会生成一个插入意向锁,然后锁的状态设置为等待状态,现象就是插入语句会被阻塞。

「操作 2」插入的是 id = 2 的新记录,在主键索引的 B+树定位到插入的位置如下图。

插入位置的下一条记录是 id = 5 的记录,而事务 A 在 id 为 5 的主键索引上已经加了 X 型的 next-key 锁,这里面包含了间隙锁。所以「操作 2」的插入操作会发生阻塞,这就是事务 C 的 update 语句阻塞的原因。

从这我们也可以知道间隙锁的作用,就是阻止其他事务在间隙锁的范围内插入新记录,从而避免可重复读隔离级别下幻读的现象。

我们也可以通过 select * from performance_schema.data_locks\G; 这条语句,查看事务 C 在加什么锁的时候导致阻塞。

从上面的输出信息,可以看到事务 C 在加「插入意向锁」的时候,发生了阻塞。

插入意向锁是插入操作才会有的锁,而事务 C 只是执行 update 语句,却出现了插入意向锁,从这里也可以证明,事务 C 这条特殊的 update 语句运行的时候,被拆分成了两个操作,一个是删除,另一个是插入。

总之,如果 update 语句更新的是普通字段的值,就会对发生更新的记录加 X 型记录锁。

但是,如果 update 语句更新的是索引的值,那么在运行的时候会被拆分成删除和插入操作,这时候分析锁的时候,要从这两个操作的角度去分析。

完啦!

怎么样,够不够细节?

有关我打赌!这个 SQL 题,大部分人答不出来的更多相关文章

  1. Hive SQL 五大经典面试题 - 2

    目录第1题连续问题分析:解法:第2题分组问题分析:解法:第3题间隔连续问题分析:解法:第4题打折日期交叉问题分析:解法:第5题同时在线问题分析:解法:第1题连续问题如下数据为蚂蚁森林中用户领取的减少碳排放量iddtlowcarbon10012021-12-1212310022021-12-124510012021-12-134310012021-12-134510012021-12-132310022021-12-144510012021-12-1423010022021-12-154510012021-12-1523.......找出连续3天及以上减少碳排放量在100以上的用户分析:遇到这类

  2. sql - 查询忽略时间戳日期的时间范围 - 2

    我正在尝试查询我的Rails数据库(Postgres)中的购买表,我想查询时间范围。例如,我想知道在所有日期的下午2点到3点之间进行了多少次购买。此表中有一个created_at列,但我不知道如何在不搜索特定日期的情况下完成此操作。我试过:Purchases.where("created_atBETWEEN?and?",Time.now-1.hour,Time.now)但这最终只会搜索今天与那些时间的日期。 最佳答案 您需要使用PostgreSQL'sdate_part/extractfunction从created_at中提取小时

  3. ruby - 这个 ruby​​ 注入(inject)魔术是如何工作的? - 2

    我今天看到了一个ruby​​代码片段。[1,2,3,4,5,6,7].inject(:+)=>28[1,2,3,4,5,6,7].inject(:*)=>5040这里的注入(inject)和之前看到的完全不一样,比如[1,2,3,4,5,6,7].inject{|sum,x|sum+x}请解释一下它是如何工作的? 最佳答案 没有魔法,符号(方法)只是可能的参数之一。这是来自文档:#enum.inject(initial,sym)=>obj#enum.inject(sym)=>obj#enum.inject(initial){|mem

  4. ruby-on-rails - ruby 新手,有人可以帮我从控制台破译这个错误吗? - 2

    我真的只是不确定这意味着什么或我应该做什么才能让网页在我的本地主机上运行。现在它只是显示一个错误,上面写着“我们很抱歉,但出了点问题。”当我运行railsserver并在chrome中打开localhost:3000时。这是控制台输出:StartedGET"/users/sign_in"for127.0.0.1at2013-07-0512:07:07-0400ProcessingbyDevise::SessionsController#newasHTMLCompleted500InternalServerErrorin55msNoMethodError(undefinedmethod`

  5. sql - 在 Rails Console for PostgreSQL 的表中显示数据 - 2

    我找到了这样的东西:Rails:Howtolistdatabasetables/objectsusingtheRailsconsole?这一行没问题:ActiveRecord::Base.connection.tables并返回所有表但是ActiveRecord::Base.connection.table_structure("users")产生错误:ActiveRecord::Base.connection.table_structure("projects")我认为table_structure不是Postgres方法。如何列出Postgres数据库的Rails控制台中表中的所有

  6. ruby - 为什么这个救援语法有效? - 2

    好的,所以我有了我正在使用的应用程序的这种方法,它可以在生产中使用。我的问题为什么这行得通?这是新的Ruby语法吗?defeditload_elements(current_user)unlesscurrent_user.role?(:admin)respond_todo|format|format.json{render:json=>@user}format.xml{render:xml=>@user}format.htmlendrescueActiveRecord::RecordNotFoundrespond_to_not_found(:json,:xml,:html)end

  7. ruby - 为什么这个 eval 在 Ruby 中不起作用 - 2

    你能解释一下吗?我想评估来自两个不同来源的值和计算。一个消息来源为我提供了以下信息(以编程方式):'a=2'第二个来源给了我这个表达式来评估:'a+3'这个有效:a=2eval'a+3'这也有效:eval'a=2;a+3'但我真正需要的是这个,但它不起作用:eval'a=2'eval'a+3'我想了解其中的区别,以及如何使最后一个选项起作用。感谢您的帮助。 最佳答案 您可以创建一个Binding,并将相同的绑定(bind)与每个eval相关联调用:1.9.3p194:008>b=binding=>#1.9.3p194:009>eva

  8. ruby - 防止SQL注入(inject)/好的Ruby方法 - 2

    Ruby中防止SQL注入(inject)的好方法是什么? 最佳答案 直接使用ruby?使用准备好的语句:require'mysql'db=Mysql.new('localhost','user','password','database')statement=db.prepare"SELECT*FROMtableWHEREfield=?"statement.execute'value'statement.fetchstatement.close 关于ruby-防止SQL注入(inject

  9. ruby-on-rails - 如何在 Rails 中的不同数据库上执行直接 SQL 代码 - 2

    我正在编写一个Rails应用程序,它将监视某些特定数据库的数据质量。为了做到这一点,我需要能够对这些数据库执行直接SQL查询——这当然与用于驱动Rails应用程序模型的数据库不同。简而言之,这意味着我无法使用通过ActiveRecord基础连接的技巧。我需要连接的数据库在设计时是未知的(即:我不能将它们的详细信息放在database.yaml中)。相反,我有一个模型“database_details”,用户将使用它来输入应用程序将在运行时执行查询的数据库的详细信息。因此与这些数据库的连接实际上是动态的,细节仅在运行时解析。 最佳答案

  10. Ruby:我怎样才能复制这个数组? - 2

    (跟进我之前的问题,Ruby:howcanIcopyavariablewithoutpointingtothesameobject?)我正在编写一个简单的Ruby程序来在.svg文件中进行一些替换。第一步是从文件中提取信息并将其放入数组中。为了避免每次调用此函数时都从磁盘读取文件,我尝试使用memoize设计模式-在第一次调用后的每次调用中都使用缓存结果。为此,我使用了一个在函数之前定义的全局变量。但是,即使我在返回局部变量之前将该变量.dup为局部变量,调用该变量的函数仍在修改全局变量。这是我的实际代码:#memoizetokeepfromhavingtoreadoriginalfi

随机推荐