【问题标题】:When using MySQL's FOR UPDATE locking, what is exactly locked?使用 MySQL 的 FOR UPDATE 锁定时,究竟锁定了什么?
【发布时间】:2011-08-29 07:14:13
【问题描述】:

这不是一个完整/正确的 MySQL 查询伪代码:

Select *
 from Notifications as n
 where n.date > (CurrentDate-10 days)
 limit by 1
 FOR UPDATE

http://dev.mysql.com/doc/refman/5.0/en/select.html 声明: 如果将 FOR UPDATE 与使用页锁或行锁的存储引擎一起使用,则查询检查的行将被写锁定,直到当前事务结束

这里是只有返回的一条记录被 MySQL 锁定还是它必须扫描所有记录才能找到单个记录?

【问题讨论】:

  • 锁定all Records it has to SCAN TO FIND the SINGLE RECORD 会非常愚蠢,我真的怀疑 MySQL 是这样工作的。想想 MySQL 搜索引擎中的算法——当它看到某行并知道这不是您需要的行时,为什么还要花额外的时间来设置锁?!我建议您不接受答案,以便其他 MySQL 人员对此发表评论
  • 另外,作为 Oracle DB 开发人员,我向您保证,Oracle 只会锁定满足 WHERE 条件的行。因此,这在技术上是可行的,我不认为 MySQL 那个 很多逊色
  • 虽然看起来我的答案最终是正确的,但我建议您选择另一个答案,因为它是正确的并且实际上已经测试过它,而我的只是指正如 Alexander 指出的那样,文档可以通过多种方式阅读。

标签: mysql sql


【解决方案1】:

我们为什么不试试呢?

设置数据库

CREATE DATABASE so1;
USE so1;
CREATE TABLE notification (`id` BIGINT(20), `date` DATE, `text` TEXT) ENGINE=InnoDB;
INSERT INTO notification(id, `date`, `text`) values (1, '2011-05-01', 'Notification 1');
INSERT INTO notification(id, `date`, `text`) values (2, '2011-05-02', 'Notification 2');
INSERT INTO notification(id, `date`, `text`) values (3, '2011-05-03', 'Notification 3');
INSERT INTO notification(id, `date`, `text`) values (4, '2011-05-04', 'Notification 4');
INSERT INTO notification(id, `date`, `text`) values (5, '2011-05-05', 'Notification 5');

现在,启动两个数据库连接

连接 1

BEGIN;
SELECT * FROM notification WHERE `date` >= '2011-05-03' FOR UPDATE;

连接 2

BEGIN;

如果 MySQL 锁定所有行,则以下语句将阻塞。如果它只锁定它返回的行,它不应该阻塞。

SELECT * FROM notification WHERE `date` = '2011-05-02' FOR UPDATE;

确实会阻塞。

有趣的是,我们也不能添加将被读取的记录,即

INSERT INTO notification(id, `date`, `text`) values (6, '2011-05-06', 'Notification 6');

还有块!

我现在不能确定 MySQL 是否会在一定百分比的行被锁定时继续锁定整个表,或者它实际上在确保SELECT ... FOR UPDATE 查询的结果永远不会真正聪明的地方在持有锁时被另一个事务(INSERTUPDATEDELETE)更改。

【讨论】:

  • +1 用于举例证明。这种行为看起来很可怕。您是否尝试仅锁定 1 行?你为什么不按id 列选择,我不相信日期文字:)。 MySQL/InnoDB 的版本是多少?
  • +1。如果对唯一列进行锁定,则不会阻塞整个表。我试过CREATE TABLE notification (id` BIGINT(20) NOT NULL AUTO_INCREMENT, date DATE, text TEXT, PRIMARY KEY (id)) ENGINE=InnoDB;` 有效。但是如果没有指定唯一/主键,它就不起作用,即。 SELECT * FROM notification WHERE id` = '1' FOR UPDATE;` 在原始架构上
  • 这个例子对我很有启发。尝试通过 ID 选择单个记录进行更新导致阻塞,直到我将 ID 字段设置为主键。然后,一切正常。重要的教训是仅在使用唯一键时才使用 FOR UPDATE,否则您的整个表也可能会被锁定,因为它会扫描整个表/索引以查找所有匹配项,从而锁定所有内容。
  • @fabspro 您不需要使用唯一键。任何键都可以使用,无论它是否唯一。
  • 我自己的测试表明,将for update 与非索引列上的 where 过滤器一起使用会导致整个表锁定,而与索引列上的 where 过滤器一起使用会导致过滤行锁定的所需行为。所以主键不是必须的,任何键都可以。
【解决方案2】:

我知道这个问题已经很老了,但我想分享一些我对索引列所做的相关测试的结果,这些测试产生了一些非常奇怪的结果。

表结构:

CREATE TABLE `t1` (                       
  `id` int(11) NOT NULL AUTO_INCREMENT,                 
  `notid` int(11) DEFAULT NULL,                         
  PRIMARY KEY (`id`)                                    
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

使用INSERT INTO t1 (notid) VALUES (1), (2),..., (12) 插入了 12 行。在连接 1

BEGIN;    
SELECT * FROM t1 WHERE id=5 FOR UPDATE;

连接2上,以下语句被阻止:

SELECT * FROM t1 WHERE id!=5 FOR UPDATE;
SELECT * FROM t1 WHERE id<5 FOR UPDATE;
SELECT * FROM t1 WHERE notid!=5 FOR UPDATE;
SELECT * FROM t1 WHERE notid<5 FOR UPDATE;
SELECT * FROM t1 WHERE id<=4 FOR UPDATE;

最奇怪的是SELECT * FROM t1 WHERE id&gt;5 FOR UPDATE;没有被阻止,也没有

...
SELECT * FROM t1 WHERE id=3 FOR UPDATE;
SELECT * FROM t1 WHERE id=4 FOR UPDATE;
SELECT * FROM t1 WHERE id=6 FOR UPDATE;
SELECT * FROM t1 WHERE id=7 FOR UPDATE;
...

我还想指出,当来自 connection 1 的查询中的 WHERE 条件与非索引匹配时,整个表似乎已被锁定排。例如,当 connection 1 执行 SELECT * FROM t1 WHERE notid=5 FOR UPDATE 时,所有来自 connection 2 的带有 FOR UPDATEUPDATE 查询的 select 查询都会被阻止。

-编辑-

这是一个相当具体的情况,但这是我能找到的唯一表现出这种行为的情况:

连接 1:

BEGIN;
SELECT *, @x:=@x+id AS counter FROM t1 CROSS JOIN (SELECT @x:=0) b HAVING counter>5 LIMIT 1 FOR UPDATE;
+----+-------+-------+---------+
| id | notid | @x:=0 | counter |
+----+-------+-------+---------+
|  3 |     3 |     0 |       9 |
+----+-------+-------+---------+
1 row in set (0.00 sec)

来自连接 2

SELECT * FROM t1 WHERE id=2 FOR UPDATE; 被屏蔽;

SELECT * FROM t1 WHERE id=4 FOR UPDATE;阻止。

【讨论】:

  • 这很有趣。我想知道为什么也会发生这些阻塞。
  • 这是因为基于索引的间隙锁。
  • 这可能是由于表中的行数非常少。数据库可以选择绕过索引并直接进行全表扫描以获取行。 id=5。这解释了为什么 id 5 的行没有被阻止。
【解决方案3】:

线程很老了,只是为了分享我关于@Frans 执行的上述测试的两分钱

连接 1

BEGIN;
SELECT * FROM notification WHERE `date` >= '2011-05-03' FOR UPDATE;

连接 2

BEGIN;

SELECT * FROM notification WHERE `date` = '2011-05-02' FOR UPDATE;

并发事务 2 肯定会被阻塞,但原因是 NOT 事务 1 持有整个表的锁。下面解释了幕后发生的事情:

首先,InnoDB存储引擎的默认隔离级别是Repeatable Read。在这种情况下,

1-当where条件中使用的列没有被索引时(如上):

引擎有义务执行全表扫描以过滤掉不符合条件的记录。已扫描的每一行首先被锁定。 MySQL 稍后可能会释放那些与 where 子句不匹配的记录上的锁。这是对性能的优化,但是这种行为违反了 2PL 约束。

如前所述,当事务 2 启动时,它需要为检索到的每一行获取 X 锁,尽管只有一条记录 (id = 2) 与 where 子句匹配。最终事务 2 将等待第一行的 X 锁(id = 1),直到事务 1 提交或回滚。

2-当where条件中使用的列是主索引时

只有满足条件的索引条目被锁定。这就是为什么在 cmets 中有人说某些测试没有被阻止。

3 - where条件中使用的列是索引但不唯一时

这个案例比较复杂。 1) 索引条目被锁定。 2) 一个 X 锁附加到相应的主索引。 3) 在匹配搜索条件的记录前后,对不存在的条目附加两个间隙锁。

【讨论】:

    【解决方案4】:

    您发布的文档页面中的链接提供了有关locking 的更多信息。在这个页面中

    A SELECT ... FOR UPDATE 读取最新的可用数据,在它读取的每一行上设置排他锁。因此,它设置的锁与搜索的 SQL UPDATE 在行上设置的锁相同。

    这似乎很清楚,它必须扫描所有行。

    【讨论】:

    • 不确定我是否理解正确,但你是说,如果我选择WHERE 中的最后一行和列没有索引,它会锁定整个表吗?这显然是错误的。至少 Oracle 只锁定 selected 行
    • Oracle 比 MySQL 好得多 :) 我不知道通过实验,只通过阅读文档,但 它似乎在说什么。不过听起来确实很愚蠢。
    • 这听起来非常愚蠢。鉴于 MySQL 的流行,我真的怀疑它的工作方式。而声明setting exclusive locks on each row it reads 可以用其他方式解释。虽然我同意这不是 100% 清楚
    • 我不知道; MySQL 做了一些非常愚蠢的事情:-/ 我通常只是开始,做我需要做的任何事情并提交,但是当我这样做时不知道它在内部做什么。
    • @ Alexander Malakhov - 这似乎是它的工作方式,我刚刚对其进行了测试,发现它会锁定整个表,除非你在 where 子句中索引列。这里的文档确实应该改进,因为它非常令人困惑。 “它读取的每一行”会让我认为它“读取”的行将基于我的 WHERE 子句中的条件。因此,如果我的 WHERE 子句将结果限制为 1 个返回行,那就是我希望被锁定的行。但似乎“它读取的每一行”意味着每一行“被数据库扫描”。
    【解决方案5】:

    来自mysql官方文档:

    锁定读取、UPDATE 或 DELETE 通常会在 SQL 语句处理过程中扫描的每条索引记录上设置记录锁。语句中是否存在将排除该行的 WHERE 条件并不重要。

    对于 Frans 回答中讨论的情况,所有行都被锁定,因为在 sql 处理期间存在表扫描:

    如果您没有适合您的语句的索引,并且 MySQL 必须扫描整个表来处理该语句,则表的每一行都会被锁定,这反过来又会阻止其他用户对表的所有插入。创建良好的索引很重要,这样您的查询就不会不必要地扫描许多行。

    在此处查看最新文档:https://dev.mysql.com/doc/refman/8.0/en/innodb-locks-set.html

    【讨论】:

    • 有没有人解释一下为什么会有这样的认识?
    【解决方案6】:

    正如其他人所提到的,SELECT... FOR UPDATE 会锁定在默认隔离级别中遇到的所有行。尝试将运行此查询的会话的隔离设置为 READ COMMITTED,例如在查询之前加上:set session transaction isolation level read committed;

    【讨论】:

      【解决方案7】:

      它锁定查询选择的所有行。

      【讨论】:

      • 锁定所有行已扫描SELECTed 行的超集。
      猜你喜欢
      • 1970-01-01
      • 2010-12-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-03-10
      • 1970-01-01
      相关资源
      最近更新 更多