之前我只知道MyBatis在重复查询查询相同SQL时,为了避免直接对数据库进行查询,提高性能,所以加入了缓存机制,但是一直都没有太注意这个问题。
直到最近在工作中发现一个问题:在一个事务中多次查询Oracle的sequence时,竟然返回了相同的值!
发现这个问题是因为一个主订单会存在多个商品,在写入订单详情的时候,就需要创建多个订单详情,因为我们使用的是Oracle,所以创建了一个sequence用于生成订单详情单号。
众(ni)所(bu)周(zhi)知(dao),这一块的内容都是前人写的,之前一直是在他的基础上进行一些修改。最近有个需求比较独立,所以就单独做,不过收单这一块的流程还是大致保持了原来的逻辑,包括这次的主角,订单详情单号。
订单详情需要对每个商品创建一条记录,大概是这样的:
for (Goods goods : goodsList) {
Long id = SeqMapper.getSeq();
XXXXX
}
<select id="getSeq" resultType="java.lang.Long">
select SEQ_TEST_ID.nextval from dual
</select>
收单流程涉及到许多SQL,我们需要保证它们的原子性,所以必须使用事务。
然而问题来了,测试的时候发现报主键重复的错误。
debug看了看,SeqMapper.getSeq()
时确实每次返回的值都是一样,为什么呢?
难道是在一次事务中取的sequence值都一样?
于是我尝试性的修改了一下事务的传播性,增加了一个方法:
@Transactional(propagation = Propagation.REQUIRES_NEW)
public String getSeqNo() {
return SeqMapper.getSeq();
}
因为循环调用时每次都新开了一个事务,所以就能够正确取到不同的sequence值了。
虽然这样从功能来说是没有问题了,但是自觉告诉我肯定不是这样的,因为sequence是原子性的,即使是在一个事务中也不可能nextval时返回相同的值。
并且,为什么之前的代码没有问题?
于是我认真看了下以前的代码,发现了一个不同之处。
之前的老哥在SeqMapper.getSeq()
后,马上就构建好数据insert了一次,而我是SeqMapper.getSeq()
并构建好数据后,先存入了list,之后一并insert。
为了确定是这个原因,我做了一个测试,在SeqMapper.getSeq()
后分别做了insert/update/select操作,发现加入insert/update都可以使SeqMapper.getSeq()
获取到最新的值。
到这里我恍然大悟,肯定是因为MyBatis的缓存机制的原因。
因为insert/update这类的操作会删除掉缓存,所以下一次SeqMapper.getSeq()
一定就是新值了。
而select或者不做任何操作,SeqMapper.getSeq()
则是从MyBatis的缓存中获取的值,不会去数据库执行操作,所以就不会取到新值。
为什么我能想到这里呢?是因为MySQL自己也有缓存,你连续两次相同的select会发现第二次比第一次快,就是因为MySQL也有缓存,而当你对表做insert/update/delete之类的操作时MySQL防止脏读就会删除缓存。
所以我们就可以确定是MyBatis的缓存问题了,解决也很简单:flushCache="true"
<select id="getSeq" resultType="java.lang.Long" flushCache="true">
select SEQ_TEST_ID.nextval from dual
</select>
每次刷新缓存就可以了。
这次解决问题还是很轻松的,几分钟时间就定位解决了问题,不过因为MyBatis缓存之前接触的很少,所以就需要总结总结。
最后还是得补习一下MyBatis缓存的基本知识。
MyBatis缓存分为一级缓存和二级缓存,它们的作用域不同。
一级缓存是Sql Session级别,每个会话拥有一个独立的Local Cache,即使是执行不同mapper的sql都可以使用到缓存。
二级缓存是Mapper级别,xml中同一个namespace共享一个cache,即使是在多个Sql Session中,只要是同一个Mapper都能够使用到缓存。
开启缓存后,SQL查询的顺序是:二级缓存 -> 一级缓存 -> 数据库