【问题标题】:How many rows will be locked by SELECT ... ORDER BY xxx LIMIT 1 FOR UPDATE?SELECT ... ORDER BY xxx LIMIT 1 FOR UPDATE 将锁定多少行?
【发布时间】:2011-04-17 16:14:17
【问题描述】:

我有一个结构如下的查询:

SELECT ..... WHERE status = 'QUEUED' ORDER BY position ASC LIMIT 1 FOR UPDATE;

这是对 InnoDB 表的单表 SELECT 语句。字段 position (INT NOT NULL) 上有一个索引。 status 是 ENUM 并且也被索引。

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

是否可以通过EXPLAIN 查询确定哪些行将被锁定?如果是 - 如何?对空表的查询说明如下:

1;'SIMPLE';'job';'index';<null>;'index_position';[34,...];<null>;1;'Using where'

【问题讨论】:

  • 正确答案请看下面我的回答。
  • 您是否在“data1”上添加了索引?

标签: mysql transactions rowlocking


【解决方案1】:

这是一个很好的问题。 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 获得,但默认情况下不启用)。

  • 使用基于行的复制,如果将隔离级别更改为已提交读,则只需锁定匹配的一行。

  • 如果你想成为一个受虐狂,你也可以打开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

【讨论】:

  • 如果我根本不使用任何复制怎么办?我是否让你正确地使用了 ORDER BY,整个索引范围被锁定,因此整个表也被锁定?
  • 如果你不使用复制或二进制日志进行时间点恢复,那么设置 innodb_locks_unsafe_for_binlog (时间点恢复非常好 - 我不知道你为什么会't)。锁定多少表取决于 ORDER BY(从内部角度来看,它永远不是表锁,但如果您没有索引,则可以在每一行上设置单独的锁)。
  • 我应该注意,您需要新的 InnoDB 锁 - dev.mysql.com/doc/refman/5.5/en/innodb-locks-table.html information_schema 表来调试正在发生的锁定。
  • @Morgan Tocker,好吧,看来你错了。请看我的回答。
  • 或者可能是特定版本?
【解决方案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 的方法,它也可以。

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

我的 MySQL 版本是 5.0.45

【讨论】:

  • 您是否将 SELECT 语句嵌套在事务中? - 我重复了你的测试用例,我可以产生我正在解释的结果。
【解决方案3】:

在某些版本的 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 上出现同样的错误。

【讨论】:

  • 嗯,看来这不是错误,而是功能请求。另外,这不是我的情况。但感谢您提供信息!
【解决方案4】:

与其他数据库不同,在 MySQL 中,查询将锁定索引位置。这实际上意味着所有当前具有status 等于'QUEUED' 或希望它从另一个事务更改为'QUEUED' 的行都被锁定。我发现的唯一解决方案是选择没有FOR UPDATE 的行,然后使用基于ID 的过滤器选择它们,并在它们被锁定后重新检查条件。不太好,但它可以完成工作。

【讨论】:

  • 查看我的评论。解决方法是可能的(基于行的复制)。
  • 不要打扰SELECT。相反,只需执行 UPDATE table SET filter_column=my_process_id WHERE status='QUEUED' LIMIT X,然后 SELECT 在事务中将 filter_column 设置为 my_process_id 的所有内容。这样,您可以在 table/index/whatever 的任何部分获得尽可能短的锁定时间。
猜你喜欢
  • 1970-01-01
  • 2019-01-29
  • 2018-02-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-12-04
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多