SqlSession 使用门面模式,实际完成数据查询、更新操作的是Executor, 而Executor 分两大类,一类带cache和一类不带cache。
【DefaultSqlSession】
@Override
public int update(String statement, Object parameter) {
//.....
MappedStatement ms = configuration.getMappedStatement(statement);
// 实际调用executor.update 方法
return executor.update(ms, wrapCollection(parameter));
//....
}
@Override
public void commit(boolean force) {
// 实际调用executor.commit 方法
executor.commit(isCommitOrRollbackRequired(force));
//....
}
一缓存
一级缓存是SqlSession级别的缓存
<settings>
<!--配置默认的执行器, 默认SIMPLE, 可选项SIMPLE、REUSE、BATCH-->
<setting name = "defaultExecutorType" value = "SIMPLE" />
</settings>
如果没有配置defaultExecutorType默认 就是SimpleExecutor
, 继承自BaseExecutor, 采用了模板方法设计模式。
public abstract class BaseExecutor implements Executor {
protected Transaction transaction;
protected Executor wrapper;
protected PerpetualCache localCache;
}
BaseExecutor 存在一个属性PerpetualCache localCache
, 这个就是本地缓存,即一级缓存。PerpetualCache 很简单, 就是一个内部封装了Map<Object, Object> cache
实现了Cache接口的缓存类。
public class PerpetualCache implements Cache {
private final String id;
private final Map<Object, Object> cache = new HashMap<>();
}
执行查询时,先从缓存中获取,存在则返回,不存在则查询数据库并将查询结果放在缓存中。
【BaseExecutor】
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
//.....
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);
}
//...
return list;
}
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
//.....
try {
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
localCache.putObject(key, list);
//...
}
而且从上面的代码就可以看出一缓存是和Executor实例绑定的,而Executor是和SqlSession 实例绑定的。
【DefaultSqlSessionFactory.java】
public SqlSession openSession() {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
//.....
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
// 创建Executor实例
final Executor executor = configuration.newExecutor(tx, execType);
// 创建SQLSession实例
return new DefaultSqlSession(configuration, executor, autoCommit);
//........
}
在创建SqlSession时,就会同步创建一个Executor, 所以每一个SQLSession都会拥有一个独有的Executor, Executor上存在一级缓存的引用,SQLSession拥有Executor实例,这也是为什么,一级缓存是 SqlSession级别 的缓存。
Mybatis是怎么保证正确获取缓存结果的呢。 Mybatis将影响查询结果的数据都封装到CacheKey
中,同时重写了 equals方法。即PerpetualCache内部的Map,key类型是CacheKey
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
CacheKey cacheKey = new CacheKey();
// Mapper的namespace + 元素id
cacheKey.update(ms.getId());
// 伪分页,通常不会使用RowBounds分页
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
// 已解析的SQL语句,
cacheKey.update(boundSql.getSql());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
// mimic DefaultParameterHandler logic
// 解析参数
for (ParameterMapping parameterMapping : parameterMappings) {
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) {
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
cacheKey.update(value);
}
}
// 环境ID
if (configuration.getEnvironment() != null) {
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}
最终生成的结果, 前两个字段是hashcode和checksum。后续的字段便是id, start,end,sql, 参数,环境id
912292398:333997506:org.mybatis.mapper.UserMapper.test1:0:2147483647:SELECT * from test where id = ?:1:development
当参数SQL改变,参数改变那么就无法获取缓存数据。
二级缓存也是使用相同的规则,创建CacheKey
二级缓存
对于Mybatis版本: 3.5.9,默认是开启二级缓存的。早期版本可能默认是关闭的,可以手动开启
<settings>
<!--全局性地开启或关闭所有映射器配置文件中已配置的任何缓存-->
<setting name = "cacheEnabled" value = "true" />
</settings>
上面只是开启了全局的配置,真正要生效还需要配置自定义的缓存策略 只有在 mapper.xml 文件中设置了cache标签,才正真实现了二级缓存。
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>
可用的清除策略有:
- LRU – 最近最少使用:移除最长时间不被使用的对象。
- FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
- SOFT – 软引用:基于垃圾回收器状态和软引用规则移除对象。
- WEAK – 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。
当开启缓存并配置缓存后,查询操作的执行流程 二级缓存 -> 一级缓存 -> 数据库。
上面介绍过,SQLSession中包含一个Executor。它内部会根据defaultExecutorType 创建不同的类型。如果配置了cacheEnabled=true
,则会创建一个CachingExecutor。
【DefaultSqlSessionFactory】
final Executor executor = configuration.newExecutor(tx, execType);
【Configuration】
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) {
//如果开启了二级缓存,而创建CachingExecutor 实例
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
当配置了cacheEnabled=true
时,创建的Executor实际类型为CachingExecutor,它使用了装饰器模式。它内部维护了一个其他的Executor实现,通常是SimpleExecutor
public class CachingExecutor implements Executor {
private final Executor delegate;
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
}
select标签有两个属性配置缓存功能:
- useCache:将其设置为 true 后,将会导致本条语句的结果被二级缓存缓存起来,默认值:对 select 元素为 true。仅select标签有效。
- flushCache: 将其设置为 true 后,只要语句被调用,都会导致本地缓存和二级缓存被清空,默认值:false。其他更新标签(update、delete、insert)默认是true。
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
// 获取在Mapper文件中配置的缓存
Cache cache = ms.getCache();
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
// 从缓存中获取数据
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
// 如果找不到则委派给 SimpleExecutor(具体实现需要看配置)
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
// 如果没有配置二级缓存则 委派给 SimpleExecutor(具体实现需要看配置)
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
二级缓存的cache
那么问题来了MappedStatement中的cache 是什么,为什么可以保证不同SQLSession之间的数据共享。
如果是基于XML文件的方式, 那么在解析mapper.xml 文件的时候,会解析里面的cache元素, 创建一个Cache实例, 这个Cache实例被这个Mapper下的所有的查询操作共享的。
每一个Mapper.xml 都会有一个自己的MapperBuilderAssistant。经过下面的调用链,会对每个Mapper.xml(命名空间)生成独有的cache
【XMLMapperBuilder】
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();
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
}
}
【MapperBuilderAssistant】
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实例,内部调用了setStandardDecorators,此处就是二级缓存的最关键的代码。
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("Error building standard cache decorators. Cause: " + e, e);
}
}
经常上述操作,最终创建SynchronizedCache(实现了Cache接口) 实例, 显然二级缓存使用装饰器模式。可以看到上面有一个 SerializedCache, 所以POJO必须是可序列化的(实现Serializable接口)
Mapper.xml文件中的每一个SQL元素(select、update、delete、insert)都会解析为一个MappedStatement,同一个Mapper.xml文件下解析出的MappedStatement 里面都包含一个指向同一个cache。 所以二级缓存是Mapper(命名空间)级别的
Mybatis 缓存顶级类Cache,提供了获取和设置缓存结果的方法。
public interface Cache {
void putObject(Object key, Object value);
Object getObject(Object key);
这样调用putObject方法,就能从SynchronizedCache一步步的调用,最终保存到Perpetual的Map中。
上面是MappedStatement中存放的cache。实际上, CachingExecutor还会再包装一层,这就是CachingExecutor另一个属性的意义。它用于管理TransactionalCache 。
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;
private final Set<Object> entriesMissedInCache;
}
字面意义就是事务缓存, 所以对它的缓存结果仅有在事务提交后才可见。
【TransactionalCache】
@Override
public void putObject(Object key, Object object) {
entriesToAddOnCommit.put(key, object);
}
调用TransactionalCache的 putObject方法并没有将数据 委派给内部的Cache delegate
存储。仅当提交时才会将数据最终保存。
public void commit() {
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);
}
}
}
缓存失效
一级缓存失效
1、一级缓存是SQLSession级别的, 所以必须是同一个SqlSession
,才有可能生效
2、一级缓存 flushCache
必须为false
3、两次查询之间存在更新操作
4、手动调用了sqlSession.clearCache()
, 会导致缓存失效
5、cacheKey 不相同, 即参数、sql、environment等
二级缓存失效
1、没有开启二级缓存
2、没有提交 (sqlSession.commit())。 SqlSession 在未提交的时候,SQL 语句产生的查询结果还没有放入二级缓存中。
3、存在更新操作
4、存在多表关联,此时产生脏数据