MyBatis二级缓存

Myabtis包含一级缓存及二级缓存,一级缓存是基于SqlSession的(SqlSession间隔离)而二级缓存是基于Mapper的namespace的(如果两个mapper文件的namespace一致,那么将共享二级缓存);

本文主要讨论

1.验证Myabtis二级缓存的存在

2.二级缓存的开启方式

3.userCache及flushCache

4.debug查看Mybatis源码了解Mybatis二级缓存底层数据结构(HashMap)及工作模式

5.分析TransactionalCache类,探讨二级缓存生效、失效时机

本文只涉及二级缓存,更多了解一级缓存,请移步Mybatis一级缓存 

1. 验证二级缓存存在

A.Mybatis二级缓存默认不开启,在不设置开启二级缓存下执行如下测试代码及控制台打印结果可见,两次查询都走了SQL查询

    /**
     * 测试二级缓存是否存在
     */
    @Test
    public void testLevelTwoExist(){
        SqlSessionFactory sessionFactory = sqlSessionFactoryBuilder.build(resourceAsStream);
        SqlSession sqlSession1 = sessionFactory.openSession();
        SqlSession sqlSession2 = sessionFactory.openSession();
        SqlSession sqlSession3 = sessionFactory.openSession();
        UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
        UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
        UserMapper userMapper3 = sqlSession3.getMapper(UserMapper.class);
        System.out.println("--------------执行SqlSession1创建的UserMapper1对象的查询---------------------");
        User user1 = userMapper1.findById(1);
        sqlSession1.close();
        System.out.println("--------------执行SqlSession2创建的UserMapper2对象的查询---------------------");
        User user2 = userMapper2.findById(1);
        System.out.println("两次查询出来的对象 user1 user2 是否相等 : " + (user1 == user2));
        sqlSession1.close();
    }

B.开启二级缓存 并 验证

B1.在SqlMapConfig.xml中开启二级缓存设置

<!--    开启二级缓存   步骤1-->
    <settings>
        <setting name="cacheEnabled" value="true"/>
    </settings>

PS:注意Settings标签的位置,此文件需要严格根据标签类型按照指定顺序配置(properties?,settings?,typeAliases?,typeHandlers?,objectFactory?,objectWrapperFactory?,reflectorFactory?,plugins?,environments?,databaseIdProvider?,mappers?)".

B2.需要开启二级缓存的Mapper.xml文件中开启缓存 (以下三种写法均可,因为标签中默认的type值即为 org.apache.ibatis.cache.impl.PerpetualCache)

PS: 如果是注解开发,可在对应的Mapper接口类名上增加 @CacheNamespace 注解

<cache />
<cache > < /cache>
<cache type="org.apache.ibatis.cache.impl.PerpetualCache"></cache>

B3.对应的实体类需实现Serializable接口,如果没有实现,会报以下异常

org.apache.ibatis.cache.CacheException: Error serializing object.  Cause: java.io.NotSerializableException: com.kay.pojo.User

 原因:MyBatis二级缓存,存放的value是将对象的进行序列话后存放的,具体见本贴后面章节

B4.再次执行A步骤中SQL语句结果如下

分析:第一次查询走的SQL,第二次没有走SQL从二级缓存中读取的

Cache Hit Ratio [com.kay.annotation.dao.UserAnnotationMapper]: 0.5 (命中率 两次查询一次从缓存中获取到了数据,一次没有获取到,命中率为1/2)===》》 间接说明,Mybatis二级缓存同一级缓存一样,查询时从缓存中获取,缓存中没有才会走数据库查询

为什么User1与User2不是同一个对象?

因为二级缓存中存取的值不是对象,而是对象对应的值(区别于一级缓存中存放的是对象)

注意:

在两次查询中间,如果第一次查询的sqlSession没有执行sqlSession1.close()或者执行sqlSession1.commit;方法,第二次查询时,将不走缓存而仍然会走SQL查询(见源码分析部分)

2. 验证二级缓存被删除

A.在2执行代码的两次查询中间,增加同mapper下的SqlSession 的增删改操作,第二次查询将走SQL数据库查询  ====》》》 同一namespace下,出现增删改操作,二级缓存就会被清除(防止数据脏读)

 /**
     * 测试二级缓存被删除
     */
    @Test
    public void testLevelTwoInvalid(){
        SqlSessionFactory sessionFactory = sqlSessionFactoryBuilder.build(resourceAsStream);
        SqlSession sqlSession1 = sessionFactory.openSession();
        SqlSession sqlSession2 = sessionFactory.openSession();
        SqlSession sqlSession3 = sessionFactory.openSession(true);
        UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
        UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
        UserMapper userMapper3 = sqlSession3.getMapper(UserMapper.class);
        System.out.println("--------------执行SqlSession1创建的UserMapper1对象的查询---------------------");
        User user1 = userMapper1.findById(1);
        sqlSession1.close(); // 查询后关闭sqlSession1
        System.out.println("--------------执行SqlSession3创建的UserMapper3对象的增删改操作---------------------");
        User user = new User();
        user.setId(4);
        user.setUsername("sunshineKay");
        userMapper3.insertOne(user);// 插入后关闭sqlSession1
        sqlSession3.close();
        System.out.println("--------------执行SqlSession2创建的UserMapper2对象的查询---------------------");
        User user2 = userMapper2.findById(1);
        System.out.println("两次查询出来的对象 user1 user2 是否相等 : " + (user1 == user2));
        sqlSession1.close();
    }

注意:

同上一步骤一样,sqlSeesion在执行完查询或者新增等操作,都需要sqlSession.close才会生效

3. userCache及flushCache

A.userCache用于设置一个标签为的statement是否开启二级缓存(即:具体某个查询方法是否开启二级缓存) 默认为true 

当修改UserMapper.xml文件中statement中id findById 对应userCache为false

<!--    禁止 namespace="com.kay.dao.UserMapper"下 id为findById 使用二级缓存-->
    <select id="findById" parameterType="int" resultType="com.kay.pojo.User" useCache="true">
        select * from user where id = #{id}
    </select>

或者注解方式可在Mapper接口类对应的方法中(@Options(useCache=true/false))

    // findOneById 禁止使用二级缓存,每次查询将直接执行SQL从数据查找
    @Options(useCache = false)
    @Select("select * from user where id = #{id}")
    User findOneById(Integer id);

执行验证二级缓存存在的代码,第二次查询时,将不走缓存

B.flushCache用于设置是否刷新缓存,默认为true,当增删改操作对应的statement 配置了flushCache=false,将不再刷新缓存,可能会出现脏读

B1.修改UserMapper.xml文件中statement中id为 updateUsername的flushCache=false

<!--    设置updateUsername执行后不刷新缓存flushCache="false"-->
    <update id="updateUsername" parameterType="com.kay.pojo.User" flushCache="false">
        update user set username = #{username} where id = #{id}
    </update>

或者 注解方式可在Mapper接口类对应的方法上配置 @Options(flushCache = Options.FlushCachePolicy.FALSE))

    // updateUsername 执行后不刷新二级缓存
    @Options(flushCache = Options.FlushCachePolicy.FALSE)
    @Update("update user set username = #{username} where id = #{id}")
    void updateUsername(User user);

B2.编写如下代码,在两次查询Id=1的User中间,对id=1的用户名重新赋值 

/**
     * 测试二级缓存 当某个update设置 flushCache="false" 即更新后不刷新缓存
     */
    @Test
    public void testLevelTwoFlushCache(){
        SqlSessionFactory sessionFactory = sqlSessionFactoryBuilder.build(resourceAsStream);
        SqlSession sqlSession1 = sessionFactory.openSession();
        SqlSession sqlSession2 = sessionFactory.openSession();
        SqlSession sqlSession3 = sessionFactory.openSession(true);
        UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
        UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
        UserMapper userMapper3 = sqlSession3.getMapper(UserMapper.class);
        System.out.println("--------------执行SqlSession1创建的UserMapper1对象的查询---------------------");
        User user1 = userMapper1.findById(1);
        System.out.println("id 为 1 的对象" + user1);
        sqlSession1.close(); // 查询后关闭sqlSession1
        System.out.println("--------------执行SqlSession3创建的UserMapper3对象的增删改操作---------------------");
        User user = new User();
        user.setId(1);
        user.setUsername("sunshineKay");
        userMapper3.updateUsername(user);// 插入后关闭sqlSession1
        sqlSession3.close();
        System.out.println("更新后id为1 的用户用户名 : " + "sunshineKay");
        System.out.println("--------------执行SqlSession2创建的UserMapper2对象的查询---------------------");
        User user2 = userMapper2.findById(1);
        System.out.println("查询id 为 1 的对象 " + user2);
        System.out.println("两次查询出来的对象 user1 user2 是否相等 : " + (user1 == user2));
        sqlSession1.close();
    }

 B3. 虽然数据库已经将用户1的名称改为了sunshineKay但是第二次读取的用户信息仍为之前的===》》》更新用户名时,不在刷新缓存,导致缓存中为第一次查询时存入的旧数据,出现了脏读

 B4.  flushCache仅用于对脏读不会有影响的特殊业务中,一般情况不进行配置,使用默认的flushCache=true即可

4.断点模式跟踪 Mybatis源码查看二级缓存的工作流程

A. Myabtis中的Executor接口作为代码执行器用来执行SQL,SqlSession中的select请求将会进入到Executor的query方法,而Executor仅有两大实现类CachingExecutor及BaseExecutor;

在一级缓存 Mybatis一级缓存中已知BaseExecutor中涉及到一级缓存相关,是否CachingExecutor涉及到二级缓存呢?同时断点CachingExecutor和CachingExecutor中query方法 执行查询操作时,断点进入CachingExecutor中的query方法(之所以会进入到CachingExecutor中query是因为在SqlMapConfig.xml中配置的cacheEnabled属性是true,如果没有配置或者配置的false会走BaseExecutor中的query

query方法通过createKey创建缓存key,并调用query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)方法

在此方法中

A1.从TransationalCacheManager中通过key从二级缓存中获取数据,

A2.二级缓存中有值直接返回,无值调用SimpleExecutor中的query方法(从 Mybatis一级缓存中可知,SimpleExecutor中的query方法会先查看一级缓存中是否有数据,无数据再执行SQL 查询数据库,并将查询结果放放入一级缓存中)

A3.调用TransationalCacheManager 的putObject方法,将数据存入二级缓存

B.根据CachingExecutor中query方法中的(List) tcm.getObject(cache, key);继续跟踪,查看二级缓存存取值逻辑。进入到TransactionalCacheManager的getObject方法,发现本质是调用了TransationCache.getObject

C.继续进入到TransactionalCache中的getObject()方法,在此方法中,实际调用的是PerpetualCache方法中的getObject()

D.继续进入到PerpetualCache中的getObject()方法 发现使用的keyMap.put() 而 private Map cache = new HashMap();本质是一个HaspMap集合 ====>>>> mybatis 二级缓存同一级缓存数据结构一样都为HashMap 

==== >>>>Mybatis开启二级缓存在mapper.xml中标签中,默认type为

PerpetualCache">

E.从步骤C中可看到TransactionalCache向下依次经历了SynchroninzedCache、Log4jImpl、SerializedCache、LruCache、PertetualCache,在SerializedCache中getObject时对对象进行了反序列化,而在存值的时候,对对象进行了序列话 =====>>>  开启二级缓存时,必须要将实体对象implement Serializable 

5.TransactionalCache

 如截图,在TransactionCache方法中,存入缓存或取缓存逻辑存在差异,在取值时 ,是通过delegate(Cache对象)取值,取值为空时,将该key存放到entriesMissedInCache(Set)中,而在存值的时候,会直接存入到entriesToAddOnCommit(HashMap);真正将Cache存放是调用了flushPendingEntries()即commit()方法时,而cache的删除是在调用commit()方法时

=====》》》》 这也就是为什么sqlSession1查询的缓存对SqlSession2生效的条件是sqlSession执行了close()(close方法内部调用了commit方法)或者commit()方法

为什么TransactionalCache类中需要增加Map对象entriesToAddOnCommit临时存放缓存?

这是因为二级缓存是从MappedStatement中获取的(初始化MyBatis时,解析mapper.xml中的<cache>标签时创建一个Cache对象,放入了configuration对象中,并将cache放入了MapperBuilderAssistant.currentCache中。在将SQL语句解析成MappedStatement时,会从MapperBuilderAssistant.currentCache中取出cache放入到MappedStatement中),而MappedStatement存在全局中。多个CachingExecutor可以获取到,如果直接存放到二级缓存中,就会出现线程安全问题,导致脏读;

总结:

Mybatis二级缓存底层数据结构为HashMap;

Mybatis在查询数据时,会优先从二级缓存中查找,其次从一级缓存中查找,二者都找不到才会执行SQL从数据库读取

Mybatis开启二级缓存的同时,需要让对应的实体类实现序列化接口,是因为二级缓存存取的过程中需要将对象序列化及反序列化(二级缓存不仅仅只存储在内存中,有可能存储在硬盘中)

二级缓存生效在于操作进行了commit或者close;

同一个namespace下的操作对象,执行了数据更改,就会导致二级缓存清空,因此二级缓存一般不适用,一般用户不长变化的数据,比如省市区这类数据;

思考:

1. PerpetualCache 为开启二级缓存默认类实现类,此构造方法需要传入Id,此Id为Maper.xml文件的namespace,这是否是二级缓存是Mapper级别的区分?(解释:在解析cahce标签时,会将解析出来的Cache对象,存放发到configuration对象中,key为id即构造方法中传入的

2. 修改标签中的type值为其他实现了Cache接口的类,比如 

 <cache type="org.apache.ibatis.cache.decorators.LruCache"></cache>

会报 Caused by: org.apache.ibatis.cache.CacheException: Invalid base cache implementation (class org.apache.ibatis.cache.decorators.LruCache).  Base cache implementations must have a constructor that takes a String id as a parameter.  Cause: java.lang.NoSuchMethodException: org.apache.ibatis.cache.decorators.LruCache.(java.lang.String) 异常,这应该是配置的自定义实现类需要一个传入参数为String的构造方法

3.Mybati的二级缓存底层数据结构为HashMap,这种数据结构只适用于单服务器工作,无法实现分布式缓存,可通过整合第三方缓存框架,对缓存数据进行集中管理,比如redis,ehcache、memcached

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值