使用MySQL的FOR UPDATE锁定时,究竟锁定了什么?

Mar*_*kus 66 mysql sql

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

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

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

这里只有一个由MySQL锁定的记录或它必须扫描的所有记录才能找到单个记录吗?

Fra*_*ans 98

我们为什么不试试呢?

设置数据库

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');
Run Code Online (Sandbox Code Playgroud)

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

连接1

BEGIN;
SELECT * FROM notification WHERE `date` >= '2011-05-03' FOR UPDATE;
Run Code Online (Sandbox Code Playgroud)

连接2

BEGIN;
Run Code Online (Sandbox Code Playgroud)

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

SELECT * FROM notification WHERE `date` = '2011-05-02' FOR UPDATE;
Run Code Online (Sandbox Code Playgroud)

确实它确实阻止了.

有趣的是,我们也无法添加可读的记录,即

INSERT INTO notification(id, `date`, `text`) values (6, '2011-05-06', 'Notification 6');
Run Code Online (Sandbox Code Playgroud)

块也是!

我不能确定在这一点上MySQL是否会继续并在锁定一定百分比的行时锁定整个表,或者确保SELECT ... FOR UPDATE查询结果永远不会被另一个事务更改时实际上非常智能(用INSERT,UPDATEDELETE),而锁被保持.

  • +1.如果对唯一列进行锁定,则不会阻止整个表.我试过`CREATE TABLE notification(`id` BIGINT(20)NOT NULL AUTO_INCREMENT,`date` DATE,`text` TEXT,PRIMARY KEY(`id`))ENGINE = InnoDB;`有效.但如果未指定unique/primary key,则无效.`SELECT*FROM notification WHERE`id` ='1'FOR UPDATE;`on原始模式 (34认同)
  • +1举例来证明.这种行为看起来很可怕.你试过只锁一行吗?你为什么不选择`id`列,我不相信日期文字:).什么是MySQL/InnoDB的版本? (6认同)
  • @fabspro您不需要使用唯一键.任何密钥都可以使用,无论它是否唯一. (5认同)
  • 我自己的测试显示使用`for update`,其中非索引列上的过滤器导致整表锁定,而索引列上的过滤器导致过滤行锁定的所需行为.所以主键不是必需的,任何关键都足够了. (3认同)
  • 这个例子对我很有启发。尝试通过 ID 选择要更新的单个记录会导致阻塞,直到我将 ID 字段设为主键。然后,一切正常。重要的一课是仅在使用唯一键时才使用 FOR UPDATE,否则您的整个表也可能被锁定,因为它会扫描整个表/索引以查找所有匹配项,从而锁定所有内容。 (2认同)
  • 但是在进一步测试时,使用索引,如果在导致空集的where过滤器上运行`for update`,则不会阻止对同一空集的另一个查询.而没有索引,运行`for update`将阻止查询相同的空集. (2认同)

小智 31

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

连接 1

BEGIN;
SELECT * FROM notification WHERE `date` >= '2011-05-03' FOR UPDATE;
Run Code Online (Sandbox Code Playgroud)

连接 2

BEGIN;

SELECT * FROM notification WHERE `date` = '2011-05-02' FOR UPDATE;
Run Code Online (Sandbox Code Playgroud)

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

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

1- 当 where 条件中使用的列未编入索引时(如上例):

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

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

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

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

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

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


con*_*cat 20

我知道这个问题很老了,但是我想分享一些相关测试的结果,我已经用索引列完成了这些测试产生了一些非常奇怪的结果.

表结构:

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

插入12行INSERT INTO t1 (notid) VALUES (1), (2),..., (12).在连接1上:

BEGIN;    
SELECT * FROM t1 WHERE id=5 FOR UPDATE;
Run Code Online (Sandbox Code Playgroud)

连接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;
Run Code Online (Sandbox Code Playgroud)

奇怪的部分是,SELECT * FROM t1 WHERE id>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;
...
Run Code Online (Sandbox Code Playgroud)

我还想指出,当来自连接1的查询中的条件与非索引行匹配时,似乎整个表都被锁定.例如,当连接1执行时,阻止所有选择查询和来自连接2的查询.WHERESELECT * FROM t1 WHERE notid=5 FOR UPDATEFOR UPDATEUPDATE

- 编辑 -

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

连接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)
Run Code Online (Sandbox Code Playgroud)

连接2:

SELECT * FROM t1 WHERE id=2 FOR UPDATE; 被封锁;

SELECT * FROM t1 WHERE id=4 FOR UPDATE;不是堵塞.


El *_*obo 10

您发布的文档页面中的以下链接提供了有关锁定的更多信息.在这个页面中

SELECT ... FOR UPDATE读取最新的可用数据,在其读取的每一行上设置独占锁.因此,它设置搜索的SQL UPDATE将在行上设置的相同锁.

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

  • @Alex Malakhov - 这似乎是它的工作方式,我只是测试它并发现它锁定整个表,除非你索引where子句中的列.这里的文档应该真正得到改进,因为它非常混乱."它读取的每一行"会让我认为它"读取"的行将基于我的WHERE子句中的条件.因此,如果我的WHERE子句将结果约束为1返回的行,那就是我希望锁定的行.但似乎"它读取的每一行"意味着每行"由数据库扫描". (3认同)

小智 6

从mysql官方文档:

锁定读取,UPDATE或DELETE通常会对在处理SQL语句时扫描的每个索引记录设置记录锁定。语句中是否存在排除行的条件并不重要。

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

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

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