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


    视频地址:https://www.bilibili.com/video/BV1nP411A7Gu


    MyBatis的缓存是一个常见的面试题

    • 一级缓存的作用域为何是 sqlSession、二级缓存的作用域为何是 mapper
    • 怎么理解 一、二级缓存都是基于 PerpetualCache 的HashMap的本地缓存
    • 为什么一级缓存无法被关闭
    • 怎么才能使用二级缓存?如果使用了二级缓存一级缓存还有用么
    • 如果一级缓存不可以关闭,那在分布式的系统中,如何解决数据一致性问题
    • 如果开启了二级缓存,那缓存的命中顺序将是如何呢

    注:这个是基于源码的角度来讲解缓存,如果对MyBatis源码不熟悉的小伙伴建议先看看这个 MyBatis 执行原理,源码解读


    问题

    在写这篇博客的时候突然想到了一个简单的问题,如果你可以回答出来,那么MyBatis的一、二级缓存你将吃透了

    两次获取的结果是否一致呢?

    mapper.getId()
    
    // 打断点 ,然后去修改数据库的数据
    
    mapper.getId()
    
    • 1
    • 2
    • 3
    • 4
    • 5

    前置

    前置的内容在上一章执行原理里面都已经讲过了,这里只提出一些个关键点,帮助理解。

    • 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);
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    创建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());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    invoke

    public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
      return mapperMethod.execute(sqlSession, args);
    }
    
    • 1
    • 2
    • 3

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    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);
            }
          }
        }
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    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();
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    newExecutor

    它这里创建什么类型的Executor,我们不用关注,我们的目的是缓存

    我们可以看到当 CachingExecutor == true 的时候,会把executor装饰成 CachingExecutor ,CachingExecutor 就是二级缓存了 (ps:这里使用的是装饰器模式)

    1. cacheEnabled 默认就是 true
    2. CachingExecutor 是使用二级缓存的条件之一,后面我们会看到另外一个条件是 mapper 里面增加
    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    最终的调用如下

      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();
        }
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    @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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    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;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    一级缓存

    我们再来基于上面的 BaseExecuto#query 来看看一级缓存


    Q:一级缓存的开启方式

    默认就开启的,无法关闭,因为一定会走这段代码,但是我们可以让缓存失效,失效有两种方式 修改缓存级别为 STATEMMENT (默认是 session)开启 isFlushCacheRequired


    缓存级别可以通过配置文件来配置

    mybatis:
      configuration:
        local-cache-scope: statement
    
    • 1
    • 2
    • 3

    isFlushCacheRequired 是MappedStatement的一个配置属性,首先会获取 insert/update/delete/select 标签中的 flushCache 值,如果没有配置,那如果是select 就默认 true,其它就是false

    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    
    • 1

    Q:为什么说一级缓存是基于 session 的

    这个session,是 sqlSession,而不是cookie、session中的session


    list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
    
    • 1

    这句代码是在 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();
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    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);
          }
        }
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    如果每个方法在执行结束后都关闭了 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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    现在真相大白了,如果我们方法是开启事物的,则当前事物内是获取的同一个 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();
      }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    之前我们也说了,最终都是装载的 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);
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    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();
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    二级缓存

    理解了一级缓存,我们再来看二级缓存其实也很简单,还是以问题的方式来解答


    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    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);
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    Q:二级缓存的作用域

    二级缓存是存在 MappedStatement 里面的,而MappedStatement 是由一个个 select、insert、update、delete 标签解析来的,所以就说二级缓存的作用域是 Mapper


    Q:一二级缓存的执行顺序

    在 CachingExecutor 里面的 query 方法很容易就看到了,如果开启二级缓存那就用二级,不然就是一级

    在这里插入图片描述


    Q:cache 标签的属性配置

    上面我们只是讲解了最基础的二级缓存,其实cache标签还有很多参数

    <cache type="" eviction="" readOnly="" flushInterval="" size=""  blocking="">cache>
    
    • 1

    解析 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);
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    我们来看看这个 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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    通过上面的缓存实现,我们发现了很多缓存的装饰器,什么 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);
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    这里的装饰器模式真实妙哇,看完之后大呼过瘾,随便怎么组合进行装饰,是理解装饰器模式的好案例。


    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);
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    当然了如果你把设置成只读的,就没问题了,但这样又失去了它存在的意义。


    Q:缓存在分布式场景的问题

    通过上面我们知道所有的缓存都是基于Mybatis的基础之上的,如果你直接对数据操作,那Mybatis是无法感知的,而在分布式的场景下,其实就相当于直接修改数据库了。

    一级缓存: 一级缓存是 sqlSession,而只要没开启事物哪怕一个线程每次获取的也都是新的 sqlSession,相当于没有缓存。而对于一个事务而言,我们就是要保证它多次读取的数据是一致的。

    二级缓存:二级缓存默认就是不开启的,在分布式的情况下也一定不要开启二级缓存。


    Q:二级缓存何时过期呢?

    因为MappedStatement 不存在删除的情况,所以二级缓存失效只有两种情况

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

    问题解答

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

    mapper.getId()
    
    // 打断点 ,然后去修改数据库的数据
    
    mapper.getId()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    1. 无事务 + 无二级缓存 : 两次查询的结果不一样
    2. 无事务 + 有二级缓存 : 两次查询的结果一样
    3. 有事务 + 不管有没有二级缓存 :两次查询的结果一样
  • 相关阅读:
    打造千万级流量秒杀第二十二课 本地缓存实战:如何使用内存缓存提升数据命中率?
    对“方法”的解读
    如何在PostgreSQL中使用CTE(公共表表达式)来简化复杂的查询逻辑?
    蓝桥杯官网练习题(旋转)
    缓存一致性解决方案——改数据时如何保证缓存和数据库中数据的一致性
    P1443 马的遍历
    java毕业设计现有传染病查询系统mybatis+源码+调试部署+系统+数据库+lw
    Yakit工具篇:专项漏洞检测的配置和使用
    《PostgreSQL中的JSON处理:技巧与应用》
    015、Python模块-os模块详解
  • 原文地址:https://blog.csdn.net/Tomwildboar/article/details/127570765