注:本系列源码分析基于mybatis 3.5.6,源码的gitee仓库仓库地址:funcy/mybatis.
本文将从源码的角度来分析mybatis的缓存。
在前面分析mybatis的sql执行流程时,CachingExecutor一路执行下去会遇到两个缓存:
CachingExecutor#query(...)执行时,会先判断缓存中是否存在,不存在时才去调用具体的Executor查询BaseExecutor#query(...)执行时,会先从localCache中获取,不存在时才从数据库中查询
我们还是以上一文中的sql执行过程为例,逐步分析这两个缓存。
1. mybatis一级缓存
让我们进入BaseExecutor#query(...)方法:
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds,
ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
...
List<E> list;
try {
queryStack++;
// 1. 直接从缓存中获取数据
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 从数据库中获取
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
// 2. 缓存范围为STATEMENT时,会清除缓存
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
// 清除数据库中的记录
clearLocalCache();
}
}
return list;
}
这个方法有点长,不过我们只需要关注两个地方即可:
- 直接从缓存中(
localCache)获取数据 - 缓存范围为
STATEMENT时,每次查询后会清除缓存,即数据不会被缓存
1.1 什么是一级缓存
首先我们来看下localCache是个啥:
public abstract class BaseExecutor implements Executor {
...
/**
* 这就是 localCache
*/
protected PerpetualCache localCache;
protected BaseExecutor(Configuration configuration, Transaction transaction) {
...
// 在这里赋值
this.localCache = new PerpetualCache("LocalCache");
}
...
这个localCache就是PerpetualCache,我们继续:
public class PerpetualCache implements Cache {
private final String id;
private final Map<Object, Object> cache = new HashMap<>();
public PerpetualCache(String id) {
this.id = id;
}
/**
* 设置缓存的操作
*/
@Override
public void putObject(Object key, Object value) {
cache.put(key, value);
}
/**
* 获取缓存的操作
*/
@Override
public Object getObject(Object key) {
return cache.get(key);
}
/**
* 移除缓存的操作
*/
@Override
public Object removeObject(Object key) {
return cache.remove(key);
}
/**
* 清空缓存
*/
@Override
public void clear() {
cache.clear();
}
...
}
PerpetualCache就是包装了Map的处理,缓存的添加、获取、移除等操作,实际上就是对Map的操作。
到这里我们就明白了,所谓的一级缓存(localCache),就是一个Map,记录都是保存在内存中的。注意:这里的Map是HashMap,是非线程安全的。
1.2 一级缓存的更新时机
在BaseExecutor#query(...)方法中,如果没命中缓存,则会从数据库中查询,我们来看看数据库的查询流程:
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);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
代码可以看的很清楚了,在处理查询(doQuery(...))后,会手动移除一级缓存(localCache.removeObject(...)),并设置一级缓存(localCache.putObject(...)).
当然了,在update操作中也有类似的处理缓存的操作,这里就不一一看了。
1.3 一级缓存的作用范围及禁用
一级缓存中,缓存数据的Map是HashMap,并非线程安全的,因此一级缓存并非线程安全的。那么它的作用范围呢?
通过代码的追溯,localCache是Executor的成员变量,而Executor又是DefaultSqlSession的成员变量。在前面的分析中,DefaultSqlSession并不是线程安全的,比较合理的方式是为每个线程新建一个DefaultSqlSession,至此可以推断出一级缓存(localCache)的作用范围为SqlSession,保存在内存中。
由于一级缓存的作用范围为SqlSession,因此使用时可能会导致数据库更新了,但缓存还没变。
举例说明下:
// 配置文件路径
String resource = "org/apache/ibatis/demo/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(inputStream);
// 使用 sqlSession01 获取 userMapper
SqlSession sqlSession01 = null;
SqlSession sqlSession02 = null;
try {
sqlSession01 = factory.openSession(true);
sqlSession02 = factory.openSession(true);
UserMapper userMapper01 = sqlSession01.getMapper(UserMapper.class);
// 使用 sqlSession02 获取 userMapper
UserMapper userMapper02 = sqlSession02.getMapper(UserMapper.class);
// 处理查询与更新操作,顺序很重要
// 使用 userMapper01 查询
List<User> users = userMapper01.selectList(3L, 10);
System.out.println("userMapper01 得到的users: " + users);
// 使用 sqlSession02 更新
int result = userMapper02.updateUser(3L, "HelloWorld");
System.out.println("更新的记录数: " + result);
// 使用 sqlSession02 查询
List<User> users2 = userMapper02.selectList(3L, 10);
System.out.println("userMapper02 得到的users: " + users2);
// 使用 sqlSession01 查询
List<User> users01 = userMapper01.selectList(3L, 10);
System.out.println("userMapper01 得到的users: " + users01);
} finally {
// 省略关闭操作
...
}
以上代码先是获取了两个SqlSession,然后分别从这两个SqlSession得到UserMapper的实例userMapper01、userMapper02,然后我们使用这两个UserMapper实例进行操作:
- 使用
userMapper01查询id为3的记录 - 使用
userMapper02更新id为3的记录 - 使用
userMapper02查询id为3的记录 - 使用
userMapper01查询id为3的记录
运行,得到的结果如下:
userMapper01 得到的users: [User{id=3, loginName='test', nick='test'}]
更新的记录数: 1
userMapper02 得到的users: [User{id=3, loginName='test', nick='HelloWorld'}]
userMapper01 得到的users: [User{id=3, loginName='test', nick='test'}]
从运行结果来看,userMapper02更新成功了,userMapper02也能查到更新后数据了,但userMapper01第二次查到的依然是更新前的记录,这也就是说,使用userMapper01第一次查询时,记录被缓存了,第二次查询时,并没有查询数据库,而是直接从缓存里获取数据了。
从以上示例可以看到,开启一级缓存后,并不能查到实时数据,并且一级缓存并没有提供对外操作的入口,启用后极有可能会产生严重后果,那么我们要怎么关闭它呢?
在mybatis文档的settings节点介绍中,有一个属性可以解决这个问题:
我们在配置文件中这样设置:
<configuration>
<properties resource="org/apache/ibatis/demo/config.properties">
</properties>
<settings>
<!-- 设置一级缓存级别 -->
<setting name="localCacheScope" value="STATEMENT"/>
</settings>
...
</configuration>
这样设置之后,一级缓存就不再缓存数据了。关于这样做的原理,就得回到BaseExecutor#query(...)方法了:
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds,
ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
...
if (queryStack == 0) {
...
// 2. 缓存范围为STATEMENT时,会清除缓存
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// 清除数据库中的记录
clearLocalCache();
}
}
return list;
}
/**
* 清除缓存的操作
*/
@Override
public void clearLocalCache() {
if (!closed) {
localCache.clear();
localOutputParameterCache.clear();
}
}
从源码来看,将localCacheScope设置为STATEMENT后,每次查询完成后,都会调用localCache.clear()来清除缓存了。
2. mybatis二级缓存
接下来我们再来看看二级缓存。
2.1 开启二级缓存
在创建执行器时,mybatis在创建完具体的执行器后,会再缓存执行器:
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
// 根据类型创建具体的执行器
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
// 缓存装饰
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
// 处理 plugin(插件)
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
在之后处理sql的查询/更新操作时,都是调用CachingExecutor进行,比如查询操作:
@Override
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) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
// 从缓存中获取
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);
}
这里的Cache cache = ms.getCache()就是用来操作二级缓存的。默认情况下,二级缓存并未开启:
可以看到,cache 为 null,表示并未启用二级缓存。那么该如何启用呢?mybatis文档告诉了我们启用方式:
我们在UserMapper.xml中添加<cache/>标签:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.apache.ibatis.demo.mapper.UserMapper">
<cache/>
...
</mapper>
再次执行查询时,就会发现cache已经不为null了:
2.2 配置二级缓存
在mybatis文档中,二级缓存还有其他的配置,这里也一并贴出来:
这些参数本文就不细讲了,我们来看下这些参数是在哪里解析的。
<cache/>标签定义在mapper.xml文件中,我们当然就想到它应该是在解析mapper.xml文件时解析到的,我们进入<mapper>标签的解析方法XMLMapperBuilder#configurationElement:
private void configurationElement(XNode context) {
try {
...
// 解析二级缓存标签
cacheElement(context.evalNode("cache"));
...
} catch (Exception e) {
...
}
}
继续进入XMLMapperBuilder#cacheElement方法:
private void cacheElement(XNode context) {
if (context != null) {
String type = context.getStringAttribute("type", "PERPETUAL");
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
String eviction = context.getStringAttribute("eviction", "LRU");
Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
Long flushInterval = context.getLongAttribute("flushInterval");
Integer size = context.getIntAttribute("size");
boolean readWrite = !context.getBooleanAttribute("readOnly", false);
boolean blocking = context.getBooleanAttribute("blocking", false);
Properties props = context.getChildrenAsProperties();
// 构建Cache对象
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size,
readWrite, blocking, props);
}
}
- 缓存的默认实现类为
PerpetualCache,我们也可以指定type为我们自己的实现类,下节再说明 - 缓存过期的默认策略为
LruCache readOnly默认值为falseblocking默认值为falsesize默认值为nullflushInterval默认值为null
我们继续进入MapperBuilderAssistant#useNewCache看看cache的构建过程:
public Cache useNewCache(Class<? extends Cache> typeClass,
Class<? extends Cache> evictionClass,
Long flushInterval,
Integer size,
boolean readWrite,
boolean blocking,
Properties props) {
// 构建缓存
Cache cache = new CacheBuilder(currentNamespace)
.implementation(valueOrDefault(typeClass, PerpetualCache.class))
.addDecorator(valueOrDefault(evictionClass, LruCache.class))
.clearInterval(flushInterval)
.size(size)
.readWrite(readWrite)
.blocking(blocking)
.properties(props)
.build();
// 添加缓存
configuration.addCache(cache);
currentCache = cache;
return cache;
}
这个方法主要是调用CacheBuilder来构建Cache,我们进入CacheBuilder#build方法:
public Cache build() {
// 设置默认的实现
setDefaultImplementations();
// 实例化,反射实例化
Cache cache = newBaseCacheInstance(implementation, id);
setCacheProperties(cache);
// 使用的是默认缓存才需要装饰
if (PerpetualCache.class.equals(cache.getClass())) {
for (Class<? extends Cache> decorator : decorators) {
cache = newCacheDecoratorInstance(decorator, cache);
setCacheProperties(cache);
}
// 设置标准的装饰器,用来处理eviction,flushInterval,size,readOnly等配置
cache = setStandardDecorators(cache);
}
// 如果不是 PerpetualCache,不会处理eviction,flushInterval,size,readOnly等配置
else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
cache = new LoggingCache(cache);
}
return cache;
}
/**
* 设置缓存的默认实现类
*/
private void setDefaultImplementations() {
if (implementation == null) {
implementation = PerpetualCache.class;
if (decorators.isEmpty()) {
decorators.add(LruCache.class);
}
}
}
/**
* 处理标准装饰器
* @param cache
* @return
*/
private Cache setStandardDecorators(Cache cache) {
try {
MetaObject metaCache = SystemMetaObject.forObject(cache);
if (size != null && metaCache.hasSetter("size")) {
metaCache.setValue("size", size);
}
if (clearInterval != null) {
// 定其清理
cache = new ScheduledCache(cache);
((ScheduledCache) cache).setClearInterval(clearInterval);
}
if (readWrite) {
// 序列化
cache = new SerializedCache(cache);
}
// 日志
cache = new LoggingCache(cache);
// 同步
cache = new SynchronizedCache(cache);
// 阻塞
if (blocking) {
cache = new BlockingCache(cache);
}
return cache;
} catch (Exception e) {
throw new CacheException(...);
}
}
默认情况下,缓存的实现类为PerpetualCache,对于默认的缓存,实例化后,会根据配置的参数进行相关的装饰操作,如配置了clearInterval参数,就会使用ScheduledCache进行装饰:
@Override
public void putObject(Object key, Object object) {
clearWhenStale();
delegate.putObject(key, object);
}
@Override
public Object getObject(Object key) {
return clearWhenStale() ? null : delegate.getObject(key);
}
private boolean clearWhenStale() {
// 判断是否超时
if (System.currentTimeMillis() - lastClear > clearInterval) {
clear();
return true;
}
return false;
}
ScheduledCache的功能是定期清理缓存,在put、get操作时,都会判断是否超时,如果超时了就会触发清理操作。
mybatis的cache实现及装饰器如下:
可以看到,真正的实现只有PerpetualCache,其余的均为装饰器。
来看看得到的Cache:
由于层层装饰,cache的层次有点多,最底层的cache为PerpetualCache,这是真正干活的类,它的id为org.apache.ibatis.demo.mapper.UserMapper,也就是Mapper接口的包名.类名。
我们继续看看configuration.addCache(cache)操作:
protected final Map<String, Cache> caches = new StrictMap<>("Caches collection");
public void addCache(Cache cache) {
caches.put(cache.getId(), cache);
}
这一步的操作就是把cache保存到caches中了。注意到caches类型为StrictMap,稍微看下它的定义:
protected static class StrictMap<V> extends HashMap<String, V> {
...
}
就是HashMap的子类了。
这一步得到的caches的结果如下:
这一步得到cache后,在后面解析select|insert|update|delete语句时,会把得到的当前得到的cache一并放入到MappedStatement对象中,相关操作为MapperBuilderAssistant#addMappedStatement(...)方法,就不过多分析了。
2.3 TransactionalCacheManager
再回到CachingExecutor#query(...)方法,处理操作的方法如下:
public class CachingExecutor implements Executor {
/**
* 缓存管理
*/
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds,
ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
// 这个就是 PerpetualCache 装饰后的对象
Cache cache = ms.getCache();
// 操作缓存
if (cache != null) {
// 从缓存中获取
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
...
// 添加到缓存中
tcm.putObject(cache, key, list);
}
...
}
...
}
...
}
可以看到,得到cache后,使用TransactionalCacheManager进行操作,我们进入TransactionalCacheManager看看相关方法:
public class TransactionalCacheManager {
// 也是一个Map,保存的是 TransactionalCache
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
public Object getObject(Cache cache, CacheKey key) {
return getTransactionalCache(cache).getObject(key);
}
/**
* 从transactionalCaches 中获取 TransactionalCache,如果不存在则新建
*/
private TransactionalCache getTransactionalCache(Cache cache) {
return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
}
...
}
关于TransactionalCache,它也是Cache的实现类:
public class TransactionalCache implements Cache {
/**
* 真正的缓存操作类
*/
private final Cache delegate;
private final Set<Object> entriesMissedInCache;
...
public TransactionalCache(Cache delegate) {
this.delegate = delegate;
this.entriesToAddOnCommit = new HashMap<>();
...
}
@Override
public Object getObject(Object key) {
// issue #116
Object object = delegate.getObject(key);
if (object == null) {
entriesMissedInCache.add(key);
}
// issue #146
if (clearOnCommit) {
return null;
} else {
return object;
}
}
@Override
public void putObject(Object key, Object object) {
// 先添加到临时缓存中
entriesToAddOnCommit.put(key, object);
}
/**
* 处理事务的提交操作
* 事务提交时,才把临时缓存中的缓存添加到缓存中
*/
public void commit() {
if (clearOnCommit) {
delegate.clear();
}
// 更新缓存
flushPendingEntries();
reset();
}
/**
* 更新缓存
*/
private void flushPendingEntries() {
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
delegate.putObject(entry.getKey(), entry.getValue());
}
for (Object entry : entriesMissedInCache) {
if (!entriesToAddOnCommit.containsKey(entry)) {
delegate.putObject(entry, null);
}
}
}
...
}
这个类实现了如下功能:
- 它也是个包装类,实现了缓存的事务功能
- 添加对象时,会行添加到临时缓存中,当事务提交后,才会真正添加到二级缓存中
- 对象的移除也是类似的操作,只有当事务提交后才会真正操作二级缓存
2.4 使用自定义二级缓存
从上面的分析来看,mybatis为每个mapper.xml(namespace)都创建了一个缓存对象(默认实现类为PerpetualCache),与MappedStatement绑定在一起,因此二级缓存的作用范围为全局,而且对于每个namespace都对应一个缓存对象。
二级缓存的默认实现为PerpetualCache,我们来看看它的内容:
public class PerpetualCache implements Cache {
...
// 用hashmap来保存记录
private final Map<Object, Object> cache = new HashMap<>();
@Override
public void putObject(Object key, Object value) {
cache.put(key, value);
}
@Override
public Object getObject(Object key) {
return cache.get(key);
}
@Override
public Object removeObject(Object key) {
return cache.remove(key);
}
@Override
public void clear() {
cache.clear();
}
...
}
从代码来看,PerpetualCache其中维护了一个名为cache的成员变量,它是HashMap类型的,缓存的添加/获取/清除等都是操作这个HashMap,mybatis的二级缓存同样也是保存在内存中的!
HashMap并不是线程安全的,我们在多线程环境下能使用二级缓存的默认实现(PerpetualCache)吗?答案是可以,虽然HashMap不是线程安全的,但经过缓存装饰器之后,二级缓存的添加/获取/清除等操作就是线程安全了,这就是SynchronizedCache的功劳:
public class SynchronizedCache implements Cache {
// 被装饰的对象,也是真正干活的对象
private final Cache delegate;
...
public SynchronizedCache(Cache delegate) {
this.delegate = delegate;
}
@Override
public synchronized void putObject(Object key, Object object) {
delegate.putObject(key, object);
}
@Override
public synchronized Object getObject(Object key) {
return delegate.getObject(key);
}
@Override
public synchronized Object removeObject(Object key) {
return delegate.removeObject(key);
}
...
}
只是简单地在添加/获取/清除等操作方法上加了synchronized关键字,至于性能方面就呵呵了。
为了更好地发挥二级缓存的功能,mybatis可以让开发者自定义二级缓存的实现:
注意,如果使用了自定义缓存,那么eviction,flushInterval,size,readOnly等配置将不会生效,这点在CacheBuilder#build方法中也能得到体现:
在自定义缓存时,我们可以使用redis等分布式缓存来存储数据,这里就不多分析了。
2.5 何时使用二级缓存
分析完了二级缓存,什么情况下适合使用二级缓存(mybits的默认实现)呢?
从前面的分析来看,二级缓存的默认实现有以下特点:
- 作用范围为全局(整个jvm进程)
- 保存在内存中(不支持分布式)
- 并发性能低(操作方法添加了
synchronized关键字)
结合以上所述,mybatis提供的二级缓存只 适合在单机、对并发要求不高的情况下使用。
需要注意的是,以上条件是针对mybats提供的二级缓存默认实现,我们还可以自定义二级缓存,以实现其在分布式、高并发环境的使用条件。
3. 一二级缓存特性汇总
3.1 缓存的配置汇总
cacheEnabled
在 mybatis 配置文件中,有一个settings节点,下面有一配置为cacheEnabled:
这个cacheEnabled开启或关闭的是哪个缓存呢?
让我们再回到Configuration#newExecutor(...)方法:
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
...
// 缓存装饰
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
...
}
这个cacheEnabled就是settings节点下cacheEnabled的值。从代码来看,当cacheEnabled为false时,CachingExecutor就不会创建了,而 CachingExecutor 是用来处理二级缓存的,这样一来,二缓存就不能使用了,即使在mapper.xml中加了<cache/>标签,也不能使用二级缓存了。
那一级缓存还能使用吗?从前面的分析来看,二级缓存的操作是在BaseExecutor中,因此并不会受这里的影响。
3.2 mybatis缓存对比
| 缓存类型 | 作用范围 | 保存位置 | 是否线程安全 | 性能 | 是否支持分布式 |
|---|---|---|---|---|---|
| 一级 | session | 内存 | 否 | 高 | 否 |
| 二级(默认实现) | 全局 | 内存 | 是 | 低 | 否 |
3.3 合理使用mybatis缓存
如何合理使用mybatis缓存呢?本人给出的建议是:
- 对于一级缓存,
localCacheScope设置为STATEMENT; - 对于二级缓存,如果使用
mybatis默认提供的,在单机、并发要求不高的情况下可以使用,多机情况下千万不要使用;如果使用自定义缓存,可以根据使用场景自由进行缓存实现。
本文原文链接:my.oschina.net/funcy/blog/… ,限于作者个人水平,文中难免有错误之处,欢迎指正!原创不易,商业转载请联系作者获得授权,非商业转载请注明出处。