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


77

这不是完整/正确的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人员可以对此发表评论
Alexander Malakhov

另外,作为Oracle DB开发人员,我向您保证Oracle仅锁定满足WHERE条件的行。因此,这是技术上是可行的,我不认为MySQL的 多少劣势
亚历山大Malakhov

1
虽然看起来我的答案最终是正确的,但我建议您选择另一个答案,因为它是正确的并且已经过实际测试,而我的只是指的是文档(正如亚历山大指出的那样,可以读入更多内容)单程。
El Yobo

Answers:


109

我们为什么不尝试一下呢?

设置数据库

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),而锁被保持。


6
+1举例说明。这种行为看起来很可怕。您是否尝试仅锁定1行?为什么不按id列选择,我不信任日期文字:)。MySQL / InnoDB的版本是什么?
亚历山大·马拉霍夫

42
+1。如果对唯一列进行了锁定,则不会阻塞整个表。我尝试了CREATE TABLE notification (ID` BIGINT(20)NOT NULL AUTO_INCREMENT,dateDATE,textTEXT,PRIMARY KEY(id))ENGINE = InnoDB;`。但是,如果未指定唯一/主键,即无法使用。SELECT * FROM notification WHERE id` ='1'FOR UPDATE;`在原始模式上
Deepak

6
@fabspro您不需要使用唯一密钥。任何键都可以使用,无论它是否唯一。
thekingoftruth 2014年

3
我自己的测试表明,for update在未索引列上使用where过滤器会导致整个表的锁定,而在索引列上使用where过滤器会导致期望的已过滤行锁定行为。因此,主键不是必需的,任何键都足够。
CMCDragonkai

2
但是,在使用索引进行进一步测试时,如果for update在导致空集的where过滤器上运行,则不会阻塞对同一空集的另一个查询。而没有索引,运行for update将阻止查询同一空集。
CMCDragonkai

26

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

表结构:

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

插入12行INSERT INTO t1 (notid) VALUES (1), (2),..., (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>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;
...

我还要指出的是,当来自连接1的查询中的条件与未索引的行匹配时,似乎整个表都已锁定。例如,当执行连接1时,将阻止连接2的所有选择查询和来自连接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)

连接2

SELECT * FROM t1 WHERE id=2 FOR UPDATE; 被阻止;

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


这是有趣的。我想知道为什么这些障碍也会发生。
deathemperor's

这是因为基于索引的间隙锁定。
罗斯塔米

16

该线程很旧,只想分享我关于@Frans执行的上述测试的2美分

连接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肯定会被阻塞,但是原因不是事务1持有整个表的锁。下面解释了幕后发生的事情:

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

1-当条件未在where索引中使用的列时(如上所述):

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

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

2-当条件为主索引的列使用时

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

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

这种情况更加复杂。1)索引条目已锁定。2)一个X锁附加到相应的主索引。3)在符合搜索条件的记录的前后,两个不间断的锁被附加到不存在的条目上。


11

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

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

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


不知道我是否正确理解了您,但您是在说,如果我选择的最后一行和最后一列WHERE未建立索引,它将锁定整个表吗?这显然是错误的。至少甲骨文只锁定selected行
Alexander Malakhov

Oracle是比MySQL好得多:)我不知道通过实验,只有通过阅读文档,但是这什么,似乎说。听起来确实很愚蠢。
El Yobo

1
这听起来非常愚蠢。鉴于MySQL的流行,我真的怀疑它的工作方式。和声明setting exclusive locks on each row it reads可以用其他方式解释。尽管我同意目前还不清楚
亚历山大玛拉霍夫

我不知道; MySQL做一些非常不可思议的愚蠢的事情:-/我通常只是开始,做我需要做的所有事情,然后执行COMMIT,但是当我这样做时我不知道它在内部做什么。
El Yobo

3
@ Alexander Malakhov-这似乎确实是它的工作方式,我刚刚对其进行了测试,发现它会锁定整个表,除非您在where子句中对列进行索引。这里的文档确实应该改进,因为这非常令人困惑。“它读取的每一行”会让我认为它“读取的”行将基于我的WHERE子句中的条件。因此,如果我的WHERE子句将结果限制为1个返回的行,那是我希望锁定的行。但似乎“它读取的每一行”是指每一行“已由数据库扫描”。
dcp

10

从mysql官方文档:

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

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

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

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


1

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


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.