mybatis中的缓存分析
mybatis中存在一级缓存和二级缓存,其中一级缓存是默认开启并且不能关闭的,二级缓存需要手动开启。
一、一级缓存
mybatis中的一级缓存是SqlSession级别的缓存,只有在同一个sqlSession范围内的查询才会走一级缓存。
1.1 一级缓存的效果
先来看下一级缓存的效果
准备下面的测试代码
public class CacheTest {
public static void main(String[] args) {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUsername("root");
dataSource.setPassword("root");
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/stu_admin");
TransactionFactory transactionFactory = new JdbcTransactionFactory();
Environment environment = new Environment("dev",transactionFactory,dataSource);
Configuration configuration = new Configuration(environment);
configuration.addMapper(IUserDao.class);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
SqlSession sqlSession = sqlSessionFactory.openSession();
IUserDao userDao = sqlSession.getMapper(IUserDao.class);
User user1 = userDao.findById("1");
User user2 = userDao.findById("1");
System.out.println(user1==user2);
}
}
控制台会得到这样的输出
17:00:06.334 [main] DEBUG org.apache.ibatis.logging.LogFactory - Logging initialized using 'class org.apache.ibatis.logging.slf4j.Slf4jImpl' adapter.
17:00:06.528 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Opening JDBC Connection
17:00:06.621 [main] INFO com.alibaba.druid.pool.DruidDataSource - {dataSource-1} inited
17:00:06.856 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@167fdd33]
17:00:06.856 [main] DEBUG com.lyy.dao.IUserDao.findById - ==> Preparing: SELECT * FROM t_user WHERE user_id=?
17:00:06.887 [main] DEBUG com.lyy.dao.IUserDao.findById - ==> Parameters: 1(String)
17:00:06.965 [main] DEBUG com.lyy.dao.IUserDao.findById - <== Total: 1
true
可以看到执行了两次查询但mybatis只打印了一次sql,并且user1和user2是相等的,所以可以说明mybatis使用了缓存。但如果你重新创建一个sqlsession去执行查询就不会走缓存。
1.2 从一级缓存的源码分析一级缓存的命中原则
先给出一个mybatis中Executor的类图

mybatis的一级缓存是在BaseExecutor中实现的,mybatis通过sqlsession执行sql,sqlsession中会持有Executor。
public abstract class BaseExecutor implements Executor {
//省略其他
//这个属性就是一级缓存,它内部有一个map
protected PerpetualCache localCache;
}
我们通过研究DefaultSqlSession#selectList 方法来看下一级缓存的值是怎么被放进去的。
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
//调用执行器方法查询,所以关键在执行器上
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();
}
}
executor.query会调到BaseExecutor中
BaseExecutor
public abstract class BaseExecutor implements Executor {
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
//生成缓存key
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
//查询
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
//生成缓存key的方法
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
CacheKey cacheKey = new CacheKey();
cacheKey.update(ms.getId());//statemnetId
cacheKey.update(Integer.valueOf(rowBounds.getOffset()));
cacheKey.update(Integer.valueOf(rowBounds.getLimit()));//mybatis的分页参数
cacheKey.update(boundSql.getSql());//要发送给数据库的sql
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
// mimic DefaultParameterHandler logic
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
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);//sql参数
}
}
if (configuration.getEnvironment() != null) {
// issue #176
cacheKey.update(configuration.getEnvironment().getId());//mybaits的环境对象
}
return cacheKey;
}
}
从这个createCacheKey方法看出一级缓存的命名条件:
同一个statementID,使用的参数要一样,mybatis分页参数一样(这个一般不自己指定),发给数据库的sql要一样,
mybatis环境对象要一样。
1.3 一级缓存什么时候被放进去
继续看BaseExecutor中的query方法来看下一级缓存是什么时候被放进去的
public abstract class BaseExecutor implements Executor {
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++;
//这里是从一级缓存中查询
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) {
// issue #482
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;
}
}
所以一级缓存是在BaseExecutor的query方法中被放进去的,也就是说需要调用sqlsession.select开头的方法才会放入一级缓存。
1.4 一级缓存什么时候会被清空
当我们通过sqlsession执行update,delete,insert语句时最终都会调用org.apache.ibatis.session.defaults.DefaultSqlSession#update(java.lang.String, java.lang.Object)
方法,
public class DefaultSqlSession implements SqlSession {
public int update(String statement, Object parameter) {
try {
dirty = true;
MappedStatement ms = configuration.getMappedStatement(statement);
//调用执行器的update方法
return executor.update(ms, wrapCollection(parameter));
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error updating database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
}
一级缓存是在BaseExecutor中实现的,所以直接看BaseExecutor#update方法
public abstract class BaseExecutor implements Executor {
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);
}
}
所以一级缓存在通过sqlsession进行修改数据库数据的操作时会被清空
1.5 注意点
一级缓存是通过BaseExecutor执行的,SqlSession中持有Executor,所以一级缓存只有在同一个sqlsession的范围内会生效。
在spring整合mybatis的工程中如果不开启事务每次都会创建新的sqlsession,也就是一级缓存不生效。
如果开启了事务在同一个事务的范围内使用的是同一个sqlsession对象,这时一级缓存就生效了。
二、二级缓存
2.1开启二级缓存的方法
二级缓存默认不开启
二级缓存依赖的是org.apache.ibatis.mapping.MappedStatement#cache 属性,所以二级缓存的作用范围是在同一个statement范围内,而MappedStatement保存在org.apache.ibatis.session.Configuration#mappedStatements属性中,创建sqlsessionFactory时需要传递
Configuration对象,所以只要是根据同一个Configuration创建的sqlsessionFactory去创建sqlsession进行查询时都可以访问到同一个二级缓存,也就是二级缓存不仅可以跨sqlsession访问,也可以跨sqlsessionFactory访问。
接下来举例来验证下,那个mapper文件需要开启二级缓存,就给其中加上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="com.lyy.dao.IUserDao">
<cache></cache>
<select id="findById" resultType="com.lyy.domain.User">
SELECT * FROM t_user WHERE user_id=#{userId}
</select>
</mapper>
代码验证:
public class CacheTest {
public static void main(String[] args) {
//创建Configuration对象
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUsername("root");
dataSource.setPassword("root");
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/stu_admin");
TransactionFactory transactionFactory = new JdbcTransactionFactory();
Environment environment = new Environment("dev",transactionFactory,dataSource);
Configuration configuration = new Configuration(environment);
configuration.addMapper(IUserDao.class);
//创建第一个sqlsessionfactory
SqlSessionFactory sqlSessionFactory1 = new SqlSessionFactoryBuilder().build(configuration);
SqlSession sqlSession = sqlSessionFactory1.openSession();
User user3 = sqlSession.selectOne("com.lyy.dao.IUserDao.findById","1");
//只有执行了close或commit方法才会把查询结果存入二级缓存
sqlSession.close();
//使用sqlSessionFactory1创建新sqlsession来查询,验证二级缓存可以跨sqlsession访问
sqlSessionFactory1.openSession().selectOne("com.lyy.dao.IUserDao.findById","1");
//获取第2个sqlSessionFactory并创建sqlsession进行查询,验证二级缓存可以跨sqlsessionfactory访问
SqlSessionFactory sqlSessionFactory2 = new SqlSessionFactoryBuilder().build(configuration);
SqlSession sqlSession2 = sqlSessionFactory2.openSession();
User user4 = sqlSession2.selectOne("com.lyy.dao.IUserDao.findById","1");
}
}
2.2 二级缓存的源码分析
首先,一级缓存是在BaseExecutor中实现的,而二级缓存是在CachingExecutor中实现的,利用装饰者模式对
BaseExecutor进行了装饰,我们先看下这个装饰是在哪里完成的,Executor的创建是通过sqlsessionFactory完成的,所以看下DefaultSqlSessionFactory#openSession方法
public class DefaultSqlSessionFactory implements SqlSessionFactory {
@Override
public SqlSession openSession() {
// configuration.getDefaultExecutorType()默认值是simple类型的
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
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);
//这个newExecutor方法是在创建Executor,
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();
}
}
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor 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);
}
//这里根据Configuration.cacheEnabled属性来决定是否创建CachingExecutor来装饰executor
//在Configuration中cacheEnabled默认就是true
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
}
然后我们接着看下CachingExecutor中对二级缓存的实现
public class CachingExecutor implements Executor {
//被代理的执行器
private Executor delegate;
//缓存管理器
private TransactionalCacheManager tcm = new TransactionalCacheManager();
@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 CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
//生成key的方法直接调用的是被代理对象的,也就是BaseExecutor,所以二级缓存的命中条件和
// 一级缓存是一样的
return delegate.createCacheKey(ms, parameterObject, rowBounds, boundSql);
}
//使用二级缓存的查询方法
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
//从MappedStatement对象中获取cache,这个对象是在解析mapper文件的过程中创建的,只有我们的mapper文件
// 中有cache标签才会去创建这个对象
// 二级缓存中的数据最终就是存储在这个cache对象中的
Cache cache = ms.getCache();
if (cache != null) {
//看是否配置了查询之前刷新二级缓存
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
//如果是有输出参数的查询这个方法内部会抛出异常
ensureNoOutParams(ms, parameterObject, boundSql);
@SuppressWarnings("unchecked")
//从二级缓存中查询
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
//查不到从数据库查询
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
//把查询结果添加到缓存管理器中,但是并没有保存到二级缓存里边,只有调用了commit/close才会存
//要了解这个需要继续看tcm的实现,
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
//不使用二级缓存
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
@Override
public void close(boolean forceRollback) {
try {
//issues #499, #524 and #573
if (forceRollback) {
tcm.rollback();
} else {
//通过缓存管理器提交数据到二级缓存
tcm.commit();
}
} finally {
delegate.close(forceRollback);
}
}
@Override
public void commit(boolean required) throws SQLException {
delegate.commit(required);
//通过缓存管理器提交数据到二级缓存
tcm.commit();
}
}
上边的查询方法中出现了一个 TransactionalCacheManager,它是用来管理二级缓存的,通过它把数据存储到从
MappedStatement对象中获取到的cache对象中
public class TransactionalCacheManager {
//每一个mapper文件都对应一个cache,这个map中存放了每个mapper文件的cache对象,
//key是cache对象本身,value是用装饰者模式对cache的又一次装饰
private Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();
public Object getObject(Cache cache, CacheKey key) {
//先从transactionalCaches中获取到TransactionalCache再去查询
return getTransactionalCache(cache).getObject(key);
}
private TransactionalCache getTransactionalCache(Cache cache) {
//先根据cache看能不能找到装饰后的transactionalCache
TransactionalCache txCache = transactionalCaches.get(cache);
if (txCache == null) {
//找不到表示这个mapper文件是第一次被查询,给它创建一个cache的包装对象然后放到transactionalCaches中
txCache = new TransactionalCache(cache);
transactionalCaches.put(cache, txCache);
}
return txCache;
}
public void putObject(Cache cache, CacheKey key, Object value) {
//先获取transactionalCache,再调用先获取transactionalCache.put方法
getTransactionalCache(cache).putObject(key, value);
}
}
通过上边的代码我们了解到 TransactionalCacheManager中对MappedStatement.cache对象用TransactionalCache 又进行了一次包装,
public class TransactionalCache implements Cache {
// 被代理的对象,也就是mapperstatment对象中的cache属性
private Cache delegate;
private boolean clearOnCommit;
//暂存将要被提交到delegate中的数据,
private Map<Object, Object> entriesToAddOnCommit;
private Set<Object> entriesMissedInCache;
@Override
public void putObject(Object key, Object object) {
//可以看到当我们在CachingExecutor中调用putObj方法后数据并没有进入二级缓存
entriesToAddOnCommit.put(key, object);
}
//只有当这个方法被调用后数据才会进入二级缓存,这也就是为什么二级缓存要在调用了sqlsession
// 的commit或者close方法后才会被写入
public void commit() {
if (clearOnCommit) {
delegate.clear();
}
flushPendingEntries();
reset();
}
private void flushPendingEntries() {
//循环待放入的集合,把数据放入delegate中,也就是mapperstatment对象中持有的cache属性中
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);
}
}
}
}
二级缓存中多次使用了装饰者模式,在解析mapper文件生成MapperStatment对象时会给它的cache属性赋值,这个值这时候也已经是被装饰了很多次了,Cache本身是一个接口,
public interface Cache {
//...
}
每一次装饰就是创建一个新的实现类对象然后传入旧的实现类对象,这样cache就会增加一些新功能。
org.apache.ibatis.builder.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会被赋值给mapperstatment对象中的cache属性
currentCache = cache;
return cache;
}
2.2 二级缓存命中原则
通过上边的源码分析可以知道二级缓存的命中原则和一级缓存是一样的
2.3二级缓存什么时候被放进去
查询后并调用了sqlsession的commit或者close方法
2.4 二级缓存什么时候会被清空
CachingExecutor的update方法被执行后
2.5 使用二级缓存引发的问题
因为二级缓存在MappedStatement范围内生效,放入,清空这些操作都只会针对当前的MappedStatement,
所以如果两个MappedStatement操作同一张表,MappedStatement1先查询并缓存了数据,MappedStatement2再去修改数据,那MappedStatement1一直都会读取到旧的数据