如何强制MySQL获取事务的表锁?

时间:2018-11-14 21:05:19

标签: mysql locking rowlocking

我正在尝试使用InnoDB存储引擎在MySQL数据库表上执行操作。此操作是INSERT-or-UPDATE类型的操作,其中有一组传入的数据,并且表中可能已经有一些数据需要更新。例如,我可能有此表:

test_table
+-------+--------------+------+-----+---------+----------------+
| Field | Type         | Null | Key | Default | Extra          |
+-------+--------------+------+-----+---------+----------------+
| id    | int(11)      | NO   | PRI | NULL    | auto_increment |
| value | varchar(255) | NO   |     | NULL    |                |
+-------+--------------+------+-----+---------+----------------+

...以及一些示例数据:

+----+-------+
| id | value |
+----+-------+
|  1 | foo   |
|  2 | bar   |
|  3 | baz   |
+----+-------+

现在,我想“合并”以下值:

2, qux
4, corge

我的代码最终会发出以下查询:

BEGIN;
SELECT id, value FROM test WHERE id=2 FOR UPDATE;
UPDATE test SET id=2, value='qux' WHERE id=2;
INSERT INTO test (id, value) VALUES (4, 'corge');
COMMIT;

(我不确定SELECT ... FOR UPDATEUPDATE会发生什么,因为我使用的是Java的MySQL Connector / J库,而只是在Java上调用updateRow方法ResultSet。为了争辩,我们假设上面的查询实际上是发布给服务器的内容。

注意:上表是一个简单的示例,用于说明我的问题。实际表更加复杂,执行SELECT ... FOR UPDATE时,我没有使用PK作为要匹配的字段。因此,仅通过查看传入的数据是否需要对记录进行INSERT或UPDATE并不明显。必须咨询数据库以确定是否使用INSERT / UPDATE。

以上查询在大多数情况下都可以正常工作。但是,当有更多记录要“合并”时,可以交错SELECT ... FOR UPDATEINSERT行,在这里我无法预测将发布SELECT ... FOR UPDATE还是INSERT并以什么顺序。

结果是有时事务会死锁,因为一个线程已为UPDATE操作锁定了表的一部分,并正在等待表锁定(对于INSERT ,这需要对主键索引进行锁定),而另一个线程已经获得了主键的表锁定(可能是因为它发出了INSERT查询),并且现在正在等待以获得第一个线程持有的行锁(或更可能是页面级锁)。

这是代码中唯一可更新此表的位置,并且当前未获取任何显式锁。 UPDATEINSERT的排序似乎是问题的根源。

我可以想到几种解决方法。

  1. 检测死锁(MySQL引发错误),然后重试即可。这是我当前的实现方式,因为该问题很少出现。每天发生几次。
  2. 在合并过程之前使用LOCK TABLES,然后在UNLOCK TABLES之后使用表锁。这显然不能与MariaDB Galera一起使用-将来我们可能会将此产品用于此产品。
  3. 将代码更改为始终始终首先发出INSERT个查询。这将导致首先获取任何表级锁,并避免死锁。

#3的问题在于,在一种已经相当复杂的方法中,它将需要更复杂的代码(“合并”操作本来就是复杂的)。更为复杂的代码还意味着大约要增加两倍的查询量(SELECT用于确定行ID是否已存在,然后再使用另一个SELECT ... FOR UPDATE / UPDATE来进行实际更新)。该表存在合理的争用量,因此,我希望尽可能避免发出更多查询。

是否有一种方法可以强制MySQL在不使用LOCK TABLES的情况下获得表级锁定?也就是说,如果我们搬到Galera,这种方式会起作用吗?

1 个答案:

答案 0 :(得分:0)

我认为您可以通过获取一组行锁和间隙锁来做您想做的事情:

START TRANSACTION;
SELECT id, value
FROM test
WHERE id in (2, 4) -- list all the IDs you need to UPSERT
FOR UPDATE;
UPDATE test SET value = 'qux' WHERE id = 2;
INSERT INTO test (id, value) VALUES (4, 'corge');
COMMIT;

SELECT查询将锁定已存在的行,并为尚不存在的行创建间隙锁。间隙锁将阻止其他事务创建这些行。