视频地址:https://www.bilibili.com/video/BV1nP411A7Gu
MyBatis的缓存是一个常见的面试题
- 一级缓存的作用域为何是 sqlSession、二级缓存的作用域为何是 mapper
- 怎么理解 一、二级缓存都是基于 PerpetualCache 的HashMap的本地缓存
- 为什么一级缓存无法被关闭
- 怎么才能使用二级缓存?如果使用了二级缓存一级缓存还有用么
- 如果一级缓存不可以关闭,那在分布式的系统中,如何解决数据一致性问题
- 如果开启了二级缓存,那缓存的命中顺序将是如何呢
注:这个是基于源码的角度来讲解缓存,如果对MyBatis源码不熟悉的小伙伴建议先看看这个 MyBatis 执行原理,源码解读
问题
在写这篇博客的时候突然想到了一个简单的问题,如果你可以回答出来,那么MyBatis的一、二级缓存你将吃透了
两次获取的结果是否一致呢?
mapper.getId()
// 打断点 ,然后去修改数据库的数据
mapper.getId()
前置
前置的内容在上一章执行原理里面都已经讲过了,这里只提出一些个关键点,帮助理解。
- XML 里面的每一个 select、insert、update、dalete 标签都会被解析成一个
MappedStatement
- 每个mapper都会生成一个
MapperProxy
代理对象,当我们执行某个方法的时候就会调用代理对象的 invoke 方法
SqlSession
在 MybatisAutoConfiguration
自动注入的时候会创建 SqlSessionFactory
和 SqlSessionTemplate
SqlSessionFactory的最终实现类是 DefaultSqlSessionFactory
@Bean
@ConditionalOnMissingBean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
ExecutorType executorType = this.properties.getExecutorType();
if (executorType != null) {
return new SqlSessionTemplate(sqlSessionFactory, executorType);
} else {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
创建sqlSessionTemplate的最终方法如下,它其实也是代理实现的
它在这里加了一个拦截器,这个很重要下面说 new SqlSessionInterceptor()
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
PersistenceExceptionTranslator exceptionTranslator) {
notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
notNull(executorType, "Property 'executorType' is required");
this.sqlSessionFactory = sqlSessionFactory;
this.executorType = executorType;
this.exceptionTranslator = exceptionTranslator;
this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),
new Class[] { SqlSession.class }, new SqlSessionInterceptor());
}
MapperProxy
MapperProxy是基于 SqlSession来创建的,通过上面的自动注入我们得知这里的 SqlSession是 SqlSessionTemplate
这个SqlSessionTemplate 只是用来创建代理时候的一个模板,真正去执行的SqlSession 是DefaultSqlSession
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
invoke
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
return mapperMethod.execute(sqlSession, args);
}
execute
sqlSession 就是创建我们代理对象的 SqlSessionTemplate
excute 就是去执行我们的增删改查逻辑,简化代码
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:
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());
}
return result;
}
SqlSessionInterceptor
我们的 SqlSessionTemplate 创建的时候,加入了这个拦截器,所以正式去执行方法的时候是会先执行这个拦截器
- MapperProxy 基于 SqlSessionTemplate 来创建的
- SqlSessionTemplate 是基于反射创建的,在创建的时候加入了拦截器 SqlSessionInterceptor
- 当我们执行 MapperProxy 的 invoke 方法时候,会先进入这个拦截器
- 第一步获取 sqlSession,所以最终的sqlSession没并不是使用的 SqlSessionTemplate
- 最后会关闭session
简化后的SqlSessionInterceptor
private class SqlSessionInterceptor implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
try {
Object result = method.invoke(sqlSession, args);
if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
// force commit even on non-dirty sessions because some databases require
// a commit/rollback before calling close()
sqlSession.commit(true);
}
return result;
} catch (Throwable t) {
Throwable unwrapped = unwrapThrowable(t);
throw unwrapped;
} finally {
if (sqlSession != null) {
// 【关闭session】
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
}
}
}
}
getSqlSession (非常重要)
注: MyBatis的一级缓存是 session,其实就是这个SqlSession,不要和你之前理解的cookie、session 中的session混为一谈了
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType,
PersistenceExceptionTranslator exceptionTranslator) {
notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);
// TransactionSynchronizationManager 是事物管理器
// 这里你可以理解成当前是否开启事物,如果开启了就会把session注册进去,从而保证这个线程获取的是同一个session 【这个holder是从 ThreadLocal 里面获取的】
SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
SqlSession session = sessionHolder(executorType, holder);
if (session != null) {
return session;
}
LOGGER.debug(() -> "Creating a new SqlSession");
// 创建新的session
session = sessionFactory.openSession(executorType);
// 把holder 注册到TransactionSynchronizationManager
// TransactionSynchronizationManager 是事物管理器,【如果你没开启事物那就不会注册成功的】
registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);
return session;
}
openSession
openSession 这个方法一路进去最终的实现如下
- 创建 Executor, 这个就是最终的sql执行器,和缓存相关的
- 最终创建的 SqlSession 其实是 DefaultSqlSession
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();
}
}
newExecutor
它这里创建什么类型的Executor,我们不用关注,我们的目的是缓存
我们可以看到当 CachingExecutor == true
的时候,会把executor装饰成 CachingExecutor ,CachingExecutor 就是二级缓存了 (ps:这里使用的是装饰器模式)
- cacheEnabled 默认就是 true
- CachingExecutor 是使用二级缓存的条件之一,后面我们会看到另外一个条件是 mapper 里面增加
<cache></cache>
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) {
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
selectOne/ selectList
获取到真正的 sqlSession 之后,就会去执行我们的查询,这selectList 有很多的重载
@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.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) {
return this.selectList(statement, parameter, RowBounds.DEFAULT);
}
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
return selectList(statement, parameter, rowBounds, Executor.NO_RESULT_HANDLER);
}
最终的调用如下
private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, handler);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
CachingExecutor#query (重要)
executor 的创建看上面的 newExecutor 方法,我们知道最终创建的是 CachingExecutor
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// 对sql进行解析 参看上一篇博客
BoundSql boundSql = ms.getBoundSql(parameterObject);
// 生成缓存key 参看上一篇博客
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
// 从MappedStatement 里面获取缓存二级缓存,【这也就是为什么说 二级缓存是基于Mapper的】
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);
if (list == null) {
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
// 刚刚我们也看到创建的CachingExecutor 是基于装饰器模式创建的,里面真正的是 SimpleExecutor
// SimpleExecutor extends BaseExecutor 所以这里的query 方法是BaseExecutor的
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
BaseExecutor#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.");
}
// queryStack 这个字段暂且不看,它默认就是0
// isFlushCacheRequired 这字段表示是否要清空缓存,
// 1.它是从ms里面获取的
// 2.虽然我们的一级缓存不能被关闭,但是可以让它失效,查缓存之前删除缓存就好了
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
// resultHandler 上面就是传递的null,所以这里一定会走缓存
// 因为这句代码一定会走,这就是【一级缓存不能关闭的原因】
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();
// 如果当前缓存是 STATEMENT 级别就清空缓存【默认是session级别】
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
一级缓存
我们再来基于上面的 BaseExecuto#query
来看看一级缓存
Q:一级缓存的开启方式
默认就开启的,无法关闭,因为一定会走这段代码,但是我们可以让缓存失效,失效有两种方式 修改缓存级别为 STATEMMENT (默认是 session)
和 开启 isFlushCacheRequired
缓存级别可以通过配置文件来配置
mybatis:
configuration:
local-cache-scope: statement
isFlushCacheRequired 是MappedStatement的一个配置属性,首先会获取 insert/update/delete/select 标签中的 flushCache 值,如果没有配置,那如果是select 就默认 true,其它就是false
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
Q:为什么说一级缓存是基于 session 的
这个session,是 sqlSession,而不是cookie、session中的session
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
这句代码是在 Executor 里面执行的,所以这个 localCache 是Executor的,而Executor又被放入了 sqlSession
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();
}
}
Q:什么情况会用到一级缓存呢?
这个问题是不是很奇怪?以前我们知道一级缓存不会被关闭,理所当然的以为每次都走一级缓存了,现在我们知道其实有办法让一级缓存失效。那不是理所当然的只要没失效就会走吗?
事实证明不是这样的,我们再来回顾一下 : 一级缓存是基于 sqlSession的,如果两次查询用的不是一个 sqlSession呢?
问题变得明朗了:这个 sqlSession的生命周期是什么?
Q:这个 sqlSession的生命周期是什么?(重要)
下面是 SqlSessionInterceptor 简化后的代码,我们可以看到不管怎么样最后一定会关闭sqlSession
private class SqlSessionInterceptor implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 获取 sqlSession
SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
try {
// 执行方法
Object result = method.invoke(sqlSession, args);
return result;
} catch (Throwable t) {
// 异常处理.....
} finally {
// 【关闭 sqlSession】
if (sqlSession != null) {
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
}
}
}
}
如果每个方法在执行结束后都关闭了 sqlSession,那不是完蛋了?每次都是新的 sqlSession,那还缓存个锤子?
当然事情肯定不是这样的,我们再来仔细看看 获取和关闭sqlSession的具体逻辑
getSqlSession
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType,
PersistenceExceptionTranslator exceptionTranslator) {
notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);
// 如果当前我们开启了事物,那就从 ThreadLocal 里面获取 session
SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
SqlSession session = sessionHolder(executorType, holder);
if (session != null) {
return session;
}
LOGGER.debug(() -> "Creating a new SqlSession");
// 没有获取到 session,创建一个 session
session = sessionFactory.openSession(executorType);
// 如果当前开启了事物,就把这个session注册到当前线程的 ThreadLocal 里面去
registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);
return session;
}
closeSqlSession
public static void closeSqlSession(SqlSession session, SqlSessionFactory sessionFactory) {
notNull(session, NO_SQL_SESSION_SPECIFIED);
notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
if ((holder != null) && (holder.getSqlSession() == session)) {
LOGGER.debug(() -> "Releasing transactional SqlSession [" + session + "]");
holder.released();
} else {
LOGGER.debug(() -> "Closing non transactional SqlSession [" + session + "]");
session.close();
}
}
public void released() {
--this.referenceCount;
}
现在真相大白了,如果我们方法是开启事物的,则当前事物内是获取的同一个 sqlSession,否则每次都是不同的 sqlSession
Q:PerpetualCache 是个什么东西?
早期背面试题的时候,我连这个都不会读,后面会读了但依旧不知道是个什么东西, 其实它就是一个 HashMap ,只是给这个HashMap命名为 PerpetualCache
protected PerpetualCache localCache;
// 重写的 equals 和 hashCode 我就省略了
public class PerpetualCache implements Cache {
private final String id;
private final Map<Object, Object> cache = new HashMap<>();
public PerpetualCache(String id) {
this.id = id;
}
@Override
public String getId() {
return id;
}
@Override
public int getSize() {
return cache.size();
}
@Override
public void putObject(Object key, Object value) {
cache.put(key, value);
}
@Override
public Object getObject(Object key) {
return cache.get(key);
}
@Override
public Object removeObject(Object key) {
return cache.remove(key);
}
@Override
public void clear() {
cache.clear();
}
Q:一级缓存何时过期呢?
- 一级缓存是存在 sqlSession 里面的,毫无疑问当 sqlSession 被清空或者关闭的时候缓存就没了,而在不开启事物的情况下,每次都会关闭 sqlSession
- 当执行 insert、update、delete 的时候也会清空缓存
其实insert和delete,底层都是调用的update
@Override
public int insert(String statement, Object parameter) {
return update(statement, parameter);
}
@Override
public int update(String statement) {
return update(statement, null);
}
@Override
public int update(String statement, Object parameter) {
try {
dirty = true;
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.update(ms, wrapCollection(parameter));
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error updating database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
@Override
public int delete(String statement, Object parameter) {
return update(statement, parameter);
}
之前我们也说了,最终都是装载的 CachingExecutor
CachingExecutor#update 第一步就是删除二级缓存
@Override
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
flushCacheIfRequired(ms);
return delegate.update(ms, parameterObject);
}
private void flushCacheIfRequired(MappedStatement ms) {
Cache cache = ms.getCache();
// 前面也讲过了对于非 select isFlushCacheRequired 默认就是 true
// boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
if (cache != null && ms.isFlushCacheRequired()) {
tcm.clear(cache);
}
}
delegate.update(ms, parameterObject); 去调用我们的 BaseExecutor,也是先清空缓存
@Override
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);
}
@Override
public void clearLocalCache() {
if (!closed) {
localCache.clear();
localOutputParameterCache.clear();
}
}
二级缓存
理解了一级缓存,我们再来看二级缓存其实也很简单,还是以问题的方式来解答
Q:二级缓存的开启方式
二级缓存不像一级缓存默认就是开启的,我们需要在要开启二级缓存Mapper里面加上 cache标签,加了这个标签就开启了
通过上面的代码,我们知道在执行二级缓存之前,先会去 MappedStatement 里面获取 cache,如果 cache不为空,我们就用二级缓存,再来看看下面的源码
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
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);
if (list == null) {
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
Q:cache 标签是如何解析的
完整的 Mapper解析参看上一篇文章,这里我们只看 cache 标签解析
private void configurationElement(XNode context) {
try {
// ....
cacheElement(context.evalNode("cache"));
// ....
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
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);
}
}
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);
// 属于 builderAssistant 类,创建 MappedStatement 的时候就会把这个 currentCache 设置到 cache属性里面
currentCache = cache;
return cache;
}
Q:二级缓存的作用域
二级缓存是存在 MappedStatement 里面的,而MappedStatement 是由一个个 select、insert、update、delete 标签解析来的,所以就说二级缓存的作用域是 Mapper
Q:一二级缓存的执行顺序
在 CachingExecutor 里面的 query 方法很容易就看到了,如果开启二级缓存那就用二级,不然就是一级
Q:cache 标签的属性配置
上面我们只是讲解了最基础的二级缓存,其实cache标签还有很多参数
<cache type="" eviction="" readOnly="" flushInterval="" size="" blocking=""></cache>
解析 cache 标签时候的代码 cacheElement
private void cacheElement(XNode context) {
if (context != null) {
// Type 缓存的类型,如果没有默认就是 PerpetualCache
String type = context.getStringAttribute("type", "PERPETUAL");
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
// eviction 缓存策略,默认是 LRU 这个下面解释 【这里用到了装饰器模式哦】
String eviction = context.getStringAttribute("eviction", "LRU");
Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
// 如果你设置了这个缓存刷新时间,就会间隔这个时间去清空缓存(包装成 ScheduledCache)
Long flushInterval = context.getLongAttribute("flushInterval");
// 缓存的大小
Integer size = context.getIntAttribute("size");
// 是否只读
boolean readWrite = !context.getBooleanAttribute("readOnly", false);
// 是不是阻塞缓存,是的话会包装成 BlockingCache
boolean blocking = context.getBooleanAttribute("blocking", false);
// 获取其它自定义标签内容
Properties props = context.getChildrenAsProperties();
// 获取参数完毕,组装缓存
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
}
}
useNewCache
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;
}
我们来看看这个 build 建造者模式
public Cache build() {
// 设置默认的缓存实现类和默认的装饰器(PerpetualCache 和 LruCache)
setDefaultImplementations();
// 创建基本的缓存
Cache cache = newBaseCacheInstance(implementation, id);
// 设置自定义的参数
setCacheProperties(cache);
// 如果是PerpetualCache 的缓存,我们将进一步处理
if (PerpetualCache.class.equals(cache.getClass())) {
for (Class<? extends Cache> decorator : decorators) {
// 进行最基本的装饰
cache = newCacheDecoratorInstance(decorator, cache);
// 设置自定义的参数
setCacheProperties(cache);
}
// 创建标准的缓存,也就是根据配置来进行不同的装饰
cache = setStandardDecorators(cache);
} else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
// 如果是自定义的缓存实现,这里只进行日志装饰器
cache = new LoggingCache(cache);
}
return cache;
}
通过上面的缓存实现,我们发现了很多缓存的装饰器,什么 LruCache、ScheduledCache、LoggingCache 我们来看看到底有多少种装饰器
我们通过装饰器的名字就大概猜到了要干什么。
注:这里说一下我最开始以为它们都是cache的实现类,其实不是,真正的实现类只有 PerpetualCache
,红框框里面的都是对 PerpetualCache 的包装。
了解了缓存装饰器,我们接着看 setStandardDecorators
private Cache setStandardDecorators(Cache cache) {
try {
// 获取当前 cache的参数
MetaObject metaCache = SystemMetaObject.forObject(cache);
// 如果设置了 size 就设置size的大小
if (size != null && metaCache.hasSetter("size")) {
metaCache.setValue("size", size);
}
// 如果设置了缓存刷新时间,就进行ScheduledCache 装饰
if (clearInterval != null) {
cache = new ScheduledCache(cache);
((ScheduledCache) cache).setClearInterval(clearInterval);
}
// 如果缓存可读可写,就需要进行序列化 默认就是 true,这也是为什么我们的二级缓存的需要实现序列化
if (readWrite) {
cache = new SerializedCache(cache);
}
// 默认都装饰 日志和同步
cache = new LoggingCache(cache);
cache = new SynchronizedCache(cache);
// 如果开启了阻塞就装配阻塞
if (blocking) {
cache = new BlockingCache(cache);
}
// 返回层层套娃的cache
return cache;
} catch (Exception e) {
throw new CacheException("Error building standard cache decorators. Cause: " + e, e);
}
}
这里的装饰器模式真实妙哇,看完之后大呼过瘾,随便怎么组合进行装饰,是理解装饰器模式的好案例。
Q:为什么二级缓存的实体一定要实现序列化接口
通过上面的源码,我们知道默认就是可读可写的缓存,它会用 SynchronizedCache
进行装饰,我们来看来SynchronizedCache 的putObject 方法,你就知道了
@Override
public void putObject(Object key, Object object) {
if (object == null || object instanceof Serializable) {
delegate.putObject(key, serialize((Serializable) object));
} else {
throw new CacheException("SharedCache failed to make a copy of a non-serializable object: " + object);
}
}
当然了如果你把设置成只读的,就没问题了,但这样又失去了它存在的意义。
Q:缓存在分布式场景的问题
通过上面我们知道所有的缓存都是基于Mybatis的基础之上的,如果你直接对数据操作,那Mybatis是无法感知的,而在分布式的场景下,其实就相当于直接修改数据库了。
一级缓存: 一级缓存是 sqlSession,而只要没开启事物哪怕一个线程每次获取的也都是新的 sqlSession,相当于没有缓存。而对于一个事务而言,我们就是要保证它多次读取的数据是一致的。
二级缓存:二级缓存默认就是不开启的,在分布式的情况下也一定不要开启二级缓存。
Q:二级缓存何时过期呢?
因为MappedStatement 不存在删除的情况,所以二级缓存失效只有两种情况
- update、delete、insert 操作,参看一级缓存失效的源码
- 在cache标签里面设置了 flushInterval 参数,会被 ScheduledCache 装饰,定时删除缓存
问题解答
熟悉了上面的缓存原理,我们再来看这个问题就简单了
mapper.getId()
// 打断点 ,然后去修改数据库的数据
mapper.getId()
- 无事务 + 无二级缓存 : 两次查询的结果不一样
- 无事务 + 有二级缓存 : 两次查询的结果一样
- 有事务 + 不管有没有二级缓存 :两次查询的结果一样