记一次 MySQL 的死锁

441 阅读7分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第5天,点击查看活动详情

Mysql Deadlock

最近项目中突然遇到 MySQL 的死锁问题,日志为:Error updating database. Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction。突然联想到有次面试面试官让我手写 MySQL 死锁的代码。今天借此机会总结一下死锁的问题。

死锁代码

 create database test;
 use test;

CREATE TABLE `test` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `status` tinyint DEFAULT '0' COMMENT '0:未发布;1:已发布',
  `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`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='测试数据库';

-- session1
begin;
update test set status=1 where id=1;
update test set status=1 where id=2;
commit;

-- session2
begin;
update test set status=1 where id=2;
update test set status=1 where id=1;
begin;

-- 错误日志
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

下图显示以上代码执行结果,标记结果也可以看出 MySQL 的默认事务隔离级别。可重复读即 session2 中不会读到session1 中未提交的数据。 image.png

下图可以看到 Mysql 死锁的日志。即 session1 在等待 session2 释放锁;session2 在等待 session1 释放锁,最终就会出现死锁image.png

死锁分析


-- mysql 查询死锁的方法

show engine innodb status;

-- 需要关心的日志如下
LATEST DETECTED DEADLOCK
------------------------
2022-08-13 23:38:55 0x16e39b000
*** (1) TRANSACTION:
TRANSACTION 3349, ACTIVE 185 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1128, 2 row lock(s), undo log entries 1
MySQL thread id 10, OS thread handle 6159429632, query id 25 localhost root updating
update test set status=1 where id=2

*** (1) HOLDS THE LOCK(S):
RECORD LOCKS space id 7 page no 4 n bits 72 index PRIMARY of table `test`.`test` trx id 3349 lock_mode X locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
 0: len 8; hex 8000000000000001; asc         ;;
 1: len 6; hex 000000000d15; asc       ;;
 2: len 7; hex 020000010a03a3; asc        ;;
 3: len 1; hex 81; asc  ;;
 4: len 4; hex 62f7c4bf; asc b   ;;
 5: len 4; hex 62f7c4d6; asc b   ;;


*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 7 page no 4 n bits 72 index PRIMARY of table `test`.`test` trx id 3349 lock_mode X locks rec but not gap waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
 0: len 8; hex 8000000000000002; asc         ;;
 1: len 6; hex 000000000d16; asc       ;;
 2: len 7; hex 01000001140519; asc        ;;
 3: len 1; hex 81; asc  ;;
 4: len 4; hex 62f7c4bf; asc b   ;;
 5: len 4; hex 62f7c517; asc b   ;;
 
 
*** (2) TRANSACTION:
TRANSACTION 3350, ACTIVE 120 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1128, 2 row lock(s), undo log entries 1
MySQL thread id 11, OS thread handle 6160543744, query id 26 localhost root updating
update test set status=1 where id=1

*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 7 page no 4 n bits 72 index PRIMARY of table `test`.`test` trx id 3350 lock_mode X locks rec but not gap
Record lock, heap no 3 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
 0: len 8; hex 8000000000000002; asc         ;;
 1: len 6; hex 000000000d16; asc       ;;
 2: len 7; hex 01000001140519; asc        ;;
 3: len 1; hex 81; asc  ;;
 4: len 4; hex 62f7c4bf; asc b   ;;
 5: len 4; hex 62f7c517; asc b   ;;


*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 7 page no 4 n bits 72 index PRIMARY of table `test`.`test` trx id 3350 lock_mode X locks rec but not gap waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
 0: len 8; hex 8000000000000001; asc         ;;
 1: len 6; hex 000000000d15; asc       ;;
 2: len 7; hex 020000010a03a3; asc        ;;
 3: len 1; hex 81; asc  ;;
 4: len 4; hex 62f7c4bf; asc b   ;;
 5: len 4; hex 62f7c4d6; asc b   ;;

*** WE ROLL BACK TRANSACTION (2)
......
......

上面展示了整个死锁发生的日志。只要搜索对应的 SQL 就可以收集到关键信息,下面简单分析下死锁的整个过程。

  • session1 开启事务,并获取 id=1 数据的锁;
  • session2 开启事务,并获取 id=2 数据的锁;
  • session1 开启事务,尝试获取 id=2 数据的锁,并等待;
  • session2 开启事务,尝试获取 id=1 数据的锁,并等待;
  • 检测到死锁的出现,Mysql 回滚 session2 session1 并记录日志。

解决方案

对于 InnoDB 存储引擎来说,死锁的发生有很多种情况,总结来说就是在不同的事务中同时修改同一资源所造成的。

那么避免死锁的方式有:

  • 通过表锁来减少死锁产生的概率(效率低)
  • 多个程序尽量约定以相同的顺序访问表 (类似队列)
  • 在一个事务中尽可能做到一次锁定所有所需的数据 (逻辑复杂)

Spring Deadlock

上文分析了 SQL 语句中死锁的发生条件及解决方法,那么在日常 Coding 的时候底层的数据库操作都交给了 ORM(Mybatis) 或者 Spring 。日常工作中如何解决死锁?

其实死锁的发生的条件大同小异,对于 Spring 来说我们一定要注意事务的粒度。也就是要注意@Transactional注解的使用。尤其是要熟悉事务的传播机制,不要将所有的代码都包裹在一个事务被运行,在真正需要更新数据的时候才使用锁、事务。

Transactional propagation

  • REQUIRED 支持当前事务,如果当前没有事务,就新建一个事务
  • SUPPORTS 支持当前事务,如果没有当前事务,就以非事务方式执行
  • MANDATORY 支持当前事务,如果当前没有事务,就抛出异常
  • REQUIRES_NEW 新建事务,如果存在当前事务,把当前事务挂起
  • NOT_SUPPORTED 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起
  • NEVER 以非事务方式执行,如果当前存在事务,则抛出异常
  • NESTED 新建事务,依赖于上级事务,如果失败则一同回滚;

注意: spring事务中,默认是只有运行时异常(RuntimeException及其子类)才会回滚,而且使用的默认传播性是REQUIRED。可以对着英文的注释一同去理解。

public enum Propagation {

   /**
    * Support a current transaction, create a new one if none exists.
    * Analogous to EJB transaction attribute of the same name.
    * <p>This is the default setting of a transaction annotation.
    */
   REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED),

   /**
    * Support a current transaction, execute non-transactionally if none exists.
    * Analogous to EJB transaction attribute of the same name.
    * <p>Note: For transaction managers with transaction synchronization,
    * {@code SUPPORTS} is slightly different from no transaction at all,
    * as it defines a transaction scope that synchronization will apply for.
    * As a consequence, the same resources (JDBC Connection, Hibernate Session, etc)
    * will be shared for the entire specified scope. Note that this depends on
    * the actual synchronization configuration of the transaction manager.
    * @see org.springframework.transaction.support.AbstractPlatformTransactionManager#setTransactionSynchronization
    */
   SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS),

   /**
    * Support a current transaction, throw an exception if none exists.
    * Analogous to EJB transaction attribute of the same name.
    */
   MANDATORY(TransactionDefinition.PROPAGATION_MANDATORY),

   /**
    * Create a new transaction, and suspend the current transaction if one exists.
    * Analogous to the EJB transaction attribute of the same name.
    * <p><b>NOTE:</b> Actual transaction suspension will not work out-of-the-box
    * on all transaction managers. This in particular applies to
    * {@link org.springframework.transaction.jta.JtaTransactionManager},
    * which requires the {@code javax.transaction.TransactionManager} to be
    * made available to it (which is server-specific in standard Java EE).
    * @see org.springframework.transaction.jta.JtaTransactionManager#setTransactionManager
    */
   REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW),

   /**
    * Execute non-transactionally, suspend the current transaction if one exists.
    * Analogous to EJB transaction attribute of the same name.
    * <p><b>NOTE:</b> Actual transaction suspension will not work out-of-the-box
    * on all transaction managers. This in particular applies to
    * {@link org.springframework.transaction.jta.JtaTransactionManager},
    * which requires the {@code javax.transaction.TransactionManager} to be
    * made available to it (which is server-specific in standard Java EE).
    * @see org.springframework.transaction.jta.JtaTransactionManager#setTransactionManager
    */
   NOT_SUPPORTED(TransactionDefinition.PROPAGATION_NOT_SUPPORTED),

   /**
    * Execute non-transactionally, throw an exception if a transaction exists.
    * Analogous to EJB transaction attribute of the same name.
    */
   NEVER(TransactionDefinition.PROPAGATION_NEVER),

   /**
    * Execute within a nested transaction if a current transaction exists,
    * behave like {@code REQUIRED} otherwise. There is no analogous feature in EJB.
    * <p>Note: Actual creation of a nested transaction will only work on specific
    * transaction managers. Out of the box, this only applies to the JDBC
    * DataSourceTransactionManager. Some JTA providers might support nested
    * transactions as well.
    * @see org.springframework.jdbc.datasource.DataSourceTransactionManager
    */
   NESTED(TransactionDefinition.PROPAGATION_NESTED);


   private final int value;


   Propagation(int value) {
      this.value = value;
   }

   public int value() {
      return this.value;
   }

}