美文网首页
带你了解MyBatis一二级缓存

带你了解MyBatis一二级缓存

作者: 听风逝夜h | 来源:发表于2020-01-28 18:45 被阅读0次

    在对数据库进行噼里啪啦的查询时,可能存在多次使用相同的SQL语句去查询数据库,并且结果可能还一样,这时,如果不采取一些措施,每次都从数据库查询,会造成一定资源的浪费,所以Mybatis中提供了一级缓存和二级缓存来解决这个问题,通过把第一次查询的结果保存在内存中,如果下次有同样的语句,则直接从内存中返回。

    一级缓存

    一级缓存的作用域在同一个SqlSession,也就是说两个一样的SQL语句,第一次会从数据库中获得,并保存在一个Map<Object, Object> 中,第二次会从这个Map中返回,Mybatis默认开启了一级缓存。

    下面是代码演示

        public static void main( String[] args )
        {
            String resource = "mybatis-config.xml";
            try {
                InputStream inputStream = Resources.getResourceAsStream(resource);
                SqlSessionFactory build = new SqlSessionFactoryBuilder().build(inputStream);
    
                SqlSession sqlSession = build.openSession();
                System.out.println(sqlSession.getMapper(IUserDao.class).select());
                System.out.println(sqlSession.getMapper(IUserDao.class).select());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    

    在配置文件中加入日志实现类来打印执行日志

        <settings>
            <setting name="logImpl" value="STDOUT_LOGGING" />
        </settings>
    

    通过日志可以发现,在第一次查询时构造了sql语句,从数据库查询,第二次并没有构造sql,直接返回了缓存的数据。

    Opening JDBC Connection
    Created connection 1928931046.
    Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@72f926e6]
    ==>  Preparing: select * from tb_user 
    ==> Parameters: 
    <==    Columns: user_tel, user_name, user_passwd
    <==        Row: 1500, 123, 111
    <==        Row: 1501, 123, 222
    <==      Total: 2
    [UserEntity{userTel='1500', userName='123', userPass='111'}, UserEntity{userTel='1501', userName='123', userPass='222'}]
    [UserEntity{userTel='1500', userName='123', userPass='111'}, UserEntity{userTel='1501', userName='123', userPass='222'}]
    
    

    一级缓存源码分析

    在BaseExecutor中有一个PerpetualCache类,而PerpetualCache很简单,其中Map<Object, Object>则是用来保存缓存


    在这里插入图片描述
    在这里插入图片描述

    那就先可以假设,保存缓存时一定会调用其putObject方法,而取出缓存时一定调用getObject方法,通过debug可以看到,在第一次查询完成之后,会调用putObject方法,key为sql语句,value是结果.


    在这里插入图片描述
    调用putObject方法在BaseExecutor下的queryFromDatabase中
        private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
            //先占位?
            this.localCache.putObject(key, ExecutionPlaceholder.EXECUTION_PLACEHOLDER);
    
            List list;
            try {
                //从数据库查询
                list = this.doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
            } finally {
                this.localCache.removeObject(key);
            }
            //吧结果保存在PerpetualCache中
            this.localCache.putObject(key, list);
            if (ms.getStatementType() == StatementType.CALLABLE) {
                this.localOutputParameterCache.putObject(key, parameter);
            }
    
            return list;
        }
    

    而调用queryFromDatabase在query中,首先判断缓存中有没有数据,没有则调用queryFromDatabase从数据库查找,有就直接返回

             List list;
                try {
                    ++this.queryStack;
                    //从缓存中获取
                    list = resultHandler == null ? (List)this.localCache.getObject(key) : null;
                    if (list != null) {
                        this.handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
                    } else {
                    //从数据库获取
                        list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
                    }
                } finally {
                    --this.queryStack;
                }
    

    手动清除缓存

    SqlSession提供了clearCache来清除缓存,当第一次查询玩之后,手动清除缓存,就会发现第二次也是从数据库中查找,原理也就是对PerpetualCache进行了clear();

        public static void main( String[] args )
        {
            String resource = "mybatis-config.xml";
            try {
                InputStream inputStream = Resources.getResourceAsStream(resource);
                SqlSessionFactory build = new SqlSessionFactoryBuilder().build(inputStream);
    
                SqlSession sqlSession = build.openSession();
                System.out.println(sqlSession.getMapper(IUserDao.class).select());
                sqlSession.clearCache();
                System.out.println(sqlSession.getMapper(IUserDao.class).select());
            } catch (IOException e) {
                e.printStackTrace();
            }
    
        }
    
    Opening JDBC Connection
    Created connection 1928931046.
    Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@72f926e6]
    ==>  Preparing: select * from tb_user 
    ==> Parameters: 
    <==    Columns: user_tel, user_name, user_passwd
    <==        Row: 1500, 123, 111
    <==        Row: 1501, 123, 222
    <==      Total: 2
    [UserEntity{userTel='1500', userName='123', userPass='111'}, UserEntity{userTel='1501', userName='123', userPass='222'}]
    ==>  Preparing: select * from tb_user 
    ==> Parameters: 
    <==    Columns: user_tel, user_name, user_passwd
    <==        Row: 1500, 123, 111
    <==        Row: 1501, 123, 222
    <==      Total: 2
    [UserEntity{userTel='1500', userName='123', userPass='111'}, UserEntity{userTel='1501', userName='123', userPass='222'}]
    

    并且Mybatis会在Insert、Update后都会自动删除缓存

    但是会有一个数据不一致的问题,很多人称之为坑,举个数据不一致的例子,两个SqlSession,都保存了id为1500的数据,但是当SqlSession1更新了1500的密码后,SqlSession2在获取并没有从数据库获取,而是从缓存中获取,缓存中的是旧密码,就出现缓存不一直问题

    public static void main( String[] args )
        {
            String resource = "mybatis-config.xml";
            try {
                InputStream inputStream = Resources.getResourceAsStream(resource);
                SqlSessionFactory build = new SqlSessionFactoryBuilder().build(inputStream);
    
    
                SqlSession sqlSession1 = build.openSession(true);
                SqlSession sqlSession2 = build.openSession(true);
    
                sqlSession1.getMapper(IUserDao.class).selectByTel("1500");
                sqlSession2.getMapper(IUserDao.class).selectByTel("1500");
    
                System.out.println("1500的密码---"+sqlSession2.getMapper(IUserDao.class).selectByTel("1500").getUserPass());
    
                sqlSession1.getMapper(IUserDao.class).upUserPass("1500","456");
    
                System.out.println("----"+sqlSession2.getMapper(IUserDao.class).selectByTel("1500"));
    
    
            } catch (IOException e) {
                e.printStackTrace();
            }
    
        }
    
    Cache Hit Ratio [com.hxl.dao.IUserDao]: 0.0
    Opening JDBC Connection
    Created connection 1780034814.
    ==>  Preparing: select * from tb_user where user_tel=? 
    ==> Parameters: 1500(String)
    <==    Columns: user_tel, user_name, user_passwd
    <==        Row: 1500, 123, 123
    <==      Total: 1
    Cache Hit Ratio [com.hxl.dao.IUserDao]: 0.0
    Opening JDBC Connection
    Created connection 918312414.
    ==>  Preparing: select * from tb_user where user_tel=? 
    ==> Parameters: 1500(String)
    <==    Columns: user_tel, user_name, user_passwd
    <==        Row: 1500, 123, 123
    <==      Total: 1
    Cache Hit Ratio [com.hxl.dao.IUserDao]: 0.0
    1500的密码---123
    ==>  Preparing: UPDATE tb_user set user_passwd=? where user_tel=? 
    ==> Parameters: 456(String), 1500(String)
    <==    Updates: 1
    Cache Hit Ratio [com.hxl.dao.IUserDao]: 0.0
    ----UserEntity{userTel='1500', userName='123', userPass='123'}
    
    

    解决办法在配置文件中加入<setting name="localCacheScope" value="STATEMENT"/>,原理是在BaseExecutor#query中判断了LocalCacheScope是不是等于STATEMENT,如果等于,则清除缓存,这就意味着每次查询完都清除缓存,间接的表示关闭了一级缓存

    if (this.configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        this.clearLocalCache();
    }
    

    二级缓存

    二级缓存作用于是Mapper中的namespace,脱离了SqlSession,二级缓存的总开个默认也是开启的,也就是很多人说在配置文件中加入<setting name="cacheEnabled" value="true"/>,脱裤子放屁,简直多此一举

    在这里插入图片描述
    既然有主开关了,那就还有个次开关,其实还有个次次开关,次开关就是在mapper文件中加入<cache></cache>,即表示开启二级缓存
    在这里插入图片描述
    下面代码先演示
      public static void main( String[] args )
        {
            String resource = "mybatis-config.xml";
            try {
                InputStream inputStream = Resources.getResourceAsStream(resource);
                SqlSessionFactory build = new SqlSessionFactoryBuilder().build(inputStream);
    
    
                SqlSession sqlSession1 = build.openSession();
    
                SqlSession sqlSession2 = build.openSession();
                System.out.println(sqlSession1.getMapper(IUserDao.class).selectByTel("1500"));
                //提交
                sqlSession1.commit();
    
                System.out.println(sqlSession2.getMapper(IUserDao.class).selectByTel("1500"));
    
    
            } catch (IOException e) {
                e.printStackTrace();
            }
    
        }
    
    Cache Hit Ratio [com.hxl.dao.IUserDao]: 0.0
    Opening JDBC Connection
    Created connection 1780034814.
    Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@6a192cfe]
    ==>  Preparing: select * from tb_user where user_tel=? 
    ==> Parameters: 1500(String)
    <==    Columns: user_tel, user_name, user_passwd
    <==        Row: 1500, 123, 789
    <==      Total: 1
    UserEntity{userTel='1500', userName='123', userPass='789'}
    Cache Hit Ratio [com.hxl.dao.IUserDao]: 0.5
    UserEntity{userTel='1500', userName='123', userPass='789'}
    
    Process finished with exit code 0
    
    

    当第一次查询时,命中率为0.0(Cache Hit Ratio [com.hxl.dao.IUserDao]: 0.0),第二次为0.5(Cache Hit Ratio [com.hxl.dao.IUserDao]: 0.5),所以从缓存中获取,但是如果吧其中 sqlSession1.commit();去掉,就发现两次都从数据库查找,命中率都为0.0,原因是二级缓存有一个TransactionalCacheManager来管理二级缓存,只有调用其commit()方法才会真正保存下来,在调用SqlSession的close或者commit方法时都会调用到CachingExecutor下的close和commit方法,这两个方法对缓存进行真正的提交。

    在这里插入图片描述
    在这里插入图片描述

    二级缓存源码分析

    在Configuration的newExecutor中判断了总开关是否打开,如果打开,则使用CachingExecutor

        public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
            executorType = executorType == null ? this.defaultExecutorType : executorType;
            executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
            Object executor;
            if (ExecutorType.BATCH == executorType) {
                executor = new BatchExecutor(this, transaction);
            } else if (ExecutorType.REUSE == executorType) {
                executor = new ReuseExecutor(this, transaction);
            } else {
                executor = new SimpleExecutor(this, transaction);
            }
            //判断总开关是否打开
            if (this.cacheEnabled) {
                executor = new CachingExecutor((Executor)executor);
            }
    
            Executor executor = (Executor)this.interceptorChain.pluginAll(executor);
            return executor;
        }
    

    查询时最终调用到在CachingExecutor的query方法,首先从MappedStatement中获取Cache,如果在Mapper文件中没有配置Cache标签,它会返回空,直接从数据库查询,如果配置了,还要判断标签上的useCache属性是不是为true,也就是刚才说的次次开关,可以增加useCache=“false”来关闭这个select的缓存。

    然后从缓存事物管理器中获取缓存,没有的话从数据库查询,并且添加结果到缓存事物管理器,有就返回。


    在这里插入图片描述
        public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
            Cache cache = ms.getCache();
            if (cache != null) {
                //判断是否刷新缓存
                this.flushCacheIfRequired(ms);
                //标签上是的useCache属性是否为true
                if (ms.isUseCache() && resultHandler == null) {
                    this.ensureNoOutParams(ms, boundSql);
                    //从缓存中获取
                    List<E> list = (List)this.tcm.getObject(cache, key);
                    if (list == null) {
                        list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                        //添加到缓存
                        this.tcm.putObject(cache, key, list);
                    }
    
                    return list;
                }
            }
            
            return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
        }
    

    cache配置

    cache有几个属性值

    • 一 eviction:缓存回收策略,默认是LRU
    1. LRU(Least Recently Used),最近最少使用的,最长时间不用的对象

    2. FIFO(First In First Out),先进先出,按对象进入缓存的顺序来移除他们

    3. SOFT,软引用,移除基于垃圾回收器状态和软引用规则的对象

    4. WEAK,弱引用,更积极的移除基于垃圾收集器状态和弱引用规则的对象

    • flushInterval:刷新间隔,单位为毫秒
    • size:缓存中的个数
    • readOnly:是否只读
    • type:自定义缓存
    • blocking:是否阻塞

    flushCache

    在insert、update、delete下,flushCache是默认为true的,在执行后,会刷新缓存,而select为false,查询不刷新缓存,可以更具自己需求改过来

    注意

    其实在集群环境下,一级缓存和二级缓存显的有点无能为力,比如A查询了张三的数据保存在缓存中,此时如果还是A修改了张三的数据,那么可以做到刷新缓存,但是重点是B修改了张三的信息,而A此时无法知道张三信息已经改变,出现数据不一致情况,解决办法通过Redis等进行缓存,

    相关文章

      网友评论

          本文标题:带你了解MyBatis一二级缓存

          本文链接:https://www.haomeiwen.com/subject/pidmthtx.html