使用
作为目前最常用的ORM框架之一,mybatis同样拥有提高查询效率的缓存机制。同样,它的缓存使用起来也非常简单,只需在Mapper.xml文件下加入如下配置即可。默认情况下,缓存是开启的
<cache type="org.apache.ibatis.cache.impl.PerpetualCache"
size="1024"
eviction="LRU"
flushInterval="120000"
readOnly="false"/>
结构
接着,我们开看一下mybatis内在的缓存体系
mybatis跟缓存相关的类都在cache 包里面,有一个Cache 接口,并且有一个默认的基本实现类 PerpetualCache,只要你在mybaits中使用缓存,就会创建这个类
public class PerpetualCache implements Cache {
private final String id;
private final Map<Object, Object> cache = new HashMap<>();
...
}
缓存使用map结构实现
除开最基本的实现类,mybatis还使用了装饰器模式进行扩展,即decorators包下的所有实现类。接下来,我们来对所有的装饰类来简单的分一下类:
按照颜色一次分为:基本缓存、有淘汰机制的缓存、特殊功能的缓存
分类
按照工作范围分类,mybatis缓存可分为一级缓存、二级缓存
一级缓存(local cache)
一级缓存也叫本地缓存,它的作用域是session,也就是sqlSession。也就是说当你的查询使用的是同一个sqlSession对象时,当你的sql语句和之前的是一样的时候,它就会去缓存里拿数据。一级缓存是默认开启的
接着,我们思考一个问题:既然一级缓存的所用域是sqlSession,那么PerpetualCache 应该放在哪里去进行管理呢?
猜测:既然是基于sqlSession层面的,我们干脆把缓存放到sqlSession对象里
验证:我们去看一下mybatis对sqlSession对象的封装
DefaultSqlSession .java
public class DefaultSqlSession implements SqlSession {
private final Configuration configuration;
private final Executor executor;
...
}
在DefaultSqlSession里面没有直接放一个缓存对象,只有Configuration 和Executor 对象,其中Configuration 对象是配置文件的所有数据,是全局的,且唯一的,那一级缓存放在里面显然是不合理的,那排除掉一个,一级缓存就只能放在Executor 对象里了
Executor 是一个interface接口,我们看一下它的基本实现类
BaseExecutor .java
public abstract class BaseExecutor implements Executor {
private static final Log log = LogFactory.getLog(BaseExecutor.class);
protected Transaction transaction;
protected Executor wrapper;
protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
protected PerpetualCache localCache;
protected PerpetualCache localOutputParameterCache;
protected Configuration configuration;
...
}
发现里面果然有PerpetualCache 对象,因为一个sqlSession就有一个Executor 。从而实现了同一个会话共享一个一级缓存,如下图:
接下来,我们简单写个测试类来印证一下
public void testCache() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session1 = sqlSessionFactory.openSession();
SqlSession session2 = sqlSessionFactory.openSession();
try {
BlogMapper mapper0 = session1.getMapper(BlogMapper.class);
BlogMapper mapper1 = session1.getMapper(BlogMapper.class);
Blog blog = mapper0.selectBlogById(1);
System.out.println(blog);
System.out.println("第二次查询,相同会话,获取到缓存了吗?");
System.out.println(mapper1.selectBlogById(1));
} finally {
session1.close();
}
}
注意:在测试一级缓存的时候,需要把二级缓存关掉,因为默认是打开的,同时打开打印sql语句的开关,待会可以通过sql语句是否打印来判断是取的缓存还是到数据库去拿的数据
mybatis-config.xml
<!-- 打印查询语句 -->
<setting name="logImpl" value="STDOUT_LOGGING" />
<!-- 控制全局缓存(二级缓存),默认 true-->
<setting name="cacheEnabled" value="false"/>
运行,查询结果如下:
没看到,第一次查询是操作数据库去拿,而第二次去哪的时候明显是从缓存里去拿的,这是同一个sqlSession执行的结果,那不是同一个sqlSqssion呢?
public void testCache() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session1 = sqlSessionFactory.openSession();
SqlSession session2 = sqlSessionFactory.openSession();
try {
BlogMapper mapper0 = session1.getMapper(BlogMapper.class);
Blog blog = mapper0.selectBlogById(1);
System.out.println(blog);
System.out.println("第二次查询,不同会话,获取到缓存了吗?");
BlogMapper mapper2 = session2.getMapper(BlogMapper.class);
System.out.println(mapper2.selectBlogById(1));
} finally {
session1.close();
}
}
运行结果:
结果很明显看到,查询了两次数据库。
那什么时候一级缓存会清空呢?用过缓存组件的都知道,当值发生了变换,就会导致缓存被删掉,我们用代码测试一下:
public void testCacheInvalid() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = sqlSessionFactory.openSession();
try {
BlogMapper mapper = session.getMapper(BlogMapper.class);
System.out.println(mapper.selectBlogById(1));
Blog blog = new Blog();
blog.setBid(1);
blog.setName("after modified 666");
mapper.updateByPrimaryKey(blog);
session.commit();
// 相同会话执行了更新操作,缓存是否被清空?
System.out.println("在[同一个会话]执行更新操作之后,是否命中缓存?");
System.out.println(mapper.selectBlogById(1));
} finally {
session.close();
}
}
运行结果:
很明显,在经过更新操作后,同一个sqlSqssion下的同一条sql语句又去执行了数据库操作。
接下来思考一个问题:如果我们的应用是跨会话,那我在第二个会话里update数据之后,第一个会话查询的是缓存数据呢,还是到数据库里拿最新的数据呢?
同样,我们用代码测试一下:
public void testDirtyRead() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session1 = sqlSessionFactory.openSession();
SqlSession session2 = sqlSessionFactory.openSession();
try {
BlogMapper mapper1 = session1.getMapper(BlogMapper.class);
System.out.println(mapper1.selectBlogById(1));
// 会话2更新了数据,会话2的一级缓存更新
Blog blog = new Blog();
blog.setBid(1);
blog.setName("after modified 333333333333333333");
BlogMapper mapper2 = session2.getMapper(BlogMapper.class);
mapper2.updateByPrimaryKey(blog);
session2.commit();
// 其他会话更新了数据,本会话的一级缓存还在么?
System.out.println("会话1查到最新的数据了吗?");
System.out.println(mapper1.selectBlogById(1));
} finally {
session1.close();
session2.close();
}
}
运行结果:
很明显,第一个会话还是拿的缓存的数据,也就是过时的数据,这个就出现了问题,因为一级缓存的作用域太小导致的,解决的办法就是二级缓存
二级缓存
二级缓存的作用是namespace,也就是命名空间。只要是同一个Mapper.xml的操作,就能读取到二级缓存。如果同时打开一级缓存,二级缓存,会先去二级缓存去拿取数据,如果拿到直接返回
一级缓存是Sqlsession级别的,所以放到DefaultSqlSession对象里面,而二级缓存是namespace级别的,那它应该放到哪里去管理呢?
猜测:因为二级缓存的作用域是namespace,那肯定是在sqlSession外面,同时,在一级缓存二级缓存都开启的情况下,会优先读取二级缓存,说明,二级缓存肯定在一级缓存外面,是不是有可能使用装饰器模式在Executor外面又装饰一层呢?
验证:我们去看一下Executor的装饰类
CachingExecutor.java
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
Cache cache = ms.getCache();
// cache 对象是在哪里创建的? XMLMapperBuilder类 xmlconfigurationElement()
// 由 <cache> 标签决定
if (cache != null) {
// flushCache="true" 清空一级二级缓存 >>
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
// 获取二级缓存
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
// 写入二级缓存
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
// 走到 SimpleExecutor | ReuseExecutor | BatchExecutor
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
果然找到了CachingExecutor装饰类,查询时会先到二级缓存里去拿,如果没有,再接着往下查询,并将查询结果写入到二级缓存,如下图:
使用方法
-
在mybaits-config.xml中配置,默认是打开的,可以不用管,如果需要关掉,这需要配置cacheEnabled的值为false
<setting name="cacheEnabled" value="true"/>
-
在Mapper.xml文件中加上< cache />标签
<cache type="org.apache.ibatis.cache.impl.PerpetualCache" size="1024" eviction="LRU" flushInterval="120000" readOnly="false"/>
ps:如果你想单独关掉一个sql语句的二级缓存,可以在Mapper.xml对应的< select />里加上 userCache=false
印证
接下来,我们通过代码去测试一下二级缓存的作用
public void testCache() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session1 = sqlSessionFactory.openSession();
SqlSession session2 = sqlSessionFactory.openSession();
try {
BlogMapper mapper1 = session1.getMapper(BlogMapper.class);
System.out.println(mapper1.selectBlogById(1));
System.out.println("第二次查询");
BlogMapper mapper2 = session2.getMapper(BlogMapper.class);
System.out.println(mapper2.selectBlogById(1));
} finally {
session1.close();
}
}
运行结果:
发现,跨会话时,同样是查询了两次数据库,并没有用到二级缓存,这是为什么呢?我们看之前CachingExecutor装饰类中的代码,发现二级缓存都是放到事务里面去管理的,也就是说当你没有commit时,他是不会存放到二级缓存的
我们加上commit再测试一下:
public void testCache() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session1 = sqlSessionFactory.openSession();
SqlSession session2 = sqlSessionFactory.openSession();
try {
BlogMapper mapper1 = session1.getMapper(BlogMapper.class);
System.out.println(mapper1.selectBlogById(1));
// 事务不提交的情况下,二级缓存会写入吗?
session1.commit();
System.out.println("第二次查询");
BlogMapper mapper2 = session2.getMapper(BlogMapper.class);
System.out.println(mapper2.selectBlogById(1));
} finally {
session1.close();
}
}
运行结果:
结果发现,果然二级缓存和事务有关
为什么二级缓存和事务有关,而一级缓存没有呢?
因为怕回滚,当你有两个回话的时候,你马上写入缓存,当时当commit是出现异常, 就不会提交,从而导致数据库与缓存数据不一致的问题
而一级缓存作用域是session,也就是回话,当回话挂掉后,一级缓存随即清空,因为回滚时,事务发生异常时,回话也就随机挂掉,导致一级缓存就没有了
总结
一级缓存
- 作用域:session
- 开启方式:默认开启,如果需要关系,在mybatis-config.xml文件中< setting name=“localCacheScope” value=“STATEMENT”/>
- 不足:作用域太小,导致不同的会话会出现过时的数据
二级缓存
- 作用域:namespace
- 开启方式:需要同时在mybatis-config.xml和Mapper.xml文件中配置,如果需要某一条sql不使用二级缓存,则在对应的< select />上设置userCache=false
- 特点:和事务相关