MySql有四种事务隔离级别,默认且常用的是可重复读(REPEATABLE-READ)。除了串行化级别外,其它三种级别在数据一致性方面都有或多或少的问题。自然的,不正确地使用可重复读隔离级别,也会引发数据不一致问题。
在排查一个重复提交问题时,发现了一个觉得“不太可能”的问题。现用伪代码的方式还原这个问题:
//*入口方法*//
modifyStaus(:id){
//第一次查询
Object obj=dao.select("select id,is_over,update_item_id from test_thread where id=:id");
//is_over只有两个值0和1
if(is_over==0){
//。。。省略一堆很占用时间逻辑
//变更状态为结束
int count=dao.update("update test_thread set is_over=1 where id=:id");
//调用另一个方法,备注这很可能是其它小组的同事写的一个接口
notifyOver(:id);
}
//。。。省略后续的逻辑
}
/*状态为结束时才调用*/
notifyOver(:id){
//第二次查询
Object obj=dao.select("select id,is_over,update_item_id from test_thread where id=:id");
if(obj.is_over!=1){
return;//状态不对
}
//。。。后续的逻辑
}
代码常见这种结构,即“查询-更新-再查询”,也即是重复读了一次。MySql可重复读的基本含义是:在同一事务内,只要某条记录没在本事务内修改数据,那么不管查询多少次,数据内容都与第一次查询时的内容一样。上面的代码执行后,原本以为notifyOver方法中查询到的is_over的值一定是1,实际并发运行后is_over的值可能是0,也可能是1。起初也觉得奇怪,毕竟调用过update操作,第二次查询应该是update之后的值。
这个问题比较不通情理,所以直接用Mysq命令行模拟了两个线程进行“查询-更新-再查询”的操作,最后得出结论:只有某条记录被成功更新(即update返回值为1)时,第二次查询才会查到(本事务内)更新之后的最新值,如果update语句返回值是0,那么后续查询仍然与第一次查询内容一样。这即是可重复读隔离级别的含义。
Mysql命令行测试:(is_over的初值是0)
Mysql命令行测试结论:
A线程执行完第1步后,只要被B线程抢先执行完commit,在A线程未提交事务之前查到的is_over永远是0。
究其原因还是AB两个线程对这条记录更新的内容完全一样,也就是只能有一个线程的update返回更新记录数是1。如果A线程执行update test_thread set is_over=10 where id=1;那么A线程第3步查到的is_over是10而不是0。
备注:以上命令在Mysql5.7和Mysql8.0测试结果一致。
最后总结:代码中执行update,一定要处理返回值。