乐趣区

关于java:记一次排查线上MySQL死锁过程不能只会curd还要知道加锁原理

昨晚我正在床上睡得着着的,忽然来了一条短信。

啥,线上 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;

大家有什么好方法吗?

这个死锁状况,还是挺常见的,连忙回去翻一下我的项目代码有没有这样的问题。

文章继续更新,能够微信搜一搜「一灯架构」第一工夫浏览更多技术干货。

退出移动版