問題描述
sql如下:
START TRANSACTION;
UPDATE table_a SET ... WHERE id = x ;
IF(ROW_COUNT() = 0) THEN
INSERT INTO table_a id VALUES x;
END IF;
COMMIT;
其中id為主鍵。
平均一天有不到10次的死鎖。
排查過程
首先查看程序日志,發現死鎖都只有新用戶首次登錄時才出現。也就是說,update時發現數據庫中并沒有相應的行,所以會進行接下來的插入操作,這時發生了死鎖。
然后,查詢了innodb的日志,這里貼出關鍵的部分。
------------------------
LATEST DETECTED DEADLOCK
------------------------
2018-02-02 23:35:03 7fe7f03ff700
*** (1) TRANSACTION:
TRANSACTION 72155984, ACTIVE 0 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1184, 2 row lock(s)
MySQL thread id 1279838, OS thread handle 0x7fe803bff700, query id 988205122 172.16.0.123 acspassport update
INSERT INTO table_a id VALUES x
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 85 page no 3763 n bits 184 index `PRIMARY` of table `accountdb`.`last_login_openid` trx id 72155984 lock_mode X insert intention waiting
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;
*** (2) TRANSACTION:
TRANSACTION 72155983, ACTIVE 0 sec inserting
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1184, 2 row lock(s)
MySQL thread id 1248122, OS thread handle 0x7fe7f03ff700, query id 988205121 172.16.0.123 acspassport update
INSERT INTO table_a id VALUES x
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 85 page no 3763 n bits 184 index `PRIMARY` of table `accountdb`.`last_login_openid` trx id 72155983 lock_mode X
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 85 page no 3763 n bits 184 index `PRIMARY` of table `accountdb`.`last_login_openid` trx id 72155983 lock_mode X insert intention waiting
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;
*** WE ROLL BACK TRANSACTION (2)
從兩個transaction的WAITING FOR THIS LOCK TO BE GRANTED
可以看出兩個transaction都在insert申請insert intent X lock 時等待,從而導致死鎖。
總結出精簡后的導致死鎖的過程如下,可以輕松的使用控制臺復現。
transaction A | transaction B |
---|---|
begin | begin |
update row a失敗 | |
update row b失敗 | |
insert row a等待 | |
insert row b等待 | |
死鎖發生 |
成因
innodb不是行鎖嗎,為什么會發生死鎖呢?
這里就涉及到innodb的鎖機制了,innodb使用了Repeatable Read(RR)的隔離級別。在此級別下,innodb為了防止幻讀(Phantom Rows),在實現上使用了gap lock。并且在search和scan的時候使用next-key lock(record lock + gap lock),但是對于在主鍵或唯一索引上進行查找的時候僅使用record lock。這里有一個例外,即當使用主鍵找不到的時候該記錄的時候,則在該區間加gap lock。這次死鎖的就是由這個例外情況引起的。
下面根據以上原理分析本次死鎖的成因。
transaction A | 鎖的情況 | transaction B | 鎖的情況 |
---|---|---|---|
begin | begin | ||
update row a(a>x)失敗 | 區間(x,正無窮)gap lock | ||
update row b(b>x)失敗 | 區間(x,正無窮)gap lock(gap lock之間不互斥) | ||
insert row a等待 | insert intention lock等待(gap lock only stop other transactions from inserting to the gap) | ||
insert row b等待 | insert intention lock 等待 | ||
死鎖發生 |
根據以上原理,包括
select ... where id=x lock in share mode/for update;
if(found_rows()=0)
insert into id values 1;
也可能導致死鎖。
解決方法
根據select結果判斷,這里有極小的概率出現insert duplicate,因為每次多一次select,效率肯定不如原來的。如果還有問題可以試試使用insert ignore或者insert on duplicate key update。
SELECT 1 FROM table_a WHERE id=x;
IF(FOUND_ROWS() = 0) THEN
INSERT INTO table_a id VALUES x;
ELSE
UPDATE table_a SET ... WHERE id=x;
END IF;
以上。