文章目录
- 前言
- 第六章 MyBatis缓存
- 6.1 MyBatis缓存实现类
- 6.2 MyBatis一级缓存实现原理
- 6.2.1 一级缓存在查询时的使用
- 6.2.2 一级缓存在更新时的清空
- 6.3 MyBatis二级缓存的实现原理
- 6.3.1 实现的二级缓存的Executor类型
- 6.3.2 二级缓存在查询时使用
- 6.3.3 二级缓存在更新时清空
前言
缓存是MyBatis中非常重要的特性。合理使用缓存,可以减少数据库IO,显著提升系统性能;但在分布式环境下,如果使用不当则会带来数据一致性问题。
在上一节【MyBatis3源码深度解析(十六)SqlSession的创建与执行(三)Mapper方法的调用过程】中提到,MyBatis提供了一级缓存和二级缓存来提升查询效率,一级缓存在BaseExecutor类中完成,二级缓存在CachingExecutor类中完成。
第六章 MyBatis缓存
6.1 MyBatis缓存实现类
MyBatis缓存基于JVM堆内存实现,即所有的缓存数据都存放在Java对象中。 MyBatis通过Cache接口定义缓存对象的行为,其定义如下:
源码1:org.apache.ibatis.cache.Cache
public interface Cache {
// 获取缓存ID
String getId();
// 将一个Java对象添加到缓存中
void putObject(Object key, Object value);
// 根据key获取一个缓存对象
Object getObject(Object key);
// 根据key移除一个缓存对象
Object removeObject(Object key);
// 清空缓存
void clear();
// 获取缓存中存放的数据数量
int getSize();
// 3.2.6版本后不再使用
default ReadWriteLock getReadWriteLock() {
return null;
}
}
Cache接口采用装饰器模式设计。它有一个基本的实现类PerpetualCache。
源码2:org.apache.ibatis.cache.impl.PerpetualCache
public class PerpetualCache implements Cache {
private final String id;
// 内部维护了一个HashMap容器以保存缓存数据
private final Map<Object, Object> cache = new HashMap<>();
public PerpetualCache(String id) {
this.id = id;
}
@Override
public String getId() {
return id;
}
@Override
public int getSize() {
return cache.size();
}
@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();
}
@Override
public boolean equals(Object o) {
if (getId() == null) {
throw new CacheException("Cache instances require an ID.");
}
if (this == o) {
return true;
}
if (!(o instanceof Cache)) {
return false;
}
Cache otherCache = (Cache) o;
// 当两个缓存对象的ID相同时,即认为缓存对象相同
return getId().equals(otherCache.getId());
}
@Override
public int hashCode() {
if (getId() == null) {
throw new CacheException("Cache instances require an ID.");
}
// 仅以缓存对象的ID作为因子生成hashCode
return getId().hashCode();
}
}
由 源码2 可知,PerpetualCache的实现非常简单,内部仅仅维护了一个HashMap实例存放缓存对象。
需要注意的是,PerpetualCache类重写了Object类的equals()
方法和hashCode()
方法。由equals()
方法可知,当两个缓存对象的ID相同时,即认为缓存对象相同;由hashCode()
方法可知,仅以缓存对象的ID作为因子生成hashCode。
除了基础的PerpetualCache实现类,MyBatis还提供了许多其他的实现,对PerpetualCache类的功能进行增强。借助IDE,可以列出Cache的全部实现类:
MyBatis的一级缓存,使用的是PerpetualCache;二级缓存使用的是TransactionalCache。
6.2 MyBatis一级缓存实现原理
MyBatis一级缓存默认是开启的,而且不能关闭。
至于一级缓存不能关闭的原因,MyBatis核心开发人员做出了解释:MyBatis的一些关键特性(例如通过<association>和<collextion>建立级联映射、避免循环引用(circular references)、加速重复嵌套查询等)都是基于MyBatis一级缓存实现的,而且MyBatis结果集映射相关代码重度依赖CacheKey,所以MyBatis一级缓存不支持关闭。
在MyBatis主配置文件中,有这样一个配置属性:<setting name="localCacheScope" value="SESSION"/>
,用于控制一级缓存的级别。该属性的取值为SESSION、STATEMENT。
当指定localCacheScope参数值为SESSION时,缓存对整个SqlSession有效,只有执行DML语句(更新语句)时,缓存才会被清除;当指定localCacheScope参数值为STATEMENT时,缓存仅对当前执行的SQL语句有效,当语句执行完毕后,缓存就会被清除。
前面提到,一级缓存在BaseExecutor类中完成:
源码3:org.apache.ibatis.executor.BaseExecutor
public abstract class BaseExecutor implements Executor {
// ...
// 一级缓存对象
protected PerpetualCache localCache;
// 存储过程输出参数缓存
protected PerpetualCache localOutputParameterCache;
// ...
protected BaseExecutor(Configuration configuration, Transaction transaction) {
// ...
this.localCache = new PerpetualCache("LocalCache");
this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
// ...
}
}
由 源码3 可知,一级缓存使用PerpetualCache来实现,BaseExecutor中维护了两个PerpetualCache属性,localCache用于缓存MyBatis查询结果,localOutputParameterCache用于缓存存储过程输出参数。 这两个属性均在BaseExecutor的构造方法中初始化,并指定其ID。
MyBatis通过CacheKey对象来描述缓存的Key值。如果两次查询操作的CacheKey对象相同,就认为这两次查询执行的是相同的SQL语句。 CacheKey对象通过BaseExecutor的createCacheKey()
方法来创建。
源码4:org.apache.ibatis.executor.BaseExecutor
@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
CacheKey cacheKey = new CacheKey();
// Mapper的ID
cacheKey.update(ms.getId());
// 偏移量
cacheKey.update(rowBounds.getOffset());
// 查询条数
cacheKey.update(rowBounds.getLimit());
// SQL语句
cacheKey.update(boundSql.getSql());
// ......
// SQL语句中的参数
cacheKey.update(value);
// ......
// 配置文件中的<environment>标签的ID属性值
if (configuration.getEnvironment() != null) {
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}
由 源码4 可知,与CacheKey相关的因素包括:Mapper的ID、偏移量、查询条数、SQL语句、SQL语句中的参数、配置文件中的<environment>标签的ID属性值。
执行两次查询时,只有以上因素完全相同,才会认为这两次查询执行的是相同的SQL语句,才会直接从缓存中获取查询结果。
6.2.1 一级缓存在查询时的使用
解析来研究一下BaseExecutor的query()
方法中是如何使用一级缓存的。
源码5:org.apache.ibatis.executor.BaseExecutor
@Override
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());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
// 如果<select>标签的flushCache属性为true,则直接清除缓存
// 默认为false
clearLocalCache();
}
List<E> list;
try {
queryStack++;
// 从一级缓存中根据CacheKey获取缓存结果
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();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// 如果localCacheScope参数值为STATEMENT,缓存仅对当前执行的SQL语句有效,当语句执行完毕后,缓存就会被清除
clearLocalCache();
}
}
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);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
由 源码5 可知,在BaseExecutor的query()
方法中,首先会判断<select>标签的flushCache属性值,如果该属性为true,说明任何SQL语句被调用都需要先清除缓存,因此直接调用clearLocalCache()
方法清除缓存。该属性在<select>标签中的默认值为false。
接着根据CacheKey从一级缓存中查找是否有缓存对象。如果查找不到,则调用queryFromDatabase()
方法从数据库查询数据,并将查询结果保存到一级缓存中;如果查找到了,则直接返回。
最后,该方法还会判断主配置文件中的localCacheScope参数的值是否为STATEMENT,如果是STATEMENT,缓存仅对当前执行的SQL语句有效,因此当语句执行完毕后,直接调用clearLocalCache()
方法清除缓存。
6.2.2 一级缓存在更新时的清空
除了flushCache属性和localCacheScope属性可以控制一级缓存的清空,MyBatis会在执行任意更新语句时清空缓存,即BaseExecutor的update()
方法:
源码6:org.apache.ibatis.executor.BaseExecutor
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
// 清空缓存
clearLocalCache();
// 执行任意更新语句
return doUpdate(ms, parameter);
}
由 源码6 可知,MyBatis在调用doUpdate()
方法执行更新语句之前,会调用clearLocalCache()
方法清除缓存。
下面做个简单的测试。有以下单元测试代码,两次调用相同的selectAll()
方法:
@Test
public void testCache() throws IOException, NoSuchMethodException {
Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 第一次调用selectAll
userMapper.selectAll();
// 第二次调用selectAll
userMapper.selectAll();
}
借助Debug工具,可以发现第一次调用selectAll()
方法是一级缓存中没有数据,程序会调用queryFromDatabase()
方法从数据库查询数据,并存放到一级缓存中:
第二次调用selectAll()
方法时,一级缓存中已经保存了第一次查询的数据,这次直接从缓存中就可以取到数据,而不需要再去数据库查询:
6.3 MyBatis二级缓存的实现原理
6.3.1 实现的二级缓存的Executor类型
在 MyBatis的官方文档 中说,默认情况下,只启用了本地的会话缓存,它仅仅对一个会话中的数据进行缓存。要启用全局的二级缓存,只需要在你的SQL映射文件中添加一行:<cache/>
。
官方文档意思是,默认情况下,一级缓存是打开的,二级缓存是关闭的。要开启二级缓存,需要在SQL映射文件中添加一行:<cache/>
。
源码7:org.apache.ibatis.builder.xml.XMLConfigBuilder
private void settingsElement(Properties props) {
// ......
configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));
// ......
configuration.setDefaultExecutorType(ExecutorType.valueOf(props.getProperty("defaultExecutorType", "SIMPLE")));
// ......
}
由 源码7 可知,在解析MyBatis主配置文件时,cacheEnabled属性的默认值是true,即默认使用二级缓存(允许使用不代表开启);defaultExecutorType属性的默认值是SIMPLE,即默认创建的Executor类型是SimpleExecutor。
前面提到,二级缓存是在CachingExecutor类中完成的。因此,MyBatis在创建Executor时,会根据主配置文件中的cacheEnabled属性和defaultExecutorType属性来判断创建哪种Executor。
该创建工作在Configuration对象的工厂方法newExecutor()
中完成:
源码8:org.apache.ibatis.session.Configuration
// 默认创建SimpleExecutor
protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
// 默认开启二级缓存
protected boolean cacheEnabled = true;
public Executor newExecutor(Transaction transaction) {
return newExecutor(transaction, defaultExecutorType);
}
// 指定Executor类型
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : 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);
}
// 如果cacheEnabled属性为true,则创建CachingExecutor
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
return (Executor) interceptorChain.pluginAll(executor);
}
由 源码8 可知,MyBatis默认允许使用二级缓存,其实现在CachingExecutor类中。
源码9:org.apache.ibatis.executor.CachingExecutor
public class CachingExecutor implements Executor {
private final Executor delegate;
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
// ......
}
由 源码9 可知,CachingExecutor类中维护了一个TransactionalCacheManager实例,用于管理所有的二级缓存对象。
源码10:org.apache.ibatis.cache.TransactionalCacheManager
public class TransactionalCacheManager {
// 通过HashMap对象维护二级缓存对应的TransactionalCache实例
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
// 清空缓存
public void clear(Cache cache) {
getTransactionalCache(cache).clear();
}
// 获取二级缓存对应的TransactionalCache对象
// 根据根据缓存Key获取缓存对象
public Object getObject(Cache cache, CacheKey key) {
return getTransactionalCache(cache).getObject(key);
}
// 添加缓存
public void putObject(Cache cache, CacheKey key, Object value) {
getTransactionalCache(cache).putObject(key, value);
}
// 只有调用commit()方法后缓存对象才会真正添加到TransactionalCache中
public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit();
}
}
// 当调用rollback()方法时,写入操作将被回滚
public void rollback() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.rollback();
}
}
// 如果二级缓存对应的TransactionalCache获取不到,则创建一个新的
private TransactionalCache getTransactionalCache(Cache cache) {
return MapUtil.computeIfAbsent(transactionalCaches, cache, TransactionalCache::new);
}
}
由 源码10 可知,TransactionalCacheManager类中组合了一个HashMap对象,用于维护二级缓存实例对应的TransactionalCache对象。getObject()
和putObject()
方法均要先调用getTransactionalCache()
方法获取到TransactionalCache对象,再对TransactionalCache对象进行操作。
源码11:org.apache.ibatis.cache.decorators.TransactionalCache
public class TransactionalCache implements Cache {
// 缓存对象,内部组合了一个HashMap实例
private final Cache delegate;
// 一个标志,为true时表示提交数据时要清空缓存对象,默认为false
private boolean clearOnCommit;
// 保存即将存入二级缓存的数据
private final Map<Object, Object> entriesToAddOnCommit;
// 保存从二级缓存中没有取出的数据时的缓存Key
private final Set<Object> entriesMissedInCache;
public TransactionalCache(Cache delegate) {
this.delegate = delegate;
this.clearOnCommit = false;
this.entriesToAddOnCommit = new HashMap<>();
this.entriesMissedInCache = new HashSet<>();
}
@Override
public Object getObject(Object key) {
// 从二级缓存Cache对象中获取缓存数据
Object object = delegate.getObject(key);
if (object == null) {
// 从二级缓存中没有取出数据,则将这个缓存Key保存下来
entriesMissedInCache.add(key);
}
if (clearOnCommit) {
return null;
}
return object;
}
@Override
public void putObject(Object key, Object object) {
// 将要缓存的数据保存到HashMap集合(还没有真正加入到二级缓存中)
entriesToAddOnCommit.put(key, object);
}
// 提交数据
public void commit() {
if (clearOnCommit) {
delegate.clear();
}
// 刷新待处理的数据
flushPendingEntries();
// 清空两个容器的数据
reset();
}
// 回滚数据
public void rollback() {
unlockMissedEntries();
reset();
}
private void flushPendingEntries() {
// 将entriesToAddOnCommit容器中的数据一一添加到Cache对象中
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
delegate.putObject(entry.getKey(), entry.getValue());
}
// 遍历entriesMissedInCache容器中的缓存Key
// 去重后将Value值置空,添加到Cache对象中
for (Object entry : entriesMissedInCache) {
if (!entriesToAddOnCommit.containsKey(entry)) {
delegate.putObject(entry, null);
}
}
}
private void reset() {
clearOnCommit = false;
entriesToAddOnCommit.clear();
entriesMissedInCache.clear();
}
private void unlockMissedEntries() {
// 遍历entriesMissedInCache容器中的缓存Key
// 逐一从Cache对象中移除
for (Object entry : entriesMissedInCache) {
try {
delegate.removeObject(entry);
} // catch ...
}
}
}
由 源码11 可知,TransactionalCache类对Cache类进行了增强,除了组合一个Cache对象以保存缓存对象,还分别组合了一个HashMap容器和一个HashSet容器,分别用于保存即将存入Cache对象的数据,以及保存从二级缓存中没有取出的数据时的缓存Key。
(1)如果要将数据存入二级缓存,则调用putObject()
方法。 该方法将要缓存的数据保存到entriesToAddOnCommit容器。注意此时数据还没有真正保存到Cache对象中。
要想数据真正保存到Cache对象中,还需要调用commit()
方法,该方法会将entriesToAddOnCommit容器中的数据一一添加到Cache对象中,还会以entriesMissedInCache容器中的缓存Key(去重后的)也添加到Cache对象中,只是它的Value值为null。
这样做的目的在于,即使某个缓存Key的查询结果为null,也要缓存,下次相同的缓存Key查询时,直接返回null即可。
(2)如果要将数据从二级缓存中取出来,则调用getObject()
方法,该方法会根据缓存Key从Cache对象中取数据,如果没有取到数据,则将当前缓存Key保存到entriesMissedInCache容器中;取到数据则直接返回。
6.3.2 二级缓存在查询时使用
执行查询SQL语句时,会调用CachingExecutor类的query()
方法中:
源码12:org.apache.ibatis.executor.CachingExecutor
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler)
throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
// 构造缓存Key
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler,
CacheKey key, BoundSql boundSql) throws SQLException {
// 获取MappedStatement对象中维护的二级缓存对象
Cache cache = ms.getCache();
if (cache != null) {
// 判断是否需要刷新二级缓存
flushCacheIfRequired(ms);
// 主配置文件中的cacheEnabled属性为true时使用二级缓存
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);
}
private void flushCacheIfRequired(MappedStatement ms) {
Cache cache = ms.getCache();
if (cache != null && ms.isFlushCacheRequired()) {
// 如果<select>标签的flushCache属性为true,则直接清除缓存。默认为false
tcm.clear(cache);
}
}
由 源码12 可知,CachingExecutor类的query()
方法的逻辑如下:
(1)调用createCacheKey()
方法创建缓存Key对象;
(2)调用MappedStatement对象的getCache()
方法获取维护的二级缓存对象。如果有,则进入使用二级缓存的逻辑,如果没有则直接从数据库中查询。
(3)判断是否需要刷新二级缓存。如果<select>标签的flushCache属性为true(<select>标签中默认为false),则直接调用TransactionalCacheManager的clear()
方法清除缓存。
(4)判断主配置文件中的cacheEnabled属性,为true时真正使用二级缓存。
(5)根据缓存Key从二级缓存中获取缓存数据,如果获取到了则直接返回,如果二级缓存中没有获取到数据,则从数据库中查询,再将数据库查询结果保存到二级缓存中。
6.3.3 二级缓存在更新时清空
和一级缓存一样,二级缓存也会在执行更新语句时被清空,即CachingExecutor类的update()
方法:
源码13:org.apache.ibatis.executor.CachingExecutor
@Override
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
// 必要时清空二级缓存
flushCacheIfRequired(ms);
// 执行更新语句
return delegate.update(ms, parameterObject);
}
由 源码13 可知,在执行更新SQL语句之前,会根据<select|insert|update|delete>标签的flushCache属性来判断是否需要清空二级缓存。
而<select>标签的flushCache属性值默认为false,<insert|update|delete>标签的flushCache属性值默认为true,因此在执行更新语句时会清空二级缓存。
…
下面做个简单的测试。沿用上面的单元测试代码,两次调用相同的selectAll()
方法,不一样的是,查询后需要手动调用commit()
方法(SELECT语句不会自动调用commit()
方法):
@Test
public void testCache() throws IOException, NoSuchMethodException {
Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 第一次调用selectAll并手动提交
userMapper.selectAll();
sqlSession.commit();
// 第二次调用selectAll并手动提交
userMapper.selectAll();
sqlSession.commit();
}
另外,还需要在SQL映射文件中添加一行:<cache/>
。
借助Debug工具,可以发现第一次调用selectAll()
方法时二级缓存中没有数据,程序会调用query()
方法从数据库查询数据,并存放到二级缓存中:
第二次调用selectAll()
方法时,二级缓存中已经保存了第一次查询的数据,这次直接从缓存中就可以取到数据,而不需要再去数据库查询:
注意,如果单元测试中没有sqlSession.commit();
这一行代码,会发现数据不会保存到二级缓存中。
…
本节完,更多内容请查阅分类专栏:MyBatis3源码深度解析