MySQL 8.0.x 中令人困惑的 SELECT FOR UPDATE 行为

Posted

技术标签:

【中文标题】MySQL 8.0.x 中令人困惑的 SELECT FOR UPDATE 行为【英文标题】:Confusing SELECT FOR UPDATE behavior in MySQL 8.0.x 【发布时间】:2021-06-17 20:22:37 【问题描述】:

mysql 8.0.x

我们坚持混淆 SELECT ... LIMIT 1 FOR UPDATE SKIP LOCKED 的不同行为取决于主索引字段类型。

让我们考虑 2 个类似表的情况。

案例一:

CREATE TABLE `test` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci

案例2:

CREATE TABLE `test` (
  `id` binary(11) NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci

它们的区别仅在于字段类型 - INT 和 BINARY。

插入 6 个 id 为 1 到 6 的项目。

运行 2 个并发事务。

Transaction 1:

BEGIN;

SELECT id FROM test
LIMIT 1 
FOR UPDATE skip locked;

SELECT SLEEP(10);  #for test

COMMIT;

事务 2:

BEGIN;

SELECT id
FROM test
WHERE id = 4
FOR UPDATE;

COMMIT;

如果 id 是一个 INT 字段 select ... for update from transaction 2 执行时无需等待第一个事务提交。

如果 id 是二进制文件 select ... for update from transaction 2 在第二个事务提交后执行

第二种情况的 SHOW ENGINE INNODB STATUS 输出的一部分:

---TRANSACTION 38229, ACTIVE 3 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 12, OS thread handle 6068, query id 1532 localhost 127.0.0.1 root Sending data
SELECT id
FROM test
where id = 4
FOR UPDATE
------- TRX HAS BEEN WAITING 3 SEC FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 19 page no 4 n bits 80 index PRIMARY of table `test`.`test` trx id 38229 lock_mode X waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 11; hex 3100000000000000000000; asc 1          ;;
 1: len 6; hex 000000009530; asc      0;;
 2: len 7; hex 80000000000000; asc        ;;

------------------
---TRANSACTION 38228, ACTIVE 5 sec
2 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 8, OS thread handle 12052, query id 1530 localhost 127.0.0.1 root User sleep
SELECT SLEEP(10)
-----------------------------

这是一个很大的惊喜......

谁能解释为什么 mysql (innodb) 在这些情况下表现如此不同。

【问题讨论】:

【参考方案1】:

您正在体验的是(需要做的)自动投射的效果,请参阅Type Conversion in Expression Evaluation。

对于二进制列,您要求 MySQL 将整数 4 与表中的二进制(字符串)值进行比较,当您这样做时,MySQL 会尽其所能并尝试在您的列作为数字。

MySQL 将评估为等于整数 4 的值的示例是例如字符串'4''04''000000004'' 004' 甚至'04abc'

MySQL 不能对其使用索引查找,但必须检查(并锁定)所有行,因为它们可能是一个可以计算为整数 4 的字符串。其中一行是你在第一个事务中锁定的那个,因此它必须等到锁被释放。

另一方面,如果你的列是整数,MySQL可以使用索引直接跳转到id = 4的行,除非它是偶然锁定在你的第一个行交易,可以进行第二笔交易。

因此,为了使您的二进制大小写与整数大小写相同,请将二进制列与二进制值进行比较,例如使用WHERE id = '4'。那么就不必进行强制转换了。

【讨论】:

非常感谢!实际情况是将 uuid 存储为二进制并使用函数 BIN_TO_UUID 与字段进行比较,而不是使用 UUID_TO_BIN 函数与静态值。 SELECT BIN_TO_UUID(t.id) from tbl t where BIN_TO_UUID(t.id) = '04b1792c-dda5-484b-9ebb-9f83e314a39b' FOR UPDATE 我们已经修复了查询 where t.id = UUID_TO_BIN('04b1792c-dda5-484b-9ebb-9f83e314a39b') FOR UPDATE 虽然问题为什么 mysql 锁定所有行仍然不清楚 MySQL 锁定它查看的行(因为它们的状态与查询决策相关)。如果 MySQL 可以通过索引跳转到它可以是的一行,它只需要查看(并锁定)一行。如果它必须查看所有行,如果它们可能评估为“4”(或者,在您的情况下,将其放入函数bin_to_uuid 之后的评估值),它必须锁定所有行。一般来说:如果你对列值应用一个函数(你可以把autocast看作一个函数),MySQL就不能使用索引查找来查找行。

以上是关于MySQL 8.0.x 中令人困惑的 SELECT FOR UPDATE 行为的主要内容,如果未能解决你的问题,请参考以下文章

MySQL-8.0.x DDL 原子性

Fortran程序中令人困惑的调试错误

@SessionAttribute 令人困惑

类定义中的变量范围令人困惑

Go 语言中的引用类型令人困惑

PowerShell 中 $args 的令人困惑的评估