开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 6 天,点击查看活动详情
在网上大部分文章都说MyBatis一级缓存是SqlSession级别的,二级缓存是Mapper级别的(或者说是namespace级别的)。这个结论没什么问题。可是!!!!对于初学者、想要快速的理解mybatis的缓存机制的人来说,什么是Mapper级别呢?什么又是SqlSession级别呢? 接下来将从如下几个方面并结合源码进行分析mybatis的缓存。
- 什么数据会被缓存?
- 数据何时加入到缓存中?
- 缓存中的数据何时销毁?(或者说缓存中的数据在何时有用)
- 到底什么是Mapper级别?(什么是namespace级别);什么又是SqlSession级别?
文章分为两部分:一级缓存原理和二级缓存原理。这两部分互不影响,读者可自行选择章节进行查阅
mybatis版本:3.5.12
一级缓存
默认情况下一级缓存是开启的,并且无法关闭。一级缓存实际上就是BaseExecutor中的这个属性:PerpetualCache
public abstract class BaseExecutor implements Executor {
// 一级缓存对象,其中有一个Map<Object,Object>,key值就是根据SQL计算得到的一个对象。value是缓存的结果
protected PerpetualCache localCache;
}
PerpetualCache就是常说的MyBatis一级缓存,PerpetualCache中维护了一个Map<Object,Object>,这个Map真正的在内存中缓存了我们从数据库查询得到的结果集数据。这个Map中的value值就是数据库查询的结果集数据。这个Map存储的key是SQL根据一定的算法得到的对象(其实是一个CacheKey对象,但便于理解现在不必深究)。这个Key有这样一个特性,同样的SQL通过算法得到的key值是相同的。
要想了解一级缓存的原理,必须知道在MyBatis中SqlSession执行SQL查询的大致逻辑,通常通过SqlSession.selectList()方法就可以执行SQL语句查询数据库并返回结果。其底层的原理是SqlSession把查询委托给Executor(执行器)对象。
- Executor在查询数据库前,先检查以下一级缓存中是否有改Sql对应的结果,如果有则直接从缓存中返回数据,而无需再查询结果集。
- 如果Executor的一级缓存中没有数据,则再进行数据库查询,查询完毕后把结果存入缓存中。
在源码中对应的逻辑在BaseExecutor#queryFromDatabase方法中(省略非核心代码)
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
List<E> list;
list = localCache.getObject(key);
if (list == null) {
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
return list;
}
// 从数据库中查询,并存入一级缓存
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
localCache.putObject(key, list);
return list;
}
源码就算不标注释都能看的出其执行逻辑和上述描述的一样——先查询一级缓存,如果一级缓存为空,则查询数据库并把返回的结果集放入二级缓存。
总结:一级缓存
现在再回过头来问自己这4个问题
-
什么数据会被缓存?
通过SqlSession查询数据库得到的数据,都会被缓存到Executor中的PerpetualCache对象中
-
数据何时加入到缓存中?
当查询缓存数据不存在,查询数据库得到结果集后,数据会被存入缓存。
-
缓存中的数据何时销毁?(或者说缓存中的数据在何时有用)
既然缓存都存在了Executor中,而Executor又是SqlSession的一个属性。那么只要SqlSession销毁了,缓存也就没了。也就是说同一个SqlSession执行相同的SQL语句时,除了第一次会查数据库,其他的都会查询缓存。
遗憾的是SqlSession通常使用过后就被销毁了。例如我们通常这样编程(只是示例,平时都是用Mapper开发)
public void test() throws Exception { SqlSession sqlSession = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("sqlMapConfig.xml")); sqlSession.selectList("queryUserById",1) // 其他处理逻辑 }在test方法执行完之后,局部变量就会被GC回收掉。SqlSession对象自然也就被销毁了。
就算是Mapper代理方式,底层也是在方法中调用SqlSession对象执行数据库查询,所以销毁时机是一样的。
-
什么又是SqlSession级别?
解决了前三个问题,这个问题应该能明白吧,一级缓存还是很好理解的。SqlSession级别就是指在SqlSession的生命周期内,缓存是有效的。当SqlSession对象销毁时,缓存也随之消失。
接下来是重头戏咯,来看看二级缓存吧!
二级缓存
首先二级缓存在mybatis中默认是关闭的,可以通过全局配置修改,或者通过在Mapper.xml文件中加入<cache/>标签开启指定Mapper的二级缓存
在剖析二级缓存的源码之前,先来了解下mybatis是如何处理XxxMapper.xml文件的,以及什么是MappedStatement。先来看一张图
这里简单介绍一下,MyBatis中的XxxMapper.xml文件会被解析为若干个组件存储在程序的内存中。其中select update insert delete这4种节点(后面只说select了),每个节点都会被解析成一个MappedStatement对象,其实我们只需要把MappedStatement对象当做是要给SQL包装对象就行了,MappedStatement中存储了select节点的SQL信息,命名空间(就是namespace属性值)SQL执行后的返回结果集对应的Java类型映射信息等,
MappedStatement还存储了一个有关二级缓存的属性就是Cache。但它并不存储数据,他只是作为一个标识(后面详细分析)标识哪些MappedStatement共用的一个缓存。换种方式理解,如果两个MappedStatement中的Cache属性相同,那么他们就是公用的一个缓存。这也就是Mapper级别的缓存所代表的含义,也可以说是namespace级别的缓存。
所以,我们再看看下XxxMapper.xml文件解析的成果
- 每个
XxxMapper.xml会被解析为多个MappedStatement对象,并且这些MappedStatement中的Cache属性相同 - 不同的
XxxMapper.xml解析出来的MappedStatement对象的Cache属性是不同的。除非使用<cache-ref/>生命两个mapper文件使用同一个缓存空间。 - 被解析出来的
MappedStatement对象保存在Configuration对象中(这是mybatis的全局配置对象)只有程序关闭时,它才会销毁,否则它会一直存在与程序当中!
怎么样,看到这里,虽然还没分析到源码,但我相信你已经理解了什么是mapper级别(同namespace级别)
接下来,我们就该参考源码分析二级缓存了
源码分析
前文说到,MappedStatement中的Cache属性并不存储数据,而只是作为Mapper的一个标识。那么二级缓存存储在哪里呢?
其实二级缓存真正的归宿是通过TransactionalCacheManager对象完成对二级缓存的增删操作。它是CachingExecutor的一个属性
public class CachingExecutor {
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
}
public class TransactionalCacheManager {
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
}
public class TransactionalCache implements Cache {
private final Cache delegate;
private boolean clearOnCommit;
// 二级缓存存储数据的地方!!!!终于找到了
private final Map<Object, Object> entriesToAddOnCommit;
// 事务提交前,查询的cache标识存在这个集合里。
private final Set<Object> entriesMissedInCache;
}
TransactionalCacheManager中有个Map数据结构的属性transactionalCaches,这个Map的Key就是MappedStatement的cache对象!value是领域给封装好的Cache对象。 其中数据就存储在TransactionalCache中
数据何时存入二级缓存
首先要保证开启了二级缓存。
二级缓存相关代码在CachingExecutor#query方法中,源码如下
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
Cache cache = ms.getCache();
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
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;
}
}
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
先来说下这段代码的逻辑
- 获取MappedStatement的Cache标识,(如果没开启二级缓存,就会得到null)
- 如果Cache标识存在,则刷新二级缓存(有些查询是需要在查询语句前刷新缓存的,防止其他线程已经修改了数据但没有存入数据的情况。默认不刷新)
- 从缓存中获取对象
- 如果缓存的返回值不为空直接返回,否则查询数据库
- 把查询的结果集数据放入二级缓存
其实逻辑是和一级缓存一模一样的,都是先查缓存,缓存中没有就查数据库。但是不同与一级缓存的是,二级缓存并非是要给Map那么简单直接put(key,value)就可以了,二级缓存是通过TransactionalCacheManager来管理的。来看下TransactionalCacheManager的重要属性
public class TransactionalCacheManager {
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
}
transactionalCaches这个Map的Key就是MappedStatement的cache对象!value是领域给封装好的Cache对象。 如果只停留在这一层,其实就可以断定,数据一定存储在TransactionalCache对象当中了。但我们不妨深入下TransactionalCache的重要属性如下
public class TransactionalCache implements Cache {
// 二级缓存存储数据的地方!!!!终于找到了
private final Cache delegate; // 装饰者模式
// 事务提交前,数据都在这里缓存
private final Map<Object, Object> entriesToAddOnCommit;
// 事务提交前,如果查询二级缓存会未命中,就把MS对象的Cache标识存储在这个集合中
private final Set<Object> entriesMissedInCache;
}
看到了呗,其实底层的底层还是Map对象和Cache对象(当然了,Cache底层也是Map)。
下面我们再来分析下tcm是如何把数据存入缓存和如何从缓存中取出数据的
tcm把数据存储到二级缓存
事务提交前
tcm在程序查询完数据后,先把数据存储到TransactionalCache的Map中,在事务提交后才会把数据真正的存到二级缓存。
提交事务前,把数据存TransactionalCache就是对应的TransactionalCacheManager#putObject方法
public class TransactionalCacheManager {
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
public void putObject(Cache cache, CacheKey key, Object value) {
getTransactionalCache(cache).putObject(key, value);
}
}
putObject的方法很简单,首先通过Cache标识(还记得吗,这个标识是从MS对象中来的哦,它并不存数据)获取TransactionalCache对象,然后嗲用TransactionalCache的putObject方法。TransactionalCache#putObject源码如下
public class TransactionalCache implements Cache {
// 二级缓存存储数据的地方!!!!终于找到了
private final Cache delegate; // 装饰者模式
// 事务提交前,数据都在这里缓存
private final Map<Object, Object> entriesToAddOnCommit;
// 事务提交前,如果查询二级缓存会未命中,就把MS对象的Cache标识存储在这个集合中
private final Set<Object> entriesMissedInCache;
public void putObject(Object key, Object object) {
entriesToAddOnCommit.put(key, object);
}
}
最终就是把CackeKey作为key,查询数据库得到的数据作为value存入到了Map中。(CacheKey上文有说过哈,可以就粗暴的认为他是SQL的唯一标识)
但是查询二级缓存是从delegate这个属性中查的,也就是说entriesToAddOnCommit中存储的数据都只是事务提交前的数据,如果事务回滚,或发生异常,数据都会丢失。只有在执行commit方法提交事务的时候才会把数据都存储到Cache delegate这个属性中
事务提交后
public void commit() {
flushPendingEntries();
reset();
}
private void flushPendingEntries() {
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
delegate.putObject(entry.getKey(), entry.getValue());
}
}
只有事务提交后,才会把数据存入到Cache对象中。
tcm从二级缓存读数据
tcm读取二级缓存的时候就没有那么多门门道道了,就是对应的TransactionalCacheManager#getObject方法
public class TransactionalCacheManager {
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
public Object getObject(Cache cache, CacheKey key) {
return getTransactionalCache(cache).getObject(key);
}
}
第一步也很简单,通过Cache标识符获取TransactionalCache对象,然后调用TransactionalCache对象的getObject方法,下面就来看TransactionalCache#getObject
public Object getObject(Object key) {
Object object = delegate.getObject(key);
if (object == null) {
entriesMissedInCache.add(key);
}
if (clearOnCommit) {
return null;
} else {
return object;
}
}
上一小节分析,只有事务提交后,数据才会被存入Cache对象。如果事务未提交,这里第一步delegate.getObject(key);返回的一定是null值。并别会把key加入到entriesMissedInCache集合。如果事务提交了,自然在此处也能获取到二级缓存中的值了。
总结:二级缓存
- 默认二级缓存关闭,需手动开启
- 二级缓存在事务提交后才能生效
- 之所以二级缓存被叫做Mapper级别(namespace级别)是因为一个xml文件对应一个缓存,这样,同一个xml中的所有语句查询的就是要给缓存了。
二级缓存的一些坑
返回的对象必须可序列化
如果查询返回的结果被封装成一个POJO,这个POJO必须实现Serializable,接口,否则在tcm.commit时程序会报错
org.apache.ibatis.exceptions.PersistenceException:
### Error committing transaction. Cause: org.apache.ibatis.cache.CacheException: Error serializing object. Cause: java.io.NotSerializableException: org.apache.ibatis.amy.pojo.Order
### Cause: org.apache.ibatis.cache.CacheException: Error serializing object. Cause: java.io.NotSerializableException: org.apache.ibatis.amy.pojo.Order
原因是TransactionalCache中的代理Cache对象中间代理了一层SerializedCache,而SerializedCache中的putObject必须要求对象实现Serializable接口
public void putObject(Object key, Object object) {
if (object == null || object instanceof Serializable) {
delegate.putObject(key, serialize((Serializable) object));
} else {
throw new CacheException("SharedCache failed to make a copy of a non-serializable object: " + object);
}
}
TransactionalCache代理如下
SqlSession事务不生效
SqlSession sqlSession = factory.openSession(true);
通过该代码获取的SqlSession只对增删改的操作自动提交事务,对于select查询不会自动提交事务。所以测试二级缓存时需要手动提交事务。否则会出现二级缓存失效的情况