我正在使用 Docker 映像中的 MySQL 版本
8.3.0
,采用默认配置。
我将用例简化为只有 2 列的表格。我想要实现的是阻止同时事务为同一“组”插入记录(在本例中为相同的
product_sku
),但允许它们为其他“组”插入。
通过浏览文档https://dev.mysql.com/doc/refman/8.0/en/innodb-locking-reads.html我明白,通过运行
SELECT x FROM y WHERE z FOR UPDATE
我只会锁定满足条件的行 z
,无需锁定所有其他。然而事实似乎并非如此。
我的桌子:
CREATE TABLE `example` (
`id` int NOT NULL AUTO_INCREMENT,
`product_sku` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
PRIMARY KEY (`id`),
KEY `example_product_sku_IDX` (`product_sku`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
我运行两个 MySQL 会话 -
AA
和 BB
:
AA> BEGIN;
Query OK, 0 rows affected (0,00 sec)
AA> SHOW VARIABLES WHERE Variable_name='autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | OFF |
+---------------+-------+
1 row in set (0,00 sec)
BB> BEGIN;
Query OK, 0 rows affected (0,00 sec)
BB> SHOW VARIABLES WHERE Variable_name='autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | OFF |
+---------------+-------+
1 row in set (0,00 sec)
AA> SELECT COUNT(*) FROM example WHERE product_sku = 'abc' FOR UPDATE;
+----------+
| COUNT(*) |
+----------+
| 0 |
+----------+
1 row in set (0,00 sec)
BB> SELECT COUNT(*) FROM example WHERE product_sku = 'def' FOR UPDATE;
+----------+
| COUNT(*) |
+----------+
| 0 |
+----------+
1 row in set (0,00 sec)
AA> INSERT INTO example (product_sku) VALUES ('abc');
(this query hangs up...)
BB> INSERT INTO example (product_sku) VALUES ('def');
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
(at this point query from AA executes):
Query OK, 1 row affected (4,02 sec)
根据我的理解,两个锁应该是独立的,因为它们应该锁定不同范围的行。但事实并非如此。
我尝试了以下更改,但没有成功:
example_product_sku_IDX
索引SELECT COUNT(*)
更改为 SELECT *
我试图了解是否我做错了什么或者 MySQL 有错误。我在 MySQL 板上发现了类似的问题,但它对于解释问题没有太大帮助 - https://bugs.mysql.com/bug.php?id=96748
经过一番研究,我想我可以回答我自己的问题。
有两件事与上述问题相关。
首先 - 需要设置适当的事务隔离级别,如下所述:https://dev.mysql.com/doc/refman/8.3/en/set-transaction.html
在这种情况下,查询应如下:
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
SELECT COUNT(*) FROM example WHERE product_sku = 'abc' FOR UPDATE;
INSERT INTO example (product_sku) VALUES ('abc');
COMMIT;
第二 - 在上面的示例中,如果
SELECT x FROM y FOR UPDATE
返回空结果,MySQL 将不会创建任何锁。必须至少有一条满足查询条件的记录。因此,在空表的情况下,当多个并发客户端尝试将一行插入同一个“组”时,如果使用 FOR UPDATE
类型的锁定机制,MySQL 不会阻止它们。