从锁定行取值插入时的事务阻塞问题原因及解决方法咨询
嘿,这个现象其实是InnoDB锁机制和事务读取模式共同作用的结果,我给你拆解一下:
首先看第一个事务里的update product set price = 70;——这个语句没有指定WHERE条件,相当于要更新整个product表的所有行。InnoDB引擎会给这个表加上意向排他锁(IX锁),同时给表中每一行都加上行级排他锁(X锁)。虽然你之后执行了rollback,但在sleep的20秒里,这些锁都是一直持有的。
然后重点来了:第二个事务里的insert into product_order select ... from product,这个语句里的select并不是普通的快照读(也就是我们平时说的不加锁读,用undo log返回历史版本),而是当前读——InnoDB对DML语句中的查询部分,都会强制使用当前读,目的是获取最新的数据,并且会尝试给读取的行加上共享锁(S锁)。
而排他锁(X)和共享锁(S)是互斥的:第一个事务已经把所有行都加了X锁,第二个事务要加S锁就必须等X锁释放,所以它只能一直等到第一个事务sleep结束、rollback释放锁之后才能执行。
你觉得“没更新第二个事务涉及的行”,但实际上第一个事务是全表更新,所有行都被锁了,第二个事务要读所有行,自然就产生锁冲突了。
根据你的需求,有几个可行的方案:
精准更新,避免全表锁:如果你的业务逻辑里第一个事务不需要更新全表,一定要给
update加上WHERE条件,并且确保WHERE条件能用到索引——这样InnoDB只会给匹配的行加锁,不会影响其他行的读取。比如:update product set price =70 where id=1;拆分第二个事务的读写操作:把
insert...select拆成两步,先做普通的快照读获取数据,再执行insert。普通select在默认的可重复读(RR)隔离级别下是快照读,不会请求锁,也就不会被阻塞:-- 第二个事务改成这样 START TRANSACTION; -- 第一步:快照读,获取product的数据(不会等锁) CREATE TEMPORARY TABLE tmp_product AS SELECT id, amount, price FROM product; -- 第二步:从临时表插入数据 INSERT INTO product_order(product_id, amount, price) SELECT id, amount, price FROM tmp_product; DROP TEMPORARY TABLE tmp_product; COMMIT;调整事务隔离级别(谨慎使用):把第二个事务的隔离级别改成读已提交(RC),不过这个办法要注意业务影响。RC隔离级别下,虽然
insert...select还是当前读,但InnoDB的锁范围会更小,而且不会有间隙锁的问题,但它可能导致不可重复读的问题,需要结合你的业务场景评估。设置方式:-- 在第二个事务开始前设置 SET TRANSACTION ISOLATION LEVEL READ COMMITTED; START TRANSACTION; insert into product_order(product_id, amount, price) select id, amount, price from product; commit;
内容的提问来源于stack exchange,提问作者Artsiom Kotau

