二级缓存
一级缓存因为只能在同一个SqlSession中共享,所以会存在一个问题,在分布式或者多线程的环境下,不同会话之间对于相同的数据可能会产生不同的结果,因为跨会话修改了数据是不能互相感知的,所以就有可能存在脏数据的问题,正因为一级缓存存在这种不足,所以我们需要一种作用域更大的缓存,这就是二级缓存。
二级缓存的作用范围
一级缓存作用域是SqlSession级别,所以它存储的SqlSession中的BaseExecutor之中,但是二级缓存目的就是要实现作用范围更广,那肯定是要实现跨会话共享的,在MyBatis中二级缓存的作用域是namespace,也就是作用范围是同一个命名空间,所以很显然二级缓存是需要存储在SqlSession之外的,那么二级缓存应该存储在哪里合适呢?
在MyBatis中为了实现二级缓存,专门用了一个装饰器来维护,这就是我们上一篇文章介绍Executor时还留下的没有介绍的一个对象:CachingExecutor。
如何开启二级缓存
二级缓存相关的配置有三个地方:1、mybatis-config中有一个全局配置属性,这个不配置也行,因为默认就是true。
<setting name="cacheEnabled" value="true"/>
想详细了解mybatis-config的可以点击这里。2、在Mapper映射文件内需要配置缓存标签:
<cache/> 或 <cache-ref namespace="com.lonelyWolf.mybatis.mapper.UserAddressMapper"/>
想详细了解Mapper映射的所有标签属性配置可以点击这里。3、在select查询语句标签上配置useCache属性,如下:
<select id="selectUserAndJob" resultMap="JobResultMap2" useCache="true"> select * from lw_user </select>
以上配置第1点是默认开启的,也就是说我们只要配置第2点就可以打开二级缓存了,而第3点是当我们需要针对某一条语句来配置二级缓存时候则可以使用。
不过开启二级缓存的时候有两点需要注意:1、需要commit事务之后才会生效 2、如果使用的是默认缓存,那么结果集对象需要实现序列化接口(Serializable)
如果不实现序列化接口则会报如下错误:
接下来我们通过一个例子来验证一下二级缓存的存在,还是用上面一级缓存的例子进行如下改造:
SqlSession session1 = sqlSessionFactory.openSession(); UserMapper userMapper1 = session1.getMapper(UserMapper.class); List<LwUser> userList = userMapper1.selectUserAndJob(); session1.commit();//注意这里需要commit,否则缓存不会生效 SqlSession session2 = sqlSessionFactory.openSession(); UserMapper userMapper2 = session2.getMapper(UserMapper.class); List<LwUser> userList2 = userMapper2.selectUserAndJob();
然后UserMapper.xml映射文件中,新增如下配置:
<cache/>
运行代码,输出如下结果:
上面输出结果中只输出了一次sql,说明用到了缓存,而因为我们是跨会话的,所以肯定就是二级缓存生效了。
二级缓存原理分析
上面我们提到二级缓存是通过CachingExecutor对象来实现的,那么就让我们先来看看这个对象:
我们看到CachingExecutor中只有2个属性,第1个属性不用说了,因为CachingExecutor本身就是Executor的包装器,所以属性TransactionalCacheManager肯定就是用来管理二级缓存的,我们再进去看看TransactionalCacheManager对象是如何管理缓存的:
TransactionalCacheManager内部非常简单,也是维护了一个HashMap来存储缓存。HashMap中的value是一个TransactionalCache对象,继承了Cache。
注意上面有一个属性是临时存储二级缓存的,为什么要有这个属性,我们下面会解释。
二级缓存的创建和使用
我们在读取mybatis-config全局配置文件的时候会根据我们配置的Executor类型来创建对应的三种Executor中的一种,然后如果我们开启了二级缓存之后,只要开启(全局配置文件中配置为true)就会使用CachingExecutor来对我们的三种基本Executor进行包装,即使Mapper.xml映射文件没有开启也会进行包装。
接下来我们看看CachingExecutor中的query方法:
上面方法大致经过如下流程:
- 1、创建一级缓存的CacheKey
- 2、获取二级缓存
- 3、如果没有获取到二级缓存则执行被包装的Executor对象中的query方法,此时会走一级缓存中的流程。
- 4、查询到结果之后将结果进行缓存。
需要注意的是在事务提交之前,并不会真正存储到二级缓存,而是先存储到一个临时属性,等事务提交之后才会真正存储到二级缓存。这么做的目的就是防止脏读。因为假如你在一个事务中修改了数据,然后去查询,这时候直接缓存了,那么假如事务回滚了呢?所以这里会先临时存储一下。所以我们看一下commit方法:
二级缓存如何进行包装
最开始我们提到了一些缓存的包装类,这些都到底有什么用呢?在回答这个问题之前,我们先断点一下看看获取到的二级缓存长啥样:
从上面可以看到,经过了层层包装,从内到外一次经过如下包装:
- 1、PerpetualCache:第一层缓存,这个是缓存的唯一实现类,肯定需要。
- 2、LruCache:二级缓存淘汰机制之一。因为我们配置的默认机制,而默认就是LRU算法淘汰机制。淘汰机制总共有4中,我们可以自己进行手动配置。
- 3、SerializedCache:序列化缓存。这就是为什么开启了默认二级缓存我们的结果集对象需要实现序列化接口。
- 4、LoggingCache:日志缓存。
- 5、SynchronizedCache:同步缓存机制。这个是为了保证多线程机制下的线程安全性。