昨晚正在床上睡觉,突然来了一条短信。什么,线上MySQL死机了,赶紧登录线上系统查看业务日志。可以很明显的看出这条insert语句出现了死锁。如果MySQL检测到两个事务之间存在死锁,它将回滚其中一个事务并允许另一个事务成功执行。很明显,我们的插入语句被回滚了。insertintouser(id,name,age)values(6,'张三',6);但是我们如何解决这个问题呢?哪个SQL有死锁?好在MySQL已经记录了最新的死锁日志,可以通过命令行工具查看:showengineinnodbstatus;在死锁日志中,可以清楚的看到是两条insert语句造成了死锁,最终事务2被阻塞回滚,事务1执行成功。#事务1insertintouser(id,name,age)values(5,'张三',5);#事务2insertintouser(id,name,age)values(6,'李四',6);这两条insert语句,不管怎么看,好像都不能造成死锁。让我们还原一下事件的过程。先看对应的Java代码:@Override@TransactionalpublicvoidinsertUser(Useruser){UseruserResult=userMapper.selectByIdForUpdate(user.getId());//如果userId不存在,则插入数据,否则更新if(userResult==null){userMapper.insert(user);}else{userMapper.update(user);}}业务逻辑代码很简单,如果userId不存在则插入数据,否则更新用户对象数据。从死锁日志中我们看到有两条insert语句,很明显userId=5和userId=6的数据是不存在的。所以对应的SQL执行过程可能是这样的:先用forupdate加排它锁,防止其他事务修改当前数据,然后插入数据,最后发生死锁,事务2回滚。两个事务分别锁定在两个主键ID上,为什么会死锁?如果你看了上一篇文章,你就会明白。当有id=5的这条数据时,MySQL会加上RecordLocks(记录锁),也就是只锁定id=5的记录。当id=5的记录不存在时,将锁定一个范围。假设表中的记录是这样的:idnameage1Wanger110Yideng10select*fromuserwhereid=5forupdate;这条select语句的锁定范围是(1,10]。最后两个事务的执行过程变成:通过这个例子可以看出,两个事务都可以依次锁定范围(1,10],说明范围mysql默认加的key锁是可以越界的,那么这个死锁问题怎么解决呢?我能想到的办法就是把select和insert这两条语句合并成一条语句:insertintouser(id,name,age)values(5,'ZhangSan',5)onduplicatekeyupdatename='ZhangSan',age=5;大家有什么好的解决方法吗?这种死锁情况比较常见,回头查看项目代码即可看看有没有这样的问题,文章不断更新,大家可以第一时间微信搜索“一灯架构”阅读更多技术资料。
