zoukankan      html  css  js  c++  java
  • Mybatis缓存

    缓存

    缓存是一般ORM框架都有的功能,目的就是提高查询的效率和减少数据库的压力。

    缓存结构

    Mybatis源码中与缓存相关的类都在cache包中,其中有一个Cache接口,默认实现类PerpetualCache,他是由HashMap实现的,是基础缓存。

    Mybatis的缓存功能是采用装饰器模式实现的。

    装饰器模式:在不改变原对象的基础上,将功能附加到对象上,提供了比继承更有弹性的代替方案。

    缓存继承关系:


    mybatis缓存总体分为三大类:基本缓存、淘汰算法缓存、装饰器缓存

    一级缓存

    一级缓存也叫本地缓存,Mybatis的一级缓存实在会话层进行缓存的。Mybatis的一级缓存默认是开启的,不需要任何的配置。伪关闭方法提高缓存级别(localCacheScope设置为STATEMENT,只针对statement有效)

    BaseExecutor的query()

    if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
    }
    

    mybatis执行的流程里面,缓存对象PerpetualCache是哪个对象维护的呢?

    Mybatis一级缓存是与SqlSession共存亡的,所以就不需要为SqlSession编号、再根据SqlSession的编号去查询对应的缓存了。

    DefaultSqlSession里面有两个对象属性: Configuration和Executor

    其中Configuration是全局的,不属于SqlSession,所以缓存维护在Executor里面--实际上他维护在基本执行器SimpleExecutor/ReuseExecutor/BatchExecutor的父类BaseExecutor的构造函数中持有PrepetualCache。

    protected BaseExecutor(Configuration configuration, Transaction transaction) {
        this.transaction = transaction;
        this.deferredLoads = new ConcurrentLinkedQueue<>();
        this.localCache = new PerpetualCache("LocalCache");
        this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
        this.closed = false;
        this.configuration = configuration;
        this.wrapper = this;
    }
    

    同一个会话里面,多次执行相同的SQL语句,会直接从内存取到缓存的结果,不会再去查询数据库。但不同的会话里面,执行相同的SQL,也会去查询数据库语句,不走一级缓存。

    一级缓存验证

    首先关闭二级缓存,localCacheScope设置为SESSION。

     <!-- 控制全局缓存(二级缓存),默认 true-->
    <setting name="cacheEnabled" value="false"/>
    <setting name="localCacheScope" value="SESSION"/>
    

    1.在同一个session中共享

    UserMapper mapper = session.getMapper(userMapper.class);
    System.out.println(mapper.selectOne(1));
    System.out.println(mapper.selectOne(1));
    

    2.不同session中不能共享

    SqlSession session = sqlSessionFactory.openSession();
    UserMapper mapper = session.getMapper(userMapper.class);
    System.out.println(mapper.selectOne(1));
    

    一级缓存在BaseExecutor的query()--queryFromDatabase()中存入。在queryFromDatabase之前会get()。

    //从缓存中获取数据(key是CacheKey)
    //一级缓存和二级缓存的CacheKey是同一个
    list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
    if (list != null) {
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
    } else {
        // 真正的查询流程
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
    }
    
    private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, 
    ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        List<E> list;
        // 先占位
        localCache.putObject(key, EXECUTION_PLACEHOLDER);
        try {
            // 默认Simple
            list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
        } finally {
            // 移除占位符
            localCache.removeObject(key);
        }
        // 写入一级缓存
        localCache.putObject(key, list);
        if (ms.getStatementType() == StatementType.CALLABLE) {
            localOutputParameterCache.putObject(key, parameter);
        }
        return list;
    }
    

    3.同一个会话中,update(包括delete)会导致一级缓存清空

    public int update(MappedStatement ms, Object parameter) throws SQLException {
        ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
        if (closed) {
            throw new ExecutorException("Executor was closed.");
        }
        //先执行缓存清空操作
        clearLocalCache();
        return doUpdate(ms, parameter);
    }
    
    

    QA:只有更新才会清空缓存吗?查询会清空缓存吗?怎么清空?

    一级缓存是在BaseExecutor中的update()方法中调用clearLocalCache()清空的,如果是query只有select标签的flushCache=true才清空。
    

    一级缓存的工作范围是一个会话。如果跨回话,出现什么问题?

    4.其他会话更新会导致当前会话读到的数据是过时的数据(不能跨会话共享)

    //会话2更新数据
    UserMapper mapper2 = session.getMapper(UserMapper.class);
    mapper.updateById(user);
    session.commit();
    //会话1读取到过时的数据,一级缓存不能夸会话共享
    System.out.println(mapper1.selectOne(1));
    
    不足

    使用一级缓存的时候,因为缓存不能跨会话共享,不同的会话之间对于相同的数据可能有不一样的缓存。在有多个会话或者分布式环境下,会存在查询到过时数据的问题。如果要解决这个问题,需要开启二级缓存。

    二级缓存

    二级缓存是用来解决一级缓存不能跨会话共享的问题。

    QA:如果开启了二级缓存,是在一级缓存前面还是后面执行呢?怎么维护的?

    
    作为一个作用范围更广的缓存,可定在SqlSession的外层,不然做不到SqlSession共享。
    
    而一级缓存是在SqlSession内部的,所以是在一级缓存前面执行,只有二级缓存找不到才会去一级缓存找。
    
    那么二级缓存在哪里维护的呢? 跨会话共享的话,SqlSession本身和它里面的BaseExecutor已经满足不了需求了,所以应该在BaseExecutor之外创建。
    但只有二级缓存开启后才能加载这个对象。
    
    实际上Mybatis使用了一个装饰器类(CachingExecutor)来维护。
    
    如果启用了二级缓存。Mybatis在创建Executor对象的时候会对Executor进行装饰。
    
    CachingExecutor对于查询请求,会判断二级缓存是否有缓存结果,如果有就直接返回,没有的话就交给真正的查询器Executor实现类,比如SimpleExecutor来执行查询,
    再走到一级缓存。最后把结果缓存起来,返回给用户。
    

    二级缓存开启方式

    1.在mybatis-config.xml中配置了(默认true)

    <setting name="cacheEnable" value="true"/>
    

    只要开启了二级缓存,都会使用CachingExecutor装饰基本的执行器(SIMPLE、REUSE、BATCH)

    二级缓存默认是开启的。但是每个Mapper的二级缓存开关是默认关闭的。一个Mapper要使用二级缓存,还要单独配置。

    2.在Mapper.xml配置标签:

    <cache type="org.apache.ibatis.cache.impl.PerpetualCache"
           size="1024" <!--最大缓存个数,默认1024-->
           eviction="LRU" <!--缓存策略-->
           flushInterval="120000" <!-- 自动刷新时间,未配置时只有调用时刷新 -->
           readOnly="false"/> <!-- 默认false,改为true可读可写,对象必须支持序列化 -->
    

    cache属性详解:

    Mapper.xml配置了之后。select()会被存储。update()、delete()、insert()会刷新缓存。

    QA:如果cacheEnable=true,Mapper.xml没有配置标签,还会走二级缓存吗?还会使用CachingExecutor包装对象吗?

    只要cacheEnable=true基本执行器就会被装饰。有没有配置<cache>,决定了在启用的时候能不能创建mapper这个Cache对象,最终会影响到CachingExecutor query方法里面的判断。
    也就是说,此时会被装饰,但没有cache对象,依然不会走二级缓存。
    

    QA:如果一个Mapper需要开启二级缓存,但是这里面的某些查询方法对数据实时性要求很高,不需要二级缓存,怎么办?

    可以在单个Statement ID上显示关闭二级缓存(默认是true)

    <select id="selectUser" resultMap="BaseResultMap" useCache="false">
    

    CachingExecutor query方法的判断:

    // cache 对象是在哪里创建的?  XMLMapperBuilder类 xmlconfigurationElement()
    // 由 <cache> 标签决定
    if (cache != null) {
        // flushCache="true" 清空一级二级缓存 >>
        flushCacheIfRequired(ms);
        if (ms.isUseCache() && resultHandler == null) {
            ensureNoOutParams(ms, boundSql);
            // 获取二级缓存
            // 缓存通过 TransactionalCacheManager、TransactionalCache 管理
            @SuppressWarnings("unchecked")
            List<E> list = (List<E>) tcm.getObject(cache, key);
            if (list == null) {
                list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                // 写入二级缓存
                tcm.putObject(cache, key, list); // issue #578 and #116
            }
            return list;
        }
    }
    
    二级缓存验证
    UserMapper mapper = session.getMapper(userMapper.class);
    System.out.println(mapper.selectOne(1));
    //事务不提交的情况下,二级缓存不会写入
    session.commit();
    UserMapper mapper2 = session2.getMapper(userMapper.class);
    System.out.println(mapper2.selectOne(1));
    

    QA:为什么事务不提交,二级缓存不生效?

        因为二级缓存使用TransactionalCacheManager(TCM)来管理,最后又调用了TransactionalCache的getObject()、putObject和commit方法,TransactionCache
    里面又持有真正的Cache对象,列入被层层装饰的PerpetualCache对象。
        在putObject的时候,只是添加到了entriesToAddOnCommit里面,只有它的commit()方法被调用的时候才会调用flushPendingEntries()真正写入缓存。
    他就是在DefaultSqlSession调用commit()的时候被调用的。
    
    ~~~java
    public void commit() {
        if (clearOnCommit) {
            delegate.clear();
        }
        // 真正写入二级缓存
        flushPendingEntries();
        reset();
    }
    
    private void flushPendingEntries() {
        for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
            delegate.putObject(entry.getKey(), entry.getValue());
        }
        for (Object entry : entriesMissedInCache) {
            if (!entriesToAddOnCommit.containsKey(entry)) {
                delegate.putObject(entry, null);
            }
        }
    }
    
    @Override
    public void putObject(Object key, Object object) {
        entriesToAddOnCommit.put(key, object);
    }
    

    QA:为什么增删改会清空缓存?

    在CachingExecutor的update()方法里面会调用flushCacheIfRequired(ms),isFlushCacheRequired就是从标签里面渠道的flushCache的值。而增删改操作的flush属性默认为true.

    @Override
    public int update(MappedStatement ms, Object parameterObject) throws SQLException {
        flushCacheIfRequired(ms);
        return delegate.update(ms, parameterObject);
    }
    
    private void flushCacheIfRequired(MappedStatement ms) {
        Cache cache = ms.getCache();
        // 增删改查的标签上有属性:flushCache="true" (select语句默认是false)
        // 一级二级缓存都会被清理
        if (cache != null && ms.isFlushCacheRequired()) {
            tcm.clear(cache);
        }
    }
    

    也就是说,如果不需要清空二级缓存,可以把flushCache属性修改成false(这样会造成过时数据的问题)。

    二级缓存的使用场景

    1、因为所有的增删改都会刷新二级缓存,导致二级缓存失效,所以适合在查询为主的应用中使用,比如交易历史、历史订单的查询。

    2、如果多个namespace中有针对性同一个表的操作,比如user表,如果在一个namespace中刷新了缓存,另一个namespace中没有刷新,就会出现读到脏数据的情况。

    所以推荐在一个Mapper里面只操作单表的情况使用。

    QA:怎么让多个namespace共享一个二级缓存?

    跨namespace的缓存共享的问题,可以使用来解决:

    <cache-ref namespace="com.xxx.xxx.dao.UserMapper"/>
    

    cache-ref代表引用到别的命名空间的Cache配置,两个命名空间的操作使用是同一个Cache。在关联的表比较少,或者按照业务可以进行表进行分组的时候可以使用。

    Ps:这种情况下,多个Mapper的操作都会引起缓存刷新,缓存的意义已经不大了。

    使用第三方作为二级缓存

    除了Mybatis自带的二级缓存之外,我们也可以实现Cache接口自定义二级缓存。

    例如集成redis做二级缓存:

    https://github.com/mybatis/redis-cache

    pom.xml文件依赖:

    <dependency>
        <groupId>org.mybatis.caches</groupId>
        <artifactId>mybatis-redis</artifactId>
        <version>1.0.0-beta2</version>
    </dependency>
    

    Mapper.xml配置,type使用RedisCache:

    <cache type="org.mybatis.caches.redis.RedisCache"
           eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
    

    redis.properties配置:

    host=localhost
    port=6379
    connectionTimeout=5000
    soTimeout=5000
    database=0
    

    Redis作为二级缓存的验证(需要安装Redis客户端):RedisManager

    当然在分布式环境中也可以单独的使用缓存服务,不使用Mybatis自带的二级缓存。

  • 相关阅读:
    智慧城市顶层设计策略方案(PPT)
    ant build.xml 解释!
    Excel poi API基础教程!
    操纵Excel文件的 ExcelUtil 类 !
    在selenium测试中使用XPATH功能函数starts-with、contains、descendant、ancestor、text()定位网页元素
    [ Selenium2 从零开始 by Bruce from http://seleniumcn.cn ] 1-8 视频集锦
    selenium 概念及练习 !
    selenium Object Page 设计模式理解及实现!
    使用TestNG 和 CSV文件进行数据驱动
    如何让评审人爱上我
  • 原文地址:https://www.cnblogs.com/snail-gao/p/13162336.html
Copyright © 2011-2022 走看看