mybatis缓存介绍
缓存的作用时为了提高查询效率,减少访问数据库访问次数。mybatis有一级缓存和二级缓存。执行增删改操作commit时会清空缓存。
一级缓存
一级缓存是对sqlSession的,就是用同一个sqlSession查询两次相同的sql语句时,第一次在数据库中查询并将结果存放在session的缓存中,第二次查询直接在缓存中取结果而不必在查一次数据库。
//一级缓存相关
@Test
public void testFindbyId(){
User user=userDao.findById(46);
System.out.println(user);
System.out.println(userDao.findById(46));
}
控制台输出
2020-08-31 16:47:03,033 4573506 [ main] DEBUG ansaction.jdbc.JdbcTransaction - Opening JDBC Connection
2020-08-31 16:47:05,583 4576056 [ main] DEBUG source.pooled.PooledDataSource - Created connection 1439632660.
2020-08-31 16:47:05,585 4576058 [ main] DEBUG ansaction.jdbc.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@55cf0d14]
2020-08-31 16:47:05,645 4576118 [ main] DEBUG cn.thg.dao.UserDao.findById - ==> Preparing: select * from user where id=?
2020-08-31 16:47:06,743 4577216 [ main] DEBUG cn.thg.dao.UserDao.findById - ==> Parameters: 46(Integer)
2020-08-31 16:47:08,759 4579232 [ main] DEBUG cn.thg.dao.UserDao.findById - <== Total: 1
User{id=46, username='小马宝莉', sex='女', birthday=Tue Mar 24 15:47:57 CST 2020, address='北京修正'}
2020-08-31 16:47:17,980 4588453 [ main] DEBUG cn.thg.dao.UserDao - Cache Hit Ratio [cn.thg.dao.UserDao]: 0.0
User{id=46, username='小马宝莉', sex='女', birthday=Tue Mar 24 15:47:57 CST 2020, address='北京修正'}
2020-08-31 16:47:18,053 4588526 [ main] DEBUG ansaction.jdbc.JdbcTransaction - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@55cf0d14]
2020-08-31 16:47:18,055 4588528 [ main] DEBUG ansaction.jdbc.JdbcTransaction - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@55cf0d14]
一级缓存只能作用在同一个session中,跨session无效。每开启一个sqlsession都会有它们自己的缓存。
二级缓存
二级缓存是对mapper而言的,就是无论是不是同一个sqlsession,查询两次相同的sql时,第一次会从数据库查并将结果存到缓存中,第二次直接从缓存中取。二级缓存时跨session的。
mybatis默认开启一级缓存,二级缓存需要手动开启。
关于开启二级缓存有两步,第一步是配置cacheEnable=true,第二步是在mapper.xml中配置。具体步骤百度一下吧,这里不多赘述。
mybatis查询过程涉及缓存的源码
@Test
public void testFindbyId(){
User user=userDao.findById(46);
System.out.println(user);
System.out.println(userDao.findById(46));
}
userDao是sqlSession.getMapper(UserDao.class)得来的,是一个代理对象(具体如何代理先不讲,本篇主要讲缓存,有兴趣可以自行去翻一下源码),会调用MapperProxy类中的invoke方法,然后经过一系列的调用会执行到DefaultSqlSession类的selectList方法,selectList源码如下:
//DefaultSqlSession的方法
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
List var5;
try {
MappedStatement ms = this.configuration.getMappedStatement(statement);
//调用执行器的查询方法,如果cacheEnable=true,执行器的类型是CachingExecutor,否则是SimpleExecutor
var5 = this.executor.query(ms, this.wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception var9) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + var9, var9);
} finally {
ErrorContext.instance().reset();
}
return var5;
}
关于执行器,这里使用SimpleExecutor,如果cacheEnable=true,在SqlSessionFactory类中会通过装饰者模式将SimpleExecutor包装成CachingExecutor。文章后面再简单看一下SqlSessionFactory,这里直接进入CachingExecutor。
//CachingExecutor的方法
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
CacheKey key = this.createCacheKey(ms, parameterObject, rowBounds, boundSql);
return this.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
调用this.query(…)后进入CachingExecutor的如下方法:
//CachingExecutor的方法
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
//获取二级缓存
Cache cache = ms.getCache();
//如果mapper中加上了<cache/>标签,cache就不会为空,就会进入这个分支
if (cache != null) {
this.flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
this.ensureNoOutParams(ms, boundSql);
//尝试从二级缓存中获取结果
List<E> list = (List)this.tcm.getObject(cache, key);
if (list == null) {
//获取不到,调用SimpleExecutor的查询方法
//(可能会从数据库查,也可能会从一级缓存拿结果)
list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
this.tcm.putObject(cache, key, list);
}
return list;
}
}
//如果mapper中没有<cache/>标签,执行下面语句
return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
终于看到了Cache,其实Cache里面主要是通过key-value结构进行数据缓存的。到这里如果从二级缓存中拿到结果,那这次查询就结束返回了。
delegate是一个Executor,一般实现类是SimpleExecutor。
假设我们启动了二级缓存并且没有从二级缓存中获得结果(为了可以看到一级缓存的相关源码)。
继续调用this.delegate.query(…),因为SimpleExecutor没有覆写父类的query方法,故而调用到了父类BaseExecutor的query方法:
//BaseExecutor的query方法
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 (this.closed) {
throw new ExecutorException("Executor was closed.");
} else {
if (this.queryStack == 0 && ms.isFlushCacheRequired()) {
this.clearLocalCache();
}
List list;
try {
++this.queryStack;
//前面传参时给resultHandler传了Executor.NO_RESULT_HANDLER,为空。
//尝试从一级缓存localCache中取结果
list = resultHandler == null ? (List)this.localCache.getObject(key) : null;
if (list != null) {
this.handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
//一级缓存没有数据,再访问数据库,后面就是关于访问数据库的了。
list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
--this.queryStack;
}
if (this.queryStack == 0) {
Iterator var8 = this.deferredLoads.iterator();
while(var8.hasNext()) {
BaseExecutor.DeferredLoad deferredLoad = (BaseExecutor.DeferredLoad)var8.next();
deferredLoad.load();
}
this.deferredLoads.clear();
if (this.configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
this.clearLocalCache();
}
}
return list;
}
}
至此,启动了二级缓存(一级缓存默认开启)的查询过程就找完了。
那么关于一级缓存和二级缓存主要放在哪里呢?
mybatis缓存位置
如果看博客时跟着用idea进行debug的人不难发现,二级缓存时存放在Configuration的MappedStatement这个类中的,一级缓存是放在SqlSession的执行器executor中的。
二级缓存的位置
//CachingExecutorf的方法
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
//获取二级缓存
Cache cache = ms.getCache();
...
...
}
二级缓存放在MappedStatement类中,那么MappedStatement是怎么来的呢,是什么东西呢?
往回看看调用上面方法的类:
//DefaultSqlSession的方法
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
List var5;
try {
MappedStatement ms = this.configuration.getMappedStatement(statement);
var5 = this.executor.query(ms, this.wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
...
...
}
原来MappedStatement是从Configuration中拿到的,Configuration想必大家都知道是mybatis配置文件的解析出来的信息吧。MappedStatement对应解析出来的mapper.xml文件中的相关信息,一个MappedStatement对应xml的一条sql。
看看Configuration的相关代码:
//Configuration的代码
protected final Map<String, MappedStatement> mappedStatements;
public MappedStatement getMappedStatement(String id) {
return this.getMappedStatement(id, true);
}
public MappedStatement getMappedStatement(String id, boolean validateIncompleteStatements) {
if (validateIncompleteStatements) {
this.buildAllStatements();
}
return (MappedStatement)this.mappedStatements.get(id);
}
通过全限定类名加方法名作为id获取对应dao方法的MappedStatement对象。
一级缓存的位置
通过上面找到一级缓存的代码,ctrl+左键进去看看就知道:
public abstract class BaseExecutor implements Executor {
...
...
protected PerpetualCache localCache;
...
...
}
PerpetualCache是Cache接口的实现类,里面是封装了key-value结构:
public class PerpetualCache implements Cache {
private final String id;
private Map<Object, Object> cache = new HashMap();
...
}
刷新缓存
一级缓存会在执行增删改commit或者rollback方法、update方法、close方法之后清空。
//BaseExecutor的方法
public void commit(boolean required) throws SQLException {
if (this.closed) {
throw new ExecutorException("Cannot commit, transaction is already closed");
} else {
//清空缓存
this.clearLocalCache();
this.flushStatements();
if (required) {
this.transaction.commit();
}
}
}
//BaseExecutor的方法
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (this.closed) {
throw new ExecutorException("Executor was closed.");
} else {
//清空缓存
this.clearLocalCache();
return this.doUpdate(ms, parameter);
}
}
//BaseExecutor的方法
public void rollback(boolean required) throws SQLException {
if (!this.closed) {
try {
//清空缓存
this.clearLocalCache();
this.flushStatements(true);
} finally {
if (required) {
this.transaction.rollback();
}
}
}
}
//BaseExecutor的方法
public void close(boolean forceRollback) {
try {
try {
this.rollback(forceRollback);
} finally {
if (this.transaction != null) {
this.transaction.close();
}
}
} catch (SQLException var11) {
log.warn("Unexpected exception on closing transaction. Cause: " + var11);
} finally {
this.transaction = null;
this.deferredLoads = null;
//指向空,一样是清空缓存
this.localCache = null;
this.localOutputParameterCache = null;
this.closed = true;
}
}
装饰者模式相关代码
写得有点累了,看代码注释跟进去就好。
//DefaultSqlSessionFactory的方法
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
DefaultSqlSession var8;
try {
Environment environment = this.configuration.getEnvironment();
TransactionFactory transactionFactory = this.getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
//进入这个方法就可以看到
Executor executor = this.configuration.newExecutor(tx, execType);
var8 = new DefaultSqlSession(this.configuration, executor, autoCommit);
} catch (Exception var12) {
this.closeTransaction(tx);
throw ExceptionFactory.wrapException("Error opening session. Cause: " + var12, var12);
} finally {
ErrorContext.instance().reset();
}
return var8;
}
//Configuration的方法
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? this.defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Object 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 (this.cacheEnabled) {
//如果cacheEnable=true,装饰成CachingExecutor
executor = new CachingExecutor((Executor)executor);
}
Executor executor = (Executor)this.interceptorChain.pluginAll(executor);
return executor;
}
结束!