深度分析Mybatis的一级、二级缓存
一、一级缓存的配置
MyBatis的全局配置文件中settings节点下配置:
<setting name="localCacheScope" value="SESSION"/>
默认情况下,启用了一级缓存,即本地的会话缓存,它仅仅对一个会话中的数据进行缓存。如果localCacheScope设置值为 STATEMENT,本地会话仅用在语句执行上,对相同 SqlSession 的不同调用将不会共享数据。
二、二级缓存的配置
MyBatis的全局配置文件中settings节点下配置:
<setting name="cacheEnabled" value="true"/>
默认是true,全局开启二级缓存,但是如果真是要使用二级缓存必须配置每个Mapper文件,在<mapper>节点下加如下节点:
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>
eviction:缓存清除策略
可用的清除策略有:
LRU – 最近最少使用:移除最长时间不被使用的对象。
FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
SOFT – 软引用:基于垃圾回收器状态和软引用规则移除对象。
WEAK – 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。
默认的清除策略是 LRU。
flushInterval(刷新间隔)属性可以被设置为任意的正整数,设置的值应该是一个以毫秒为单位的合理时间量。 默认情况是不设置,也就是没有刷新间隔,缓存仅仅会在调用语句时刷新。
size(引用数目)属性可以被设置为任意正整数,要注意欲缓存对象的大小和运行环境中可用的内存资源。默认值是 1024。
readOnly(只读)属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓存对象的相同实例。 因此这些对象不能被修改。这就提供了可观的性能提升。而可读写的缓存会(通过序列化)返回缓存对象的拷贝。 速度上会慢一些,但是更安全,因此默认值是 false。
在每个Mapper.xml配置文件中select、insert、update、delete语句中,也有对缓存更细粒度的配置
<select id="selectPerson" parameterType="int" parameterMap="deprecated" resultType="hashmap" resultMap="personResultMap" flushCache="false" useCache="true" timeout="10" fetchSize="256" statementType="PREPARED" resultSetType="FORWARD_ONLY">
flushCache: 将其设置为 true 后,只要语句被调用,都会导致本地缓存和二级缓存被清空,默认值:false。
useCache :将其设置为 true 后,将会导致本条语句的结果被二级缓存缓存起来,默认值:对 select 元素为 true。
<insert id="insertAuthor" parameterType="domain.blog.Author" flushCache="true" statementType="PREPARED" keyProperty="" keyColumn="" useGeneratedKeys="" timeout="20"> <update id="updateAuthor" parameterType="domain.blog.Author" flushCache="true" statementType="PREPARED" timeout="20"> <delete id="deleteAuthor" parameterType="domain.blog.Author" flushCache="true" statementType="PREPARED" timeout="20">
flushCache 将其设置为 true 后,只要语句被调用,都会导致本地缓存和二级缓存被清空,默认值:true(对于 insert、update 和 delete 语句)。
默认情况下,语句会这样来配置:
<select ... flushCache="false" useCache="true"/> <insert ... flushCache="true"/> <update ... flushCache="true"/> <delete ... flushCache="true"/>
三、一级缓存的实现
我们先使用默认的配置,默认是开启一级缓存,缓存范围是Session,查询方法的示例代码如下:
InputStream inputStream= Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory=new SqlSessionFactoryBuilder().build(inputStream);
try(SqlSession sqlSession=sqlSessionFactory.openSession()){
UserMapper userMapper=sqlSession.getMapper(UserMapper.class);
User user= userMapper.selectByPrimaryKey(9);
System.out.println(user);
user= userMapper.selectByPrimaryKey(9);
System.out.println(user);
}
我们先看sqlSessionFactory.openSession()这个方法:
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
//创建事务
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
//创建执行器
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
这里我们主要关心的是configuration.newExecutor方法:
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);
}
//这个cacheEnabled就是mybatis二级缓存的全局配置
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
//mybatis的插件链(暂不分析)
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
由上面的代码我们分析出如果不启用二级缓存,Executor 使用的是SimpleExecutor。然后我们回到前面的selectByPrimaryKey方法,调用过程为:
MapperProxy.invoke->MapperMethod.execute->DefaultSqlSession.selectOne->DefaultSqlSession.selectList->
最后到如下方法:
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
//这个MappedStatement就是根据相应Mapper.xml文件生成的配置信息,其中跟配置有关系的属性有:useCache = true,cache = null等,都跟二级缓存有关
MappedStatement ms = configuration.getMappedStatement(statement);
//这里调用的就是SimpleExecutor的父类BaseExecutor.query方法
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
然后我们分析BaseExecutor.query方法:
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
//主要是得到执行的sql语句(把#{}转换成?等)和参数信息
BoundSql boundSql = ms.getBoundSql(parameter);
//生成缓存key,根据ms的id和sql语句、环境等很多条件组装成key
//1429489964:600437586:com.tang.mappers.UserMapper.selectByPrimaryKey:0:2147483647:select\n \n \n user_id, user_type, user_name, mobile, email, user_pass, nick_name, create_time\n \n \n from tb_user where user_id = ?:9:development
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
然后Query方法就是我们主要分析实现一级缓存的方法: @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()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
//这里就会先从localCache查询是否存在缓存,这里的localCache是BaseExecutor的成员变量,类型是PerpetualCache,PerpetualCache的内部就是一个HashMap
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();
//如果设置localCacheScope的值为STATEMENT,则每次执行完会清掉缓存
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
然后我们看queryFromDatabase方法:
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
//先存放值等于EXECUTION_PLACEHOLDER的对象到HashMap中
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
//查询完删除
localCache.removeObject(key);
}
//重新存放从数据库查询到的值
localCache.putObject(key, list);
//这是sql有输出参数的情况,暂不考虑
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
最后返回到最开始的示例代码,执行第二次的selectByPrimaryKey方法,我们可以看到第二次查询直接从localCache.getObject(key)获取到对象返回。不过这还没完,在完成SqlSession的Close中Mybatis还会清掉这次会话产生的缓存。看DefaultSqlSession.close方法:
@Override
public void close() {
try {
//这里调用的是SimpleExecutor的Close方法,最后调用的是基类的BaseExecutor的Close方法
executor.close(isCommitOrRollbackRequired(false));
closeCursors();
dirty = false;
} finally {
ErrorContext.instance().reset();
}
}
BaseExecutor的Close方法如下:
@Override
public void close(boolean forceRollback) {
try {
try {
//回滚
rollback(forceRollback);
} finally {
if (transaction != null) {
transaction.close();
}
}
} catch (SQLException e) {
// Ignore. There's nothing that can be done at this point.
log.warn("Unexpected exception on closing transaction. Cause: " + e);
} finally {
transaction = null;
deferredLoads = null;
//这里清空一级缓存的引用
localCache = null;
localOutputParameterCache = null;
closed = true;
}
}
@Override
public void rollback(boolean required) throws SQLException {
if (!closed) {
try {
//清空前面产生的一级缓存的数据
clearLocalCache();
flushStatements(true);
} finally {
if (required) {
transaction.rollback();
}
}
}
}
到这里这个一级缓存的实现过程分析完成。
四、二级缓存的实现
我们继续在前面示例中的UserMapper.xml文件添加二级缓存的配置:
<cache />
只需要加上这句配置就好。
前面我们分析过sqlSessionFactory.openSession()这个方法,这里面之前创建的Executor类型是SimpleExecutor,当时配置好二级缓存后,类型就是CachingExecutor,然后我们根据一样的思路:
MapperProxy.invoke->MapperMethod.execute->DefaultSqlSession.selectOne->DefaultSqlSession.selectList->
进到CachingExecutor.query方法:
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
这个方法和之前分析一级缓存是一样的,主要看里面调用的query方法:
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
//<cache />配置的默认信息
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);
//没有二级缓存则直接调用SimpleExecutor的查询方法
if (list == null) {
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
//查询到数据后存入到二级临时缓存
//注:这里并没有直接放入到二级缓存,而是存放到临时缓存中,真正存入二级缓存的是在Session关闭时
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
//没有配置缓存直接调用SimpleExecutor的查询方法
//delegate通过创建CachingExecutor的时候可以知道delegate就是SimpleExecutor,用的是装饰模式,包装类
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
这里就是二级缓存实现的核心逻辑了,但这里特别注意的是tcm.putObject这个方法,我们进入这个方法:
public void putObject(Cache cache, CacheKey key, Object value) {
getTransactionalCache(cache).putObject(key, value);
}
然后再看如下方法:
private TransactionalCache getTransactionalCache(Cache cache) {
return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
}
看到这里我们主要分析的是TransactionalCache这个事务缓存类:
public class TransactionalCache implements Cache {
private final Cache delegate;
private boolean clearOnCommit;
private final Map<Object, Object> entriesToAddOnCommit;
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) {
// 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) {
//添加二级缓存调用的tcm.putObject方法,实际只是添加在entriesToAddOnCommit这个容器中
entriesToAddOnCommit.put(key, object);
}
@Override
public void clear() {
clearOnCommit = true;
entriesToAddOnCommit.clear();
}
//在DefaultSqlSession调用close方法时内部会调用TransactionalCacheManager的commit方法,最后才会调用TransactionalCache的commit方法提交缓存
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);
}
}
}
}
然后我们看下在一个DefaultSqlSession会话关闭时调用CachingExecutor的close方法:
@Override
public void close(boolean forceRollback) {
try {
//issues #499, #524 and #573
if (forceRollback) {
tcm.rollback();
} else {
//实际调用的是TransactionalCacheManager的commit
tcm.commit();
}
} finally {
delegate.close(forceRollback);
}
}
TransactionalCacheManager的commit方法如下:
public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit();
}
}
最终调用了TransactionalCache的commit方法。
总结:二级缓存只有关闭了会话后,才能获取到。

浙公网安备 33010602011771号