生产问题一则:MySQL隔离级别引发的数据读取失败问题
Posted 北亮bl
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了生产问题一则:MySQL隔离级别引发的数据读取失败问题相关的知识,希望对你有一定的参考价值。
先简介mysql的4种隔离级别和解决的3种问题:
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交 read-uncommitted | 是 | 是 | 是 |
读已提交 read-committed | 否 | 是 | 是 |
可重复读 repeatable-read | 否 | 否 | 是 |
串行化 serializable | 否 | 否 | 否 |
- 脏读:事务A新增或更新数据,还未提交,事务B就能读取到,然后事务A回滚了,导致事务B读取的是脏数据。
- 不可重复读:事务A读取一行数据,事务B更新该数据,事务A再次读取同一行数据,两次读取的结果是不一致的。
- 幻读:事务A读写范围数据,比如 between 1 and 3,得到1,3两行数据,事务B插入2或删除3,事务A再读取,发现数据多了或少了。
MySQL默认级别是 可重复读,查看当前级别的语句: SELECT @@transaction_isolation
MySQL解决这几种问题的主要原理是MVCC(多版本并发控制)和Gap Lock(间隙锁)。
正文,前几天发现一个线上问题:一个任务调度系统,在保存任务后,无法立即启动该任务。 简化后的代码实现如下:
@Transactional
public Autotask saveAndStart(AutotaskDto item) {
item.setId(0); // 任务只能新建
Autotask task = autotaskRepository.save(item.mapTo());
for (AutotaskdetailDto detailDto : item.getDetails()) {
detailDto.setId(0);
detailDto.setTaskid(task.getId());
autotaskdetailRepository.save(detailDto.mapTo());
}
runTask(task.getId()); // 启动任务
return task;
}
private void runTask(int id) {
ThreadHelper.exeAnsync(() -> {
Autotask taskRealTime = autotaskRepository.findById(id).orElse(null);
if (taskRealTime == null) {
throw new RuntimeException("task can't found:" + id);
}
// 其它业务逻辑
});
}
故障现象是偶发的,现象是1天或几天出现一次 task can’t found错误。
第一次Review代码未能发现有啥问题,于是通过AOP添加sql日志,日志记录方案参考,得到的日志整理后大致如下:
2021-06-29 09:30:07.799 DEBUG 17626 --- [http-nio-12130-exec-102] org.hibernate.SQL : insert into ops.Autotask(state, title, type, username) values(?, ?, ?, ?)
2021-06-29 09:30:07.801 DEBUG 17626 --- [http-nio-12130-exec-102] org.hibernate.SQL : insert into ops.Autotaskdetail(env, ipAddress, isGray, projectId, state, taskid) values(?, ?, ?, ?, ?, ?)
2021-06-29 09:30:07.949 DEBUG 17626 --- [pool-4-thread-3] org.hibernate.SQL : select * from ops.Autotask where id=?
从日志看到,SELECT确实在INSERT执行成功之后,为啥读取不到数据呢?
看完日志第一反应,会不会是读写分离导致的延迟问题?找阿里云的兄弟确认了一下,高可用版本的RDS没有只读实例,没有读写分离,也没有经过Proxy,所以跟读写分离无关。
同时,阿里云的兄弟说,会不会是2个会话,然后某个事务未提交导致的。
赶紧回去看代码,这才注意到方法上的注解:@Transactional
,恍然大悟。
我们的MySQL,设置的隔离级别是读已提交:事务B读取不到事务A未提交的更改。
由于代码启动任务是异步的,相当于新起了一个事务,由于计算机的多线程特性,代码执行的顺序是不确定的。
上述的代码,多数情况下,是方法saveAndStart
结束后(事务提交了),才启动的另一个事务,出问题的场景,是线程先启动,然后再结束saveAndStart
方法,此时就会导致新线程读取不到该方法里插入的数据。
问题定位了,解决就简单了:
- 方案1:在
autotaskRepository.findById
前面,增加Thread.sleep(1000)
,等一秒再启动。这个还是有隐患,原来的方法如果超过1秒才结束,问题依旧。 - 方案2:把save和start进行分离,推荐,职责单一。最好使用事件模式。
看起来,有时没定位到问题时,还是要对着小黄鸭,多讲讲代码啊。
以上是关于生产问题一则:MySQL隔离级别引发的数据读取失败问题的主要内容,如果未能解决你的问题,请参考以下文章