生产问题一则: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隔离级别引发的数据读取失败问题的主要内容,如果未能解决你的问题,请参考以下文章

生产问题一则:MySQL隔离级别引发的数据读取失败问题

生产问题一则:MySQL隔离级别引发的数据读取失败问题

MySql中事务的隔离级别

图解MySQL事务隔离级别

MySQL | 五分钟搞清楚 MVCC 机制

MySQL读取的记录和我想象的不一致——事物隔离级别和MVCC