最近在工作中遇到了一个事务之间可读性的问题,业务场景是这样的:
用户创建了订单之后会在后台创建一个延时15分钟的任务,当15分钟到的时候会检测这个订单是否支付了,如果支付了那么就会取消这个延时任务,如果没有支付那么就取消这个订单,但是现在有用户反馈已经支付了但是订单还是取消了,于是就这个问题我到代码里找了一下,发现了这个可读性的问题。代码逻辑是这样的
@Transactional(value = "gaotuTransactionManager", readOnly = false, rollbackFor = RuntimeException.class)
public int cancelOrder(Long payNumber) {
//1、获取订单信息
//2、检查是否是未支付
//3、更新payInfo
return 1;
}
在这里更新payInfo使用的sql是
update order set status = 1 where payNumber = 123123
这样这里就会出现一个问题,就是当上面1、2步走完的时候恰好用户支付了订单,但是因为在2检查的时候是未支付的所以3还是会把这个订单取消掉,所以这里的sql这样写是有问题的,改成下面这样就可以了。
update order set status = 1 where payNumber = 123123 and status = 2
但是这样是不是真的就没有问题了呢,于是我到测试环境去验证了一下,结果如下:
首先我开了两个mysql链接并开启事务分别为A和B,A先开启事务但是B先结束事务:
A1: start transaction;
B1: start transaction;
A2: select * from order where payNumber = 1;
B2: update order set status = 1 where payNumber = 1;
A3: select * from order where payNumber = 1;
B3: commit;
A4: select * from order where payNumber = 1;
这个时候我发现三次select请求出来的结果重的status都是2也就是说尽管B3已经更新了但是A4还是读取的原来的数据,原因是mysql的innodb 默认的隔离级别是RR也就是可重复读,可重复读的情况下,某个事务首次read记录的时间为T,未来不会读取到T时间之后已提交事务写入的记录,以保证连续相同的read读到相同的结果集
如果这个时候把隔离级别换成是RC也就是读提交呢?结果会是什么样?
结果就是A1:2,A2:2,A3:2,A4:1,在A4的时候读取到了事务B提交的新的数据。
关于这个RR和RC为啥出现这种情况是因为mysql的innodb使用了快照读这个功能,什么是快照读呢?
MySQL数据库,InnoDB存储引擎,为了提高并发,使用MVCC机制,在并发事务时,通过读取数据行的历史数据版本,不加锁,来提高并发的一种不加锁一致性读(Consistent Nonlocking Read)。
在读提交(RC),可重复读(RR)两个不同的事务的隔离级别下,快照读有什么不同呢?
-
RC下,快照读总是能读到最新的行数据快照,当然,必须是已提交事务写入的
-
RR下,某个事务首次read记录的时间为T,未来不会读取到T时间之后已提交事务写入的记录,以保证连续相同的read读到相同的结果集
到目前算是解决的为什么订单状态异常的问题,果然上线之后用户类似的反馈没了。