最近遇到一个奇怪的mysql死锁:
事务1:
begin;
select * from bridge_tx_status where id = 2 for update; 1⃣️
update bridge_tx_status set biz_nonce="1234" where transaction_id="23"; 2⃣️
commit;
事务2:
begin;
update bridge_tx_status set transaction_id="23" where id =2; 1⃣️
commit;
事务级别为可重复读,其中id=2的列,transaction_id这一列为"23",且transaction_id上没有任何索引。
复现:首先执行事务1:的第一条sql,然后执行事务2的第一条sql但是不提交,然后执行事务1的第二条sql,此时会抛出死锁。
排查过程:
首先发生死锁我们可以得出结论,事务2的“update bridge_tx_status set transaction_id="23" where id =2”语句有不止一次加锁过程。
然后把事务1中第二条语句改成与事务2中update一致:
事务1:
begin;
select * from bridge_tx_status where id = 2 for update; 1⃣️
update bridge_tx_status set transaction_id="23" where id =2; 2⃣️
commit;
重新执行复现过程,发现死锁消失。
这时我们可以得出结论,“update bridge_tx_status set transaction_id="23" where id =2”语句的多次加锁中,存在一把锁,该锁与自身是兼容的,即该锁可以在不同事务中多次进入。
进一步查阅了mysql相关锁的资料后发现了一个叫意向锁的锁恰好满足这个条件,相关资料:https://juejin.im/post/5b85124f5188253010326360
简单概括一下就是mysql在加行锁之前要先对全表加意向锁,同一张表对意向锁与意向锁是兼容的,但是意向锁与表锁是冲突的,该锁主要是为了优化行锁与表锁的冲突检测过程。
由于transaction_id没有任何索引,所以该语句需要加表锁, 此时基本上整个过程已经比较清晰了:
事务一(select * from bridge_tx_status where id = 2 for update):
1.对整张表加意向ix锁。
2.对id=2的行加 record行锁。
事务二(update bridge_tx_status set transaction_id="23" where id =2):
1.对整张表加意向ix锁。
2.对id=2的行加 record行锁。(被事务1阻塞)
事务一(update bridge_tx_status set biz_nonce="1234" where transaction_id="23"):
1.对整张表加表锁。(被事务2阻塞)
由于可以看错已经成环,所以形成死锁。