SELECT将锁定多少行... ORDER BY xxx LIMIT 1 FOR UPDATE?

Vla*_*sny 32 mysql transactions rowlocking

我有一个具有以下结构的查询:

SELECT ..... WHERE status = 'QUEUED' ORDER BY position ASC LIMIT 1 FOR UPDATE;
Run Code Online (Sandbox Code Playgroud)

它是InnoDB表上的单表SELECT语句.字段position(INT NOT NULL)上有索引.状态为ENUM,也已编入索引.

SELECT ... FOR UPDATE手册页说,它锁定了它读取的所有行.我是否理解正确,在这种情况下只会锁定一行?或者说它会锁定整个桌子?

是否可以确定哪些行将被EXPLAIN查询锁定?如果是 - 如何?解释空表上的查询显示以下内容:

1;'SIMPLE';'job';'index';<null>;'index_position';[34,...];<null>;1;'Using where'
Run Code Online (Sandbox Code Playgroud)

Mor*_*ker 27

这是一个很好的问题.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;
Run Code Online (Sandbox Code Playgroud)

因为语句只在提交后写入二进制日志,所以在从属会话#2将首先应用,并且会产生不同的结果,从而导致数据损坏.

所以InnoDB所做的就是设置额外的锁.如果is_deleted被索引,那么在session1提交之前,没有其他人能够修改或插入记录范围is_deleted=1.如果没有索引is_deleted,则InnoDB需要锁定整个表中的每一行,以确保重放的顺序相同.您可以将此视为锁定间隙,这是从行级锁定直接掌握的不同概念.

在您的情况下ORDER BY position ASC,InnoDB需要确保在最低键值和"特殊"最低可能值之间不能修改新行.如果你做了ORDER BY position DESC......好吧,那么没有人可以插入这个范围.

所以这里有解决方案:

  • 基于语句的二进制日志记录糟糕 我真的很期待未来我们都会切换到基于行的二进制日志记录(可从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
Run Code Online (Sandbox Code Playgroud)

  • 如果您不使用复制或二进制日志进行时间点恢复,则设置innodb_locks_unsafe_for_binlog(虽然时间点恢复非常好 - 但我不知道您为什么不这样做).这取决于ORDER BY有多少表被锁定(从内部角度来看,它永远不是表锁,但如果你没有索引,可以在每一行上设置单独的锁). (2认同)

Vla*_*sny 6

我已经做了测试。创建了下表:

id  data1   data2
1   1   2
2   2   1
5   2   2
6   3   3
3   3   4
4   4   3
Run Code Online (Sandbox Code Playgroud)

然后我创建了第一个与事务的连接:

SELECT id FROM test ORDER BY data1 LIMIT 1 FOR UPDATE;
Run Code Online (Sandbox Code Playgroud)

结果是 id=1 的行;

然后我从另一个连接创建了第二个事务,而没有先提交:

SELECT id FROM test WHERE data1=2 FOR UPDATE;
Run Code Online (Sandbox Code Playgroud)

它没有阻止。仅当我尝试选择第一个事务选择的行时,它才会被阻止。我尝试了以下将 ORDER BY 更改为 DESC 1,它也有效。

结论:当使用 ORDER BY 和 LIMIT 子句时,MySQL 仅阻止它实际选择的行。有关间隙锁定的解释,请参阅@Morgan 答案。

我的MySQL版本是5.0.45