一则由于索引导致的MySQL死锁分析

涉及死锁的 authorized_user 表的 DDL

CREATE TABLE `authorized_user` (
  `id`          INT(11) UNSIGNED NOT NULL AUTO_INCREMENT
  COMMENT '自增id',
  `username`    VARCHAR(50)      NOT NULL
  COMMENT '用户名',
  `password`    VARCHAR(200)     NOT NULL
  COMMENT '加密后的密码',
  `description` VARCHAR(500)     NOT NULL DEFAULT ''
  COMMENT '描述',
  `status`      TINYINT(4)       NOT NULL DEFAULT 0
  COMMENT '状态,0:已注册,1:已激活,3:已锁定,4:已注销',
  `phone_no`    VARCHAR(15)      NOT NULL DEFAULT ''
  COMMENT '手机号',
  `email`       VARCHAR(75)      NOT NULL DEFAULT ''
  COMMENT '电子邮件',
  `create_time` TIMESTAMP        NOT NULL DEFAULT CURRENT_TIMESTAMP
  COMMENT '创建时间',
  `update_time` TIMESTAMP        NOT NULL DEFAULT CURRENT_TIMESTAMP
  ON UPDATE CURRENT_TIMESTAMP
  COMMENT '最近修改时间',
  PRIMARY KEY (`id`),
  KEY `index_username` (`username`)
)
  ENGINE = InnoDB
  DEFAULT CHARSET = utf8
  COMMENT = '用户表';

死锁日志

2016-11-27 15:57:13 7fe166a92700InnoDB: transactions deadlock detected, dumping detailed information.
2016-11-27 15:57:13 7fe166a92700
*** (1) TRANSACTION:
TRANSACTION 80660631141, ACTIVE 0 sec fetching rows
mysql tables in use 1, locked 1
LOCK WAIT 7 lock struct(s), heap size 2936, 8 row lock(s)
MySQL thread id 126366554, OS thread handle 0x7fe18143e700, query id 2011631849 172.18.3.148 acc Searching rows for update
UPDATE authorized_user SET status = 1 WHERE username = 'wcy'
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 669 page no 937 n bits 136 index `PRIMARY` of table `test`.`authorized_user` trx table locks 1 total table locks 2  trx id 80660631141 lock_mode X locks rec but not gap waiting lock hold time 0 wait time before grant 0 
*** (2) TRANSACTION:
TRANSACTION 80660631143, ACTIVE 0 sec updating or deleting, thread declared inside InnoDB 4999
mysql tables in use 1, locked 1
4 lock struct(s), heap size 1184, 3 row lock(s), undo log entries 1
MySQL thread id 126366547, OS thread handle 0x7fe166a92700, query id 2011631851 172.18.3.154 acc updating
UPDATE authorized_user SET username = 'wcy100' WHERE id = 1
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 669 page no 937 n bits 136 index `PRIMARY` of table `test`.`authorized_user` trx table locks 1 total table locks 2  trx id 80660631143 lock_mode X locks rec but not gap lock hold time 0 wait time before grant 0 
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 669 page no 13359 n bits 1176 index `index_username` of table `test`.`authorized_user` trx table locks 1 total table locks 2  trx id 80660631143 lock_mode X locks rec but not gap waiting lock hold time 0 wait time before grant 0 
*** WE ROLL BACK TRANSACTION (2)

根据 MySQL 日志分析出来的涉及死锁的 SQL 语句分析

mysql> explain UPDATE authorized_user SET status = 1 WHERE username = 'wcy' \G
*************************** 1. row ***************************
           id: 1
  select_type: UPDATE
        table: authorized_user
   partitions: NULL
         type: range
possible_keys: index_username
          key: index_username
      key_len: 152
          ref: const
         rows: 1
     filtered: 100.00
        Extra: Using where
1 row in set (0.00 sec)
mysql> explain UPDATE authorized_user SET username = 'wcy100' WHERE id = 1 \G
*************************** 1. row ***************************
           id: 1
  select_type: UPDATE
        table: authorized_user
   partitions: NULL
         type: range
possible_keys: PRIMARY
          key: PRIMARY
      key_len: 4
          ref: const
         rows: 1
     filtered: 100.00
        Extra: Using where
1 row in set (0.00 sec)

原因分析

InnoDB 引擎,更新操作默认会加行级锁,行级锁会对索引加锁。如果更新语句使用多个索引,行锁会先锁非聚簇索引,再锁聚簇索引。如果两个事务中的 SQL 用到了不同的非聚簇索引或者一个用了一个没有使用(即使用索引的情况不同),这样的话就会导致这两个事务加行锁的顺序不一致,形成了多个事务之间资源(行锁)的循环等待,构成了死锁的四个必要条件之一,而其他3个条件(互斥、请求与保持、不剥夺)已经满足,所以最终导致了死锁。

具体分析(以死锁日志1为例)

由日志分析可得:

事务1用到的 SQL 语句:
UPDATE authorized_user SET status = 1 WHERE username = 'wcy'
事务1获取的锁:X lock on (669,13359,1176) index_username
事务1等待的锁:X lock on (669,937,136) PRIMARY

事务2用到的 SQL 语句:
UPDATE authorized_user SET username = 'wcy100' WHERE id = 1
事务2获取的锁:X lock on (669,937,136) PRIMARY
事务2等待的锁:X lock on (669,13359,1176) index_username

导致这种情况的 SQL 执行流程说明:

事务1的 update 语句正常的执行步骤如下:
1.由于用到了非聚簇索引,首先需要获取 index_username 上的行级锁。
2.紧接着根据主键进行更新,所以需要获取 PRIMAEY 上的行级锁。
3.更新完毕后,提交,并释放所有锁。

但是,如果在步骤1和2之间突然插入事务2的这条 SQL 语句:
UPDATE authorized_user SET username = 'wcy100' WHERE id = 1, 这条语句会先锁住聚簇索引,然后由于其更新的字段 status 有非聚簇索引,所以这条语句需要锁住 index_username。

这时候的情况变成了:事务1获取了 index_username 上的锁,等待 PRIMARY 上的锁;事务2获取了 PRIMARY 上的锁,等待 index_username 上的锁,这样就出现了死锁。

解决方案

可以将原先的单条批量 update 语句分拆成如下步骤(此处以事务1的 SQL 语句为例):

1.先获取需要更新的记录的主键

select id from authorized_user WHERE username = 'wcy'

2.开启一个事务,逐条更新

for (Integer id : ids) {    
    updateStatusById(id,1);
}

这样每一次更新操作都是针对单条记录先获取 PRIMARY 上的锁,再获取 index_username 上的锁,避免了由于在获取行锁的时候,锁索引的顺序不一致造成的死锁。

你可能感兴趣的:(一则由于索引导致的MySQL死锁分析)