MySQL并发引发的死锁案例分析

在作项目的过程当中,因为写SQL太过随意,一不当心就抛了一个死锁异常,以下:java

com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction  
        at sun.reflect.GeneratedConstructorAccessor247.newInstance(Unknown Source)  
        at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:27)  
        at java.lang.reflect.Constructor.newInstance(Constructor.java:513)  
        at com.mysql.jdbc.Util.handleNewInstance(Util.java:406)  
        at com.mysql.jdbc.Util.getInstance(Util.java:381)  
        at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:1045)  
        at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:956)  
        at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3491)  
        at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3423)  
        at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:1936)  
        at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2060)  
        at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2542)  
        at com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:1734)  
        at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:2019)  
        at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1937)  
        at com.mysql.jdbc.PreparedStatement.executeUpdate(PreparedStatement.java:1922)

表结构以下:mysql

CREATE TABLE `user_item` (  
  `id` BIGINT(20) NOT NULL,  
  `user_id` BIGINT(20) NOT NULL,  
  `item_id` BIGINT(20) NOT NULL,  
  `status` TINYINT(4) NOT NULL,  
  PRIMARY KEY (`id`),  
  KEY `idx_1` (`user_id`,`item_id`,`status`)  
) ENGINE=INNODB DEFAULT CHARSET=utf-8

SQL语句以下:sql

update user_item set status=1 where user_id=? and item_id=?

缘由分析 MySQL的事务支持与存储引擎有关,MyISAM不支持事务,INNODB支持事务,更新时采用的是行级锁。这里采用的是INNODB作存储引擎,意味着会将update语句作为一个事务来处理。前面提到行级锁必须创建在索引的基础,这条更新语句用到了索引idx_1,因此这里确定会加上行级锁。 行级锁并非直接锁记录,而是锁索引,若是一条SQL语句用到了主键索引,mysql会锁住主键索引;若是一条语句操做了非主键索引,mysql会先锁住非主键索引,再锁定主键索引。 这个update语句会执行如下步骤: 一、因为用到了非主键索引,首先须要获取idx_1上的行级锁 二、紧接着根据主键进行更新,因此须要获取主键上的行级锁; 三、更新完毕后,提交,并释放全部锁。 若是在步骤1和2之间忽然插入一条语句:update user_item .....where id=? and user_id=?,这条语句会先锁住主键索引,而后锁住idx_1。 蛋疼的状况出现了,一条语句获取了idx_1上的锁,等待主键索引上的锁;另外一条语句获取了主键上的锁,等待idx_1上的锁,这样就出现了死锁。并发

解决方案 一、先获取须要更新的记录的主键spa

select id from user_item where user_id=? and item_id=?

二、逐条更新rest

for (Long id : idList) {  
    userItemDAO.updateStatus(id, userId, 1);  
}  
update user_item set status=? where id=? and user_id=?

这样貌似解决了,都是对单条进行操做,都是先获取主键上的锁,再获取idx_1上的锁。 不过这个解决方案与先前的更新语句不同,先前的更新语句对全部记录的更新在一个事务中,采用循环更新后并不在同一个事务中,因此在for循环外面还得开一个事务。code

Exception e = (Exception)getDbfeelTransactionTemplate().execute(new TransactionCallback() {  
   public Object doInTransaction(TransactionStatus status) {  
      try {  
            for(Long id:idList) {  
        <span style="white-space:pre">  </span>userItemDAO.updateStatus(id,userId,1)  
        }  
            return null;  
      }catch(DAOException e) {  
         status.setRollbackOnly();  
         return e;  
      }  
      catch (Exception e) {  
         status.setRollbackOnly();  
         return e;  
      }  
   }  
});

**小结:**在采用INNODB的MySQL中,更新操做默认会加行级锁,行级锁是基于索引的,在分析死锁以前须要查询一下mysql的执行计划,看看是否用到了索引,用到了哪一个索引,对于没有用索引的操做会采用表级锁。若是操做用到了主键索引会先在主键索引上加锁,而后在其余索引上加锁,不然加锁顺序相反。在并发度高的应用中,批量更新必定要带上记录的主键,优先获取主键上的锁,这样能够减小死锁的发生。索引

相关文章
相关标签/搜索