对 MySQL 死锁不是特别擅长,业务中遭遇了,就尝试分析一下了。
首先有篇不错的关于 MySQL 加锁机制的文章以及一个很有意思的 INSERT 死锁例子,Mark 一下。
业务需求,写出了类似这样的 SQL:
1
2
INSERT INTO site(third_party_id, data) VALUES(%s, %s)
ON DUPLICATE KEY UPDATE data = %s
其中 site
表主键 id
自增,唯一索引 third_party_id
。
SQL 目的很简单,就是从一张三方表中导数据到自己业务的一张表中间。 就这样简单的一句 SQL,没有复杂的事务,只是会出现大量的并发。
并发跑起来之后,很快就会发现程序报出大量死锁,看下死锁记录,可以抓到一条死锁信息大致如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
*** (1) TRANSACTION:
TRANSACTION 4185716323, ACTIVE 0.021 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 5 lock struct(s), heap size 1184, 3 row lock(s)
LOCK BLOCKING MySQL thread id: 252573051 block 235860837
MySQL thread id 235860837, OS thread handle 0x7f6fcc2a4700, query id 1581628064 172.16.7.71 prod update
...
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 794 page no 8 n bits 128 index `PRIMARY` of table ``.`` trx id 4185716323 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 35; compact format; info bits 0
...
*** (2) TRANSACTION:
TRANSACTION 4185716326, ACTIVE 0.014 sec inserting
mysql tables in use 1, locked 1
5 lock struct(s), heap size 1184, 3 row lock(s)
MySQL thread id 235818269, OS thread handle 0x7f6d88fbe700, query id 1581628068 172.16.7.71 prod update
...
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 794 page no 8 n bits 128 index `PRIMARY` of table ``.`` trx id 4185716326 lock_mode X locks gap before rec
Record lock, heap no 2 PHYSICAL RECORD: n_fields 35; compact format; info bits 0
...
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 794 page no 8 n bits 128 index `PRIMARY` of table ``.`` trx id 4185716326 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 35; compact format; info bits 0
这里事务 2 由于是 INSERT with ON UPDATE
,在 constraint check 中会直接上 X 锁,参见这里。
而同时我们这里主键让其自增,针对 third_party_id
字段来做的操作,于是导致主键索引产生的 gap 锁而不是 record 锁。
接下来,与这里类似,事务2在有了 X gap 锁之后,又申请了个其实没有必要的 insert intention lock,
这一请求排在了事务1的 insert intention lock 请求之后,于是带来了死锁。
这里对于这一导入数据的业务需求,其实一次导入中并不会出现多个相同的 third_party_id
,
于是我们直接 SELECT 出来,判断一下再 INSERT 或者 UPDATE 即可,两条语句间也不需要做事务,因为不会中途被改掉。