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 行为的主要内容,如果未能解决你的问题,请参考以下文章