首次碰到 INSERT + UPDATE statement 造成的死锁问题,触及了知识盲区,费时做了分析。

死锁场景描述

下述所有事务隔离级别为 Read Committed

1. 简化后的数据库表

CREATE TABLE `user_address` (
    `id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
    `user_id` varchar(40) DEFAULT '' COMMENT '用户主键',
    `comm_address` varchar(1) DEFAULT NULL COMMENT '常用地址标识',
    `address` varchar(200) DEFAULT NULL COMMENT '地址',
    `longitude` bigint(20) DEFAULT NULL COMMENT '经度',
    `latitude` bigint(20) DEFAULT NULL COMMENT '纬度',
    `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '建立时间',
    `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
    PRIMARY KEY (`id`),
    KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='用户常用地址';

2. 死锁事务时序

时序事务 1事务 2
1START TRANSACTION
2START TRANSACTION
3INSERT INTO user_address(user_id, address, longitude, latitude, create_time, update_time) VALUES ( 'c86d0f931d164fc1b53162355cf39fb4', '浙江省 杭州市 余杭区 五常街道 XXX', 121000000, 31000000, NOW(), NOW());
4INSERT INTO user_address(user_id, address, longitude, latitude, create_time, update_time) VALUES ( 'c86d0f931d164fc1b53162355cf39fb4', '浙江省 杭州市 余杭区 五常街道 XXX', 121000000, 31000000, NOW(), NOW());
5UPDATE user_address SET comm_address = '0' WHERE user_id = 'c86d0f931d164fc1b53162355cf39fb4';
6UPDATE user_address SET comm_address = '0' WHERE user_id = 'c86d0f931d164fc1b53162355cf39fb4';

3. INNODB Monitor Output

------------------------

LATEST DETECTED DEADLOCK

------------------------

2020-11-03 08:25:25 0x7f2801e1d700

*** (1) TRANSACTION:

TRANSACTION 13335216894, ACTIVE 0 sec fetching rows

mysql tables in use 1, locked 1

LOCK WAIT 4 lock struct(s), heap size 1136, 3 row lock(s), undo log entries 1

MySQL thread id 680655, OS thread handle 139810239182592, query id 120901872537 10.1.X.YYY mask_user updating

UPDATE `user_address` SET

		`comm_address` = '0'

		WHERE `user_id` = 'c760c70e42924b869beee418d3b30165'

*** (1) WAITING FOR THIS LOCK TO BE GRANTED:

RECORD LOCKS space id 2383 page no 125759 n bits 328 index idx_user_id of table `default`.`user_address` trx id 13335216894 lock_mode X locks rec but not gap waiting

Record lock, heap no 262 PHYSICAL RECORD: n_fields 2; compact format; info bits 0

 0: len 20; hex 6337363063373065343239323462383639626565; asc c760c70e42924b869bee;;

 1: len 4; hex 803c4394; asc  <C ;;



*** (2) TRANSACTION:

TRANSACTION 13335216895, ACTIVE 0 sec starting index read, thread declared inside InnoDB 5000

mysql tables in use 1, locked 1

3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1

MySQL thread id 672593, OS thread handle 139809806997248, query id 120901872539 10.1.X.YYY mask_user updating

UPDATE `user_address` SET

		`comm_address` = '0'

		WHERE `user_id` = 'c760c70e42924b869beee418d3b30165'

*** (2) HOLDS THE LOCK(S):

RECORD LOCKS space id 2383 page no 125759 n bits 328 index idx_user_id of table `default`.`user_address` trx id 13335216895 lock_mode X locks rec but not gap

Record lock, heap no 262 PHYSICAL RECORD: n_fields 2; compact format; info bits 0

 0: len 20; hex 6337363063373065343239323462383639626565; asc c760c70e42924b869bee;;

 1: len 4; hex 803c4394; asc  <C ;;



*** (2) WAITING FOR THIS LOCK TO BE GRANTED:

RECORD LOCKS space id 2383 page no 125759 n bits 328 index idx_user_id of table `default`.`user_address` trx id 13335216895 lock_mode X locks rec but not gap waiting

Record lock, heap no 261 PHYSICAL RECORD: n_fields 2; compact format; info bits 0

 0: len 20; hex 6337363063373065343239323462383639626565; asc c760c70e42924b869bee;;

 1: len 4; hex 803c4393; asc  <C ;;



*** WE ROLL BACK TRANSACTION (2)

分析

前置知识:

  1. INSERT 会对插入行设置 X 锁 (针对不同的隔离级别,会有所不同)

INSERT sets an exclusive lock on the inserted row. This lock is an index-record lock, not a next-key lock (that is, there is no gap lock) and does not prevent other sessions from inserting into the gap before the inserted row.

  1. 记录锁锁的是索引记录。DML 使用主键(聚簇)索引时,InnoDB会锁住主键索引;使用非主键(非聚簇/辅助)索引时,InnoDB会先锁住非主键索引,再锁定主键索引。
操作事务 1事务 2
1START TRANSACTION
2START TRANSACTION
3INSERT INTO user_address(user_id, address, longitude, latitude, create_time, update_time) VALUES ( 'c86d0f931d164fc1b53162355cf39fb4', '浙江省 杭州市 余杭区 五常街道 XXX', 121000000, 31000000, NOW(), NOW());
新记录 A 针对聚簇索引被加了 记录锁 (X lock)
4INSERT INTO user_address(user_id, address, longitude, latitude, create_time, update_time) VALUES ( 'c86d0f931d164fc1b53162355cf39fb4', '浙江省 杭州市 余杭区 五常街道 XXX', 121000000, 31000000, NOW(), NOW());
新记录 B 针对聚簇索引被加了 记录锁 (X lock)
5UPDATE user_address SET comm_address = '0' WHERE user_id = 'c86d0f931d164fc1b53162355cf39fb4';
执行计划命中 idx_user_idUPDATE 操作需要获取非聚簇索引 idx_user_id 的记录锁,但由于记录对应的主键索引已被获取,进入锁等待队列
6UPDATE user_address SET comm_address = '0' WHERE user_id = 'c86d0f931d164fc1b53162355cf39fb4';
执行计划命中 idx_user_idUPDATE 操作需要获取非聚簇索引 idx_user_id 的记录锁,但由于记录对应的主键索引已被获取,进入锁等待队列,且排在事务 1 后面

操作 3、4、5、6 造成了:事务 1 的继续执行必须等待事务 2 释放操作 4 加的锁;事务 2 的继续执行必须等待事务 1 操作 5 先获得锁,并释放。死锁条件达成,MySQL 基于检测结果在两个事务间选择了影响较小的事务 2 强制执行了 Rollback 操作。

参考资料

[1]. 14.7.1 InnoDB Locking [2]. 14.7.3 Locks Set by Different SQL Statements in InnoDB