我有一个具有以下结构的查询:
SELECT ..... WHERE status = 'QUEUED' ORDER BY position ASC LIMIT 1 FOR UPDATE;
这是InnoDB表上的单表SELECT语句。字段position
(INT NOT NULL)上有索引。状态为ENUM,也是索引。
SELECT ... FOR UPDATE
手册页说,它会锁定它读取的所有行。我是否理解正确,在这种情况下只会锁定一行?或者更确切地说它会锁定整个桌子?
是否可以确定哪些行将被EXPLAIN
查询锁定?如果是 - 如何?解释空表上的查询显示以下内容:
1;'SIMPLE';'job';'index';<null>;'index_position';[34,...];<null>;1;'Using where'
答案 0 :(得分:26)
这是一个很好的问题。 InnoDB是一个行级锁定引擎,但它必须设置额外的锁以确保二进制日志的安全性(用于复制;时间点恢复)。要开始解释它,请考虑以下(天真)示例:
session1> START TRANSACTION;
session1> DELETE FROM users WHERE is_deleted = 1; # 1 row matches (user_id 10), deleted.
session2> START TRANSACTION;
session2> UPDATE users SET is_deleted = 1 WHERE user_id = 5; # 1 row matches.
session2> COMMIT;
session1> COMMIT;
因为语句只在提交后写入二进制日志,所以在从属会话#2首先应用,并产生不同的结果,导致数据损坏。
所以InnoDB所做的是设置额外的锁。如果is_deleted
被编入索引,那么在session1提交之前,其他任何人都无法修改或插入is_deleted=1
的记录范围。如果is_deleted
上没有索引,那么InnoDB需要锁定整个表中的每一行,以确保重放的顺序相同。您可以将此视为锁定间隙,这是直接从行级锁定中掌握的不同概念。
在使用ORDER BY position ASC
的情况下,InnoDB需要确保在最低键值和“特殊”最低可能值之间不能修改新行。如果你做了ORDER BY position DESC
之类的事情,那么没有人可以插入这个范围。
解决方案就是这样:
基于语句的二进制日志记录很糟糕。我真的很期待未来我们都切换到row based binary logging(可从MySQL 5.1获得,但默认情况下不可用)。
使用基于行的复制,如果将隔离级别更改为read-committed,则只需要锁定匹配的一行。
如果您想成为受虐狂,您还可以使用基于语句的复制启用innodb_locks_unsafe_for_binlog。
4月22日更新:要复制+粘贴我的测试用例的改进版本(它没有在间隙中搜索):
session1> CREATE TABLE test (id int not null primary key auto_increment, data1 int, data2 int, INDEX(data1)) engine=innodb;
Query OK, 0 rows affected (0.00 sec)
session1> INSERT INTO test VALUES (NULL, 1, 2), (NULL, 2, 1), (5, 2, 2), (6, 3, 3), (3, 3, 4), (4, 4, 3);
Query OK, 6 rows affected (0.00 sec)
Records: 6 Duplicates: 0 Warnings: 0
session1> start transaction;
Query OK, 0 rows affected (0.00 sec)
session1> SELECT id FROM test ORDER BY data1 LIMIT 1 FOR UPDATE;
+----+
| id |
+----+
| 1 |
+----+
1 row in set (0.00 sec)
session2> INSERT INTO test values (NULL, 0, 99); # blocks - 0 is in the gap between the lowest value found (1) and the "special" lowest value.
# At the same time, from information_schema:
localhost information_schema> select * from innodb_locks\G
*************************** 1. row ***************************
lock_id: 151A1C:1735:4:2
lock_trx_id: 151A1C
lock_mode: X,GAP
lock_type: RECORD
lock_table: `so5694658`.`test`
lock_index: `data1`
lock_space: 1735
lock_page: 4
lock_rec: 2
lock_data: 1, 1
*************************** 2. row ***************************
lock_id: 151A1A:1735:4:2
lock_trx_id: 151A1A
lock_mode: X
lock_type: RECORD
lock_table: `so5694658`.`test`
lock_index: `data1`
lock_space: 1735
lock_page: 4
lock_rec: 2
lock_data: 1, 1
2 rows in set (0.00 sec)
# Another example:
select * from test where id < 1 for update; # blocks
答案 1 :(得分:2)
我做过测试。创建了下表:
id data1 data2
1 1 2
2 2 1
5 2 2
6 3 3
3 3 4
4 4 3
然后我创建了第一个与事务的连接:
SELECT id FROM test ORDER BY data1 LIMIT 1 FOR UPDATE;
结果是id为1的行;
然后我从另一个连接创建了第二个事务而没有先提交:
SELECT id FROM test WHERE data1=2 FOR UPDATE;
它没有阻止。它只在我尝试选择第一个事务选择的行时阻止。我尝试了以下将ORDER BY更改为DESC,它也可以。
结论:MySQL在使用ORDER BY和LIMIT子句时仅阻止它实际选择的行。有关间隙锁定说明,请参阅@Morgan答案。
我的MySQL版本是5.0.45
答案 2 :(得分:2)
某些版本的MySQL存在错误:#67745 Too much row locks when using SELECT for UPDATE, LIMIT and ORDER BY。
版本:5.5.28,5.5.30,5.7.1
我的本地mysql 5.5.25 win64上的相同错误。
答案 3 :(得分:1)
与其他数据库不同,在MySQL中,查询将锁定索引位置。这实际上意味着当前status
等于'QUEUED'
或希望它从另一个事务更改为'QUEUED'
的所有行都将被锁定。我发现的唯一解决方案是选择没有FOR UPDATE
的行,然后使用基于ID的过滤器选择它们,并在锁定后重新检查条件。不好,但它确实有效。