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