深入理解MyBatis一级缓存和二级缓存【超详细源码解析】

19 篇文章 1 订阅
15 篇文章 0 订阅

视频地址: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 自动注入的时候会创建 SqlSessionFactorySqlSessionTemplate

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 创建的时候,加入了这个拦截器,所以正式去执行方法的时候是会先执行这个拦截器

  1. MapperProxy 基于 SqlSessionTemplate 来创建的
  2. SqlSessionTemplate 是基于反射创建的,在创建的时候加入了拦截器 SqlSessionInterceptor
  3. 当我们执行 MapperProxy 的 invoke 方法时候,会先进入这个拦截器
  4. 第一步获取 sqlSession,所以最终的sqlSession没并不是使用的 SqlSessionTemplate
  5. 最后会关闭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 这个方法一路进去最终的实现如下

  1. 创建 Executor, 这个就是最终的sql执行器,和缓存相关的
  2. 最终创建的 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:这里使用的是装饰器模式)

  1. cacheEnabled 默认就是 true
  2. 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:一级缓存何时过期呢?

  1. 一级缓存是存在 sqlSession 里面的,毫无疑问当 sqlSession 被清空或者关闭的时候缓存就没了,而在不开启事物的情况下,每次都会关闭 sqlSession
  2. 当执行 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 不存在删除的情况,所以二级缓存失效只有两种情况

  1. update、delete、insert 操作,参看一级缓存失效的源码
  2. 在cache标签里面设置了 flushInterval 参数,会被 ScheduledCache 装饰,定时删除缓存

问题解答

熟悉了上面的缓存原理,我们再来看这个问题就简单了

mapper.getId()

// 打断点 ,然后去修改数据库的数据

mapper.getId()
  1. 无事务 + 无二级缓存 : 两次查询的结果不一样
  2. 无事务 + 有二级缓存 : 两次查询的结果一样
  3. 有事务 + 不管有没有二级缓存 :两次查询的结果一样
  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值