一级缓存
一级缓存是指 SqlSession 级别的缓存 原理:使用的数据结构是一个 map,如果两次中间出现 commit 操作 (修改、添加、删除),本 sqlsession 中的一级缓存区域全部清空,默认情况下一级缓存是开启的,而且是不能关闭的。


上一篇文章分析了mybatis的源码架构,执行者的基础类是Executor,在BaseExecutor中,会调用到query方法,这里面会判断PerpetualCache(localCache)中有没有缓存,里面实际上就是一个map,缓存了查询的方法标识和查询的结果。

可以看到这localcache的Map是属于baseExecutor的,也就是属于sqlsession所以当在同一个sqlsession的会话信息里面,他其实是会根据方法标识和参数,来直接返回已经缓存了的内容的。所以,他是面向session的一个缓存。

二级缓存
二级缓存是指可以跨 SqlSession 的缓存。是 mapper 级别的缓存; 原理: 是通过 CacheExecutor 实现的。CacheExecutor其实是 Executor 的代理对象。
// --------mybatis-config.xml
<!--开启二级缓存 -->
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
//------------TestMapper.xml
<!-- 开启二级缓存 -->
<cache></cache>
//---------------------demo class
SqlSession seesion1 = sqlSessionFactory.openSession();
TestMapper testMapper = seesion1.getMapper(TestMapper.class);
Object object = testMapper.selectOne("test");
SqlSession session2 = sqlSessionFactory.openSession();
TestMapper testMapper2 = seesion1.getMapper(TestMapper.class);
Object object2 = testMapper.selectOne("test");
首先先在配置文件中开启二级缓存,然后创建两个会话运行同样的查询看看效果。

debug可以看到,调用会进入到cacheExecutor里的query方法,其实chacheExecutor是baseExecutor的包装类,这里面会先判断缓存中存不存在,这里可以看到获取缓存的方法是从ms.getCache()中获取的,MappedStatement这个类实际上是封装在Configuration里面的,是在加载配置文件生成配置类的时候就已经创建了的,通过修改配置文件开启二级缓存,会把不同sqlSession会话中的结果信息和方法信息都缓存在MappedStatement的实例变量Map中,用以其他会话的查询。
这里可以看到一级缓存 二级缓存听起来好像很厉害的样子,其实就是动态代理加上个map缓存,没什么花头,而且二级缓存还有bug,用起来很容易出问题,所以二级缓存默认是关闭的,因为二级缓存针对的是MappedStatment,所以所有的sqlsession是共享这个二级缓存的。但是二级缓存刷新的时间点非常诡异,他是在sqlsession.close()和commit()等方法中去刷新的二级缓存,这就导致了数据不一致的问题,也就是说在session1中的查询,结果被缓存到二级缓存之后,这时候session2删除了一条数据并且提交了,其实这里已经更新了二级缓存,但是如果这个时候session1关闭了,会默认调用一个commit方法,会把它里面的缓存的值给刷新到二级缓存中,所以,当有第三个session3来获取数据的时候,获取的其实是session1读取到的数据,是错误的数据,也就是获取的是最后关闭的session的数据。
其实这个问题是因为二级缓存默认加载了PerpertualCache,这个缓存类又被层层包装代理,最后包装成了TransactionalCache,但是这两个类中分别有两个Map来缓存数据,所以当session关闭的时候,会把TransactionalCache中缓存Map中的数据刷新到PerpertualCache,PerpertualCache其实是属于Configuration的,所以他是被所有会话共用的,但是TransactionalCache是属于单一的session的,所以数据更新是不及时的。
