昨晚我正在床上睡得着着的,忽然来了一条短信。
啥,线上MySQL死锁了,我连忙登录线上零碎,查看业务日志。
能分明看到是这条insert语句产生了死锁。
MySQL如果检测到两个事务产生了死锁,会回滚其中一个事务,让另一个事务执行胜利。很显著,咱们这条insert语句被回滚了。
insert into user (id, name, age) values (6, '张三', 6);
然而咱们怎么排查这个问题呢?
到底跟哪条SQL产生了死锁?
好在MySQL记录了最近一次的死锁日志,能够用命令行工具查看:
show engine innodb status;
在死锁日志中,能够分明地看到这两条insert语句产生了死锁,最终事务2被会回滚,事务1执行胜利。
# 事务1
insert into user (id,name,age) values (5,'张三',5);
# 事务2
insert into user (id,name,age) values (6,'李四',6);
这两条insert语句,怎么看也不像能产生死锁,咱们来还原一下事发过程。
先看一下对应的Java代码:
@Override
@Transactional
public void insertUser(User user) {
User userResult = userMapper.selectByIdForUpdate(user.getId());
// 如果userId不存在,就插入数据,否则更新
if (userResult == null) {
userMapper.insert(user);
} else {
userMapper.update(user);
}
}
业务逻辑代码很简略,如果userId不存在,就插入数据,否则更新user对象数据。
从死锁日志中,咱们看到有两条insert语句,很显著userId=5和userId=6的数据都不存在。
所以对应的SQL执行过程,可能就是这样的:
先用for update加上排他锁,避免其余事务批改以后数据,而后再insert数据,最初产生了死锁,事务2被回滚。
两个事务别离在两个主键ID下面加锁,为什么会产生死锁呢?
如果看过上篇文章,就会明确。
当id=5存在这条数据时,MySQL就会加Record Locks(记录锁),意思就是只在id=5这一条记录上加锁。
当id=5这条记录不存在时,就会锁定一个范畴。
假如表中的记录是这样的:
id | name | age |
---|---|---|
1 | 王二 | 1 |
10 | 一灯 | 10 |
select * from user where id=5 for update;
这条select语句锁定范畴就是 (1, 10]。
最初两个事务的执行过程就变成了:
通过这个示例看到,两个事务都能够先后锁定 (1, 10]这个范畴,阐明MySQL默认加的临键锁的范畴是能够穿插的。
那怎么解决这个死锁问题呢?
我能想到的解决办法就是,把这两个语句select和insert,合并成一条语句:
insert into user (id,name,age) values (5,'张三',5)
on duplicate key update name='张三',age=5;
大家有什么好方法吗?
这个死锁状况,还是挺常见的,连忙回去翻一下我的项目代码有没有这样的问题。
文章继续更新,能够微信搜一搜「 一灯架构 」第一工夫浏览更多技术干货。
发表回复