刚准备下班走人,被一开发同事叫住,让帮看一个比较奇怪的问题:Mybatis同一个Mapper接口的查询方法,第一次返回与第二次返回结果不同,百思不得其解!java
Talk is cheap. Show me the code. 该问题涉及的主要代码实现包括sql
mapper接口定义数据库
public interface GoodsTrackMapper extends BaseMapper<GoodsTrack> {
List<GoodsTrackDTO> listGoodsTrack(@Param("criteria") GoodsTrackQueryCriteria criteria);
}复制代码
xml定义缓存
<select id="listGoodsTrack" resultType="xxx.GoodsTrackDTO">
SELECT ...
</select>复制代码
service定义微信
@Service
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true, rollbackFor = Exception.class)
public class GoodsTrackService extends BaseService<GoodsTrack, GoodsTrackDTO> {
@Autowired
private GoodsTrackMapper goodsTrackMapper;
public List<GoodsTrackDTO> listGoodsTrack(GoodsTrackQueryCriteria criteria){
return goodsTrackMapper.listGoodsTrack(criteria);
}
public List<GoodsTrackDTO> goodsTrackList(GoodsTrackQueryCriteria criteria){
List<GoodsTrackDTO> listGoodsTrack = goodsTrackMapper.listGoodsTrack(criteria);
Map<String, GoodsTrackDTO> goodsTrackDTOMap = new HashMap<String, GoodsTrackDTO>();
for (GoodsTrackDTO goodsTrackDTO : listGoodsTrack){
String goodsId = String.valueOf(goodsTrackDTO.getGoodsId());
if (!goodsTrackDTOMap.containsKey(goodsId)){
goodsTrackDTOMap.put(goodsId, goodsTrackDTO);
}else {
GoodsTrackDTO goodsTrack = goodsTrackDTOMap.get(goodsId);
int num = goodsTrack.getGoodsNum() + goodsTrackDTO.getGoodsNum();
goodsTrack.setGoodsNum(num);
}
}
List<GoodsTrackDTO> list = new ArrayList(goodsTrackDTOMap.values());
return list;
}
}
@Service
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true, rollbackFor = Exception.class)
public class GoodsOrderService extends BaseService<GoodsOrder, GoodsOrderDTO> {
@Autowired
private GoodsTrackService goodsTrackService;
@Override
public GoodsOrderDTO create(GoodsOrderDTO goodsOrderDTO) {
//...
List<GoodsTrackDTO> rs1 = goodsTrackList(criteria);
//...
List<GoodsTrackDTO> rs2 = listGoodsTrack(criteria);
//...
}
}复制代码
大体逻辑就是在 GoodsTrackService
定义了两个查询方法,一个是直接从数据库中获取数据,第二个是从数据库中获取数据后进行了一些加工(经过某个字段进行合并累加,相似sum group by),而后在GoodsOrderService
的同一个方法(该方法是一个事务方法 )中调用这两个查询,发现rs2中的数据存在问题, 指望是都应该与数据库表的数据一致,但其中部分数据却与查出后进行了修改的rs1中的一致。mybatis
初步看,listGoodsTrack
方法直接调用的mapper方法 goodsTrackMapper.listGoodsTrack(criteria)
没作任何应用层的处理,第一反应是缓存的缘由。 我问前面的查询有没有改变查询返回的结果(一开始没细看具体实现),答曰没有。折腾一阵后,返过去细看 goodsTrackList
的实现,果真仍是眼见为实、耳听为虚。在该方法中,经过goodsId对返回的列表进行分组,对goodsNum进行累加,最后返回累加后的几个对象。可是在累加的时候,是直接做用于返回结果对象的,明明就是改变了查询结果(竟然说没有?!!)。 这就是问题所在了,mybatis在同一个事务中,对同一个查询(一样的sql,一样的参数)的返回结果进行了缓存(称为一级缓存),下一次作一样的查询时,若是中间没有任何更新操做,则直接返回缓存的数据,而在本例中由于对缓存数据作了人为的修改,因此最后致使查出的数据与数据库不一致。app
简单介绍下mybatis的两级缓存机制分布式
一级缓存:一级缓存包括SqlSession与STATEMENT两种级别,默认在 SqlSession 中实现。在一次会话中,若是两次查询sql相同,参数相同,且中间没有任何更新操做,则第二次查询会直接返回第一次查询缓存的结果,再也不请求数据库。若是中间存在更新操做,则更新操做会清除掉缓存,后面的查询就会访问数据库了。STATEMENT级别则每次查询都会清掉一级缓存,每次查询都会进行数据库访问。ide
二级缓存:二级缓存则是在同一个namesapce的多个 SqlSession 间共享的缓存,默认未开启。当开启二级缓存后,数据查询的流程就是 二级缓存 ——> 一级缓存 ——> 数据库, 同一个namespace下的更新操做,会影响同一个Cache。工具
如何开启二级缓存
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>复制代码
<cache
eviction="LRU"
flushInterval="60000"
size="512"
readOnly="true"/> 复制代码
支持的属性:
也可使用 <cache-ref namespace="mapper.UserMapper"/>
来与另外一个mapper共享二级缓存
已经定位到是因为mybatis的一级缓存致使,那如何解决本文提到的问题呢? 基本上有三个解决方向。
既然要使用缓存,那就不能更改缓存的数据,此时咱们能够在须要更改数据的地方把数据作一次副本拷贝,使其不改变缓存数据自己, 如
for (GoodsTrackDTO goodsTrackDTO : listGoodsTrack){
String goodsId = String.valueOf(goodsTrackDTO.getGoodsId());
if (!goodsTrackDTOMap.containsKey(goodsId)){
goodsTrackDTOMap.put(goodsId, ObjectUtil.clone(goodsTrackDTO));
}else {
GoodsTrackDTO goodsTrack = goodsTrackDTOMap.get(goodsId);
int num = goodsTrack.getGoodsNum() + goodsTrackDTO.getGoodsNum();
goodsTrack.setGoodsNum(num);
}
}复制代码
使用ObjectUtil.clone()方法(hutool工具包中提供)对须要更改的数据作副本拷贝。
在xml的sql定义中添加 flushCache="true" 的配置,使该查询不使用缓存,以下
<select id="listGoodsTrack" resultType="xxx.GoodsTrackDTO" flushCache="true">
SELECT ...
</select>复制代码
禁用缓存的另外一种方案是将一级缓存直接设置为STATEMENT来进行全局禁用,在mybatis-config.xml中配置:
<settings>
<setting name="localCacheScope" value="STATEMENT"/>
</settings>复制代码
再定义一个实现相同查询的mapper方法,id不同来避开使用相同的缓存,这种作法就不怎么优雅了。
<select id="listGoodsTrack2" resultType="xxx.GoodsTrackDTO" flushCache="true">
SELECT ...
</select>复制代码
避开缓存的另外一种作法是不使用事务,使两个查询不在一个SqlSession中,但有时候事务是必须的,因此得分场景来。
另外因为mybatis的缓存都是基于本地的,在分布式环境下可能致使读取的数据与数据库不一致,好比一个服务实例两次读取中间,另外一个服务实例对数据进行了更新,则后一次读取因为缓存仍是读取的旧数据,而不是更新后的数据,可能致使问题。这时能够经过将缓存设置为STATEMENT级别来禁用mybatis缓存,经过Redis,MemCached等来提供分布式的全局缓存。
做者:空山新雨,一枚仍在学习路上的IT老兵 近期做者写了几十篇技术博客,内容包括Java、Spring Boot、Spring Cloud、Docker,技术管理心得等
欢迎关注做者微信公众号:空山新雨的技术空间,一块儿学习成长