zoukankan      html  css  js  c++  java
  • mybatis缓存,从一个“灵异”事件说起

    刚准备下班走人,被一开发同事叫住,让帮看一个比较奇怪的问题:Mybatis同一个Mapper接口的查询方法,第一次返回与第二次返回结果不一样,百思不得其解!

    问题

    Talk is cheap. Show me the code. 该问题涉及的主要代码实现包括

    1.mapper接口定义

    public interface GoodsTrackMapper extends BaseMapper<GoodsTrack> {
        List<GoodsTrackDTO> listGoodsTrack(@Param("criteria") GoodsTrackQueryCriteria criteria);
    }
    

    2.xml定义

    <select id="listGoodsTrack" resultType="xxx.GoodsTrackDTO">
        SELECT ...
    </select>
    

    3.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中的一致。

    定位

    初步看,listGoodsTrack 方法直接调用的mapper方法 goodsTrackMapper.listGoodsTrack(criteria) 没做任何应用层的处理,第一反应是缓存的原因。 我问前面的查询有没有改变查询返回的结果(一开始没细看具体实现),答曰没有。折腾一阵后,返过去细看 goodsTrackList 的实现,果然还是眼见为实、耳听为虚。在该方法中,通过goodsId对返回的列表进行分组,对goodsNum进行累加,最后返回累加后的几个对象。但是在累加的时候,是直接作用于返回结果对象的,明明就是改变了查询结果(居然说没有?!!)。 这就是问题所在了,mybatis在同一个事务中,对同一个查询(同样的sql,同样的参数)的返回结果进行了缓存(称为一级缓存),下一次做同样的查询时,如果中间没有任何更新操作,则直接返回缓存的数据,而在本例中因为对缓存数据做了人为的修改,所以最后导致查出的数据与数据库不一致。

    mybatis缓存机制

    简单介绍下mybatis的两级缓存机制

    • 一级缓存:一级缓存包括SqlSession与STATEMENT两种级别,默认在 SqlSession 中实现。在一次会话中,如果两次查询sql相同,参数相同,且中间没有任何更新操作,则第二次查询会直接返回第一次查询缓存的结果,不再请求数据库。如果中间存在更新操作,则更新操作会清除掉缓存,后面的查询就会访问数据库了。STATEMENT级别则每次查询都会清掉一级缓存,每次查询都会进行数据库访问。

    • 二级缓存:二级缓存则是在同一个namesapce的多个 SqlSession 间共享的缓存,默认未开启。当开启二级缓存后,数据查询的流程就是 二级缓存 ——> 一级缓存 ——> 数据库, 同一个namespace下的更新操作,会影响同一个Cache。

    如何开启二级缓存

    1.需要在mybatis-config.xml中设置:

    <settings>
        <setting name="cacheEnabled" value="true"/>
    </settings>
    

    2.然后在mapper的xml文件的<mapper>下设置cache相关配置:

    <cache 
    	eviction="LRU"  
    	flushInterval="60000" 
    	size="512" 
    	readOnly="true"/> 
    

    支持的属性:

    • type:cache使用的类型,默认是PerpetualCache
    • eviction: 回收的策略,常见的有LRU,FIFO
    • flushInterval: 配置一定时间自动刷新缓存,单位毫秒
    • size: 最多缓存的对象个数
    • readOnly: 是否只读,若配置为可读写,则需要对应的实体类实现Serializable接口
    • blocking: 如果缓存中找不到对应的key,是否会一直blocking,直到有对应的数据进入缓存

    也可以使用 <cache-ref namespace="mapper.UserMapper"/> 来与另一个mapper共享二级缓存

    解决

    已经定位到是由于mybatis的一级缓存导致,那如何解决本文提到的问题呢? 基本上有三个解决方向。

    1.使用缓存的方案

    既然要使用缓存,那就不能更改缓存的数据,此时我们可以在需要更改数据的地方把数据做一次副本拷贝,使其不改变缓存数据本身, 如

    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工具包中提供)对需要更改的数据做副本拷贝。

    2.禁用缓存的方案

    在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>
    

    3.避开缓存的方案

    再定义一个实现相同查询的mapper方法,id不一样来避开使用相同的缓存,这种做法就不怎么优雅了。

    <select id="listGoodsTrack2" resultType="xxx.GoodsTrackDTO" flushCache="true"> 
        SELECT ...
    </select>
    

    避开缓存的另一种做法是不使用事务,使两个查询不在一个SqlSession中,但有时候事务是必须的,所以得分场景来。

    另外由于mybatis的缓存都是基于本地的,在分布式环境下可能导致读取的数据与数据库不一致,比如一个服务实例两次读取中间,另一个服务实例对数据进行了更新,则后一次读取由于缓存还是读取的旧数据,而不是更新后的数据,可能导致问题。这时可以通过将缓存设置为STATEMENT级别来禁用mybatis缓存,通过Redis,MemCached等来提供分布式的全局缓存。

    认真生活,快乐分享
    欢迎关注微信公众号:空山新雨的技术空间
    公众号二维码
    获取更多关于Spring Boot,Spring Cloud, Docker等企业实战技术

  • 相关阅读:
    你知道怎么离线安装全局 node 模块吗?
    关于开发视图
    你知道 JavaScript 中的 Arguments 对象都有哪些用途吗?
    前端 JavaScript 实现一个简易计算器
    Docker 系列 _ 01_ 一念缘起
    机器学习资料大全
    pdf、txt文本复制到EXCEL后的格式快速调整法
    习惯的养成
    磁盘剩余空间监控
    please wait while windows configures microsoft visual studio professional 2013
  • 原文地址:https://www.cnblogs.com/spec-dog/p/12302438.html
Copyright © 2011-2022 走看看