最近线上项目报了一个 MySQL 死锁 (DealLock) 错误, 虽说对业务上是没有什么影响的, 由于自己对数据库锁这块了解不是很多, 之前也没怎么的在线上碰到过. 这次刚好遇到了, 便在此记录一下.
出现死锁问题背景
项目层面: 报错的项目做的是一个批量下单的动作, 会同时写入多条订单数据, 代码之前写的是一个事务中一个循环一条一条 insert 到数据库(至于为啥没用批量插入就不追究了, 历史原因了).
数据库层面: 一张 test 表(非线上真实表), 比较重要的是有一个 type 和 name 的唯一索引. 事务隔离级别: read commited
- CREATE TABLE `test` (
- `id` bigint(11) NOT NULL ,
- `name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL ,
- `type` tinyint(4) NULL DEFAULT NULL ,
- `uid` int(11) NULL DEFAULT NULL ,
- PRIMARY KEY (`id`),
- UNIQUE INDEX `uniq_type_name` (`type`, `name`) USING BTREE
- )
- ENGINE=InnoDB
- DEFAULT CHARACTER SET=utf8 COLLATE=utf8_general_ci
- ROW_FORMAT=COMPACT
- ;
出现死锁的情况是批量下单的接口, 外部重复请求了两次, 两次相隔 10 毫秒.
两次请求执行的 sql(一次请求会执行三条 insert)除了主键 id 不一样, 其他都是一样的: 如下
- insert into test(id, name, type, uid) values(1, "DT590", 3, 1001);
- insert into test(id, name, type, uid) values(2, "DT589", 3, 1001);
- insert into test(id, name, type, uid) values(3, "DT588", 3, 1001);
报错的死锁日志:
- ------------------------
- LATEST DETECTED DEADLOCK
- ------------------------
- 2018-06-21 10:51:03 2b16deb03700
- *** (1) TRANSACTION:
- TRANSACTION 1905650677, ACTIVE 0.001 sec inserting
- mysql tables in use 1, locked 1
- LOCK WAIT 2 lock struct(s), heap size 360, 1 row lock(s), undo log entries 1
- LOCK BLOCKING MySQL thread id: 16983306 block 34208692
- MySQL thread id 34208692, OS thread handle 0x2b2203b0b700, query id 9093982364 172.24.18.106 app_redcliffc update
- INSERT INTO `test` (id, name, type, uid) VALUES (4, 'DT590', 3, 1001)
- *** (1) WAITING FOR THIS LOCK TO BE GRANTED:
- RECORD LOCKS space id 138 page no 16492 n bits 408 index `uniq_type_name` of table `db`.`test` trx id 1905650677 lock mode S waiting
- Record lock, heap no 341 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
- 0: len 4; hex 80000048; asc H;;
- 1: len 10; hex 44543631393230363835; asc DT61920685;;
- 2: len 8; hex 0461116807c09a00; asc a h ;;
- *** (2) TRANSACTION:
- TRANSACTION 1905650675, ACTIVE 0.004 sec inserting
- mysql tables in use 1, locked 1
- 3 lock struct(s), heap size 1184, 2 row lock(s), undo log entries 2
- MySQL thread id 16983306, OS thread handle 0x2b16deb03700, query id 9093982366 172.24.18.105 app_redcliffc update
- INSERT INTO `test` (id, name, type, uid) VALUES (2, 'DT589', 3, 1001)
- *** (2) HOLDS THE LOCK(S):
- RECORD LOCKS space id 138 page no 16492 n bits 408 index `uniq_type_name` of table `db`.`test` trx id 1905650675 lock_mode X locks rec but not gap
- Record lock, heap no 341 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
- 0: len 4; hex 80000048; asc H;;
- 1: len 10; hex 44543631393230363835; asc DT61920685;;
- 2: len 8; hex 0461116807c09a00; asc a h ;;
- *** (2) WAITING FOR THIS LOCK TO BE GRANTED:
- RECORD LOCKS space id 138 page no 16492 n bits 408 index `uniq_type_name` of table `db`.`test` trx id 1905650675 lock_mode X locks gap before rec insert intention waiting
- Record lock, heap no 341 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
- 0: len 4; hex 80000048; asc H;;
- 1: len 10; hex 44543631393230363835; asc DT61920685;;
- 2: len 8; hex 0461116807c09a00; asc a h ;;
- *** WE ROLL BACK TRANSACTION (1)
问题分析
死锁日志分析
session1 | session2 | |
insert into test(id, name, type, uid) values(1, "DT590", 3, 1001); | 事务一先到,先插入第一条记录 DT590,成功 | |
insert into test(id, name, type, uid) values(2, "DT589", 3, 1001); | 事务一继续插入第二天 DT589 记录,这个时候事务二请求到了,开始 插入第一条记录 DT590,然后就报出死锁,事务二回滚,事务一成功执行 | insert into test(id, name, type, uid) values(1, "DT590", 3, 1001); |
session1 | 持锁 | session2 | 持锁 |
insert into test(id, name, type, uid) values(1, "DT590", 3, 1001); | 插入一条数据库中没有的记录,对 DT590 这条记录加了一个 x 锁 | ||
insert into test(id, name, type, uid) values(2, "DT589", 3, 1001); | 这时事务一插入 DT589 时候,发现这条记录已经有了一个 gap lock(DT589 这条记录刚好被事务二插 DT590 时候申请的 gap lock 包含了),会先申请一个 insert intention waiting 插入意向锁,这个锁和事务二持有 gap lock 互斥,发生死锁。 事务一在等事务二释放这条记录 gap lock, 事务二在等事务一释放 DT590 X 锁 | insert into test(id, name, type, uid) values(1, "DT590", 3, 1001); | 事务二插入有唯一索引 DT590 这条记录,发现这条记录上已经有了 x 锁,所以会申请一个该条记录的 s 锁和 gap lock |
相关一些锁知识
InnoDB 锁细分为如下几种子类型:
record lock(RK) 锁直接加在索引记录上面, 锁住的是 key
gap lock(GK) 间隙锁, 锁定一个范围, 但不包括记录本身. GAP 锁的目的, 是为了防止同一事务的两次当前读, 出现幻读的情况
next key lock(NK) 行锁和间隙锁组合起来就叫 Next-Key Lock
insert intention lock(IK) 如果插入前, 该间隙已经由 gap 锁, 那么 Insert 会申请插入意向锁. 因为了避免幻读, 当其他事务持有该间隙的间隔锁, 插入意向锁就会被阻塞(不用直接用 gap 锁, 是因为 gap 锁不互斥)
insert 中对唯一索引的加锁逻辑 : 先做唯一索引冲突检测, 如果存在目标行, 会先对目标行加 S NK,
总结
1. 保证事务简短并在一个批处理中, 避免出现循环插入死锁问题
这里的场景记录死锁是并发插入多条记录, 顺序一样出现的死锁, 在并发插入中如果顺序不一样出现死锁的概率会更大.
来源: http://www.bubuko.com/infodetail-2660945.html