mybatis篇
一级缓存的作用域是Sqlsession级别的,也就是说不同的Sqlsession是不会走一级缓存的,那么如果需要跨Sqlsession的缓存,就需要使用到二级缓存了。
二级缓存的话默认是关闭的,所以需要我们开启,开启的方式官网也有介绍,需要在mybatis-config.xml核心配置文件中开启二级缓存功能,并且我们mapper.xml中也需要加入<cache/>标签,二者缺一不可,后面我们看源码就能知道为啥这两个缺一不可。
<settings>
<!-- 二级缓存 -->
<setting name="cacheEnabled" value="true"/>
</settings>
<cache/>
先来看个例子
public void cache() {
String resource = "mybatis-config.xml";
InputStream inputStream = null;
try {
inputStream = Resources.getResourceAsStream(resource);
} catch (IOException e) {
e.printStackTrace();
}
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
MybatisUserinfoMapper userinfoMapper = sqlSession.getMapper(MybatisUserinfoMapper.class);
MybatisUserinfoModel model = userinfoMapper.selectByKey(4);
System.out.println(model);
System.out.println("=====================");
// 执行了查询语句
SqlSession session = sqlSessionFactory.openSession();
MybatisUserinfoMapper mapper = session.getMapper(MybatisUserinfoMapper.class);
MybatisUserinfoModel userinfoModel = mapper.selectByKey(4);
System.out.println(userinfoModel);
session.close();
sqlSession.close();
}
执行结果很意外,为什么二级缓存的功能都开启了,结果sql还是执行了2次,并没有走缓存,其实,二级缓存还有一个要注意的点那就是必须要提交事务二级缓存才会保存记录,因为已经是跨SqlSession共享缓存了,所以事务必须要提交,否则会读取到因混滚导致的错误数据。
public void cache() {
String resource = "mybatis-config.xml";
InputStream inputStream = null;
try {
inputStream = Resources.getResourceAsStream(resource);
} catch (IOException e) {
e.printStackTrace();
}
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
MybatisUserinfoMapper userinfoMapper = sqlSession.getMapper(MybatisUserinfoMapper.class);
MybatisUserinfoModel model = userinfoMapper.selectByKey(4);
System.out.println(model);
System.out.println("=====================");
// 需要提交事务
sqlSession.commit();
// 执行了查询语句
SqlSession session = sqlSessionFactory.openSession();
MybatisUserinfoMapper mapper = session.getMapper(MybatisUserinfoMapper.class);
MybatisUserinfoModel userinfoModel = mapper.selectByKey(4);
System.out.println(userinfoModel);
session.close();
sqlSession.close();
}
可以看到,第二次查询没有走sql,直接从二级缓存中获取的值。
这里我们就不再具体分析sqlsession的获取和mapper的获取了,具体可以看之前的文章mybatis一级缓存原理
有个地方需要注意,二级缓存的Sqlsession中的Executor实际上是CachingExecutor
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();
}
}
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的值,否则不会存在二级缓存
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
我们知道getMapper最终的执行都会走到MapperProxy类中的invoker方法,具体就来分析这个类。
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (isDefaultMethod(method)) {
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}
@Override
public <T> T selectOne(String statement, Object parameter) {
// Popular vote was to return null on 0 results and throw exception on too many.
List<T> list = this.<T>selectList(statement, parameter);
if (list.size() == 1) {
return list.get(0);
} else if (list.size() > 1) {
throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
} else {
return null;
}
}
@Override
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();
}
}
最后来到了重点的地方
@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);
}
CacheKey我们可以认为他就是每个方法对应的一个唯一标识符。
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
// mapper.xml中我们配置的<cache/>标签
Cache cache = ms.getCache();
if (cache != null) {
// 如果flushCache配置为了true,那么就会清空一级缓存和二级缓存
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, 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);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
这里我们就可以看出为什么之前两者必须要配置,cacheEnable开启了才会用CachingExecutor包装一下BaseExecutor,而<cache/>标签只有配置了才会走缓存的逻辑
List<E> list = (List<E>) tcm.getObject(cache, key);
这里的tcm
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
public Object getObject(Cache cache, CacheKey key) {
return getTransactionalCache(cache).getObject(key);
}
private TransactionalCache getTransactionalCache(Cache cache) {
TransactionalCache txCache = transactionalCaches.get(cache);
if (txCache == null) {
txCache = new TransactionalCache(cache);
transactionalCaches.put(cache, txCache);
}
return txCache;
}
public class TransactionalCache implements Cache {
private static final Log log = LogFactory.getLog(TransactionalCache.class);
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<Object, Object>();
this.entriesMissedInCache = new HashSet<Object>();
}
@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;
}
}
到这,我们就差不多揭开了二级缓存的秘密,重要的还是<cache/>这个标签,因为它的存在就对应着每个mapper.xml中的一个具体Cache类,而这个类在每个mapper.xml中又是同一个,所以最终的值是放入了Cache类中,key为CacheKey,value就是sql执行的结果。
至于为什么需要事务提交才能命中二级缓存,我们看下put方法就知道
@Override
public void putObject(Object key, Object object) {
entriesToAddOnCommit.put(key, object);
}
这里的putObject并没有真正的把值存入Cache中,而是存入了待提交的Map中,所以再来看下commit做了什么
@Override
public void commit(boolean force) {
try {
executor.commit(isCommitOrRollbackRequired(force));
dirty = false;
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error committing transaction. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
@Override
public void commit(boolean required) throws SQLException {
delegate.commit(required);
tcm.commit();
}
具体看tcm.commit()
public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit();
}
}
而这里可以看到此处会遍历所有的TransactionCache并执行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);
}
}
}
真相就出来了,会遍历待提交的Map然后把里面的值都存入Cache中,所以后面的查询就能直接从Cache中拿到值了。
总结
二级缓存先会把Sqlsession中的Executor包装成包装成CacheingExecutor,所有的sql都会经过这个类,而该类通过mapper.xml中配置的唯一<cache/>标签生成的Cache类存放每个方法执行的结果