不看源码,真不知道springboot默认整合mybatis会导致一级缓存失效
2024-07-17 10:00 阅读(265)

在 MyBatis 中,SqlSession 缓存查询结果的主要机制包括两种:一级缓存(Local Cache)和二级缓存(Second Level Cache)。这两种缓存机制可以帮助提高查询性能和减少数据库访问次数。

一级缓存(Local Cache)

一级缓存是 SqlSession 的默认缓存机制,它的特点如下:


生命周期:一级缓存是 SqlSession 级别的缓存,也就是说,在同一个 SqlSession 中执行的所有查询操作可以共享这个缓存。


作用范围:一级缓存作用于 Session 内部,即同一个 SqlSession 对象中的多次查询可以共享缓存。


缓存规则:当执行一次查询时,查询结果会被存储在一级缓存中。如果相同的查询再次被执行,并且 SqlSession 还处于打开状态,MyBatis 会直接从缓存中获取结果,而不会再次查询数据库。


失效条件:一级缓存会在以下情况下失效:


手动清除缓存(调用 clearCache() 方法)

SqlSession 执行了任何一个会修改数据库内容的操作(比如增加、修改、删除操作),这会导致缓存失效,以保证数据的一致性。

  @Override
  public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    CacheKey cacheKey = new CacheKey();
    cacheKey.update(ms.getId());
    cacheKey.update(rowBounds.getOffset());
    cacheKey.update(rowBounds.getLimit());
    cacheKey.update(boundSql.getSql());
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
    // mimic DefaultParameterHandler logic
    for (ParameterMapping parameterMapping : parameterMappings) {
      if (parameterMapping.getMode() != ParameterMode.OUT) {
        Object value;
        String propertyName = parameterMapping.getProperty();
        if (boundSql.hasAdditionalParameter(propertyName)) {
          value = boundSql.getAdditionalParameter(propertyName);
        } else if (parameterObject == null) {
          value = null;
        } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
          value = parameterObject;
        } else {
          MetaObject metaObject = configuration.newMetaObject(parameterObject);
          value = metaObject.getValue(propertyName);
        }
        cacheKey.update(value);
      }
    }
    if (configuration.getEnvironment() != null) {
      // issue #176
      cacheKey.update(configuration.getEnvironment().getId());
    }
    return cacheKey;
  }

上面描述的是: 怎么创建缓存key ;


mapperstatement id 就是这个 com.example.mapper.UserMapper.selectUserById

<!-- UserMapper.xml -->
<mapper namespace="com.example.mapper.UserMapper">
    <select id="selectUserById" resultType="User">
        SELECT * FROM users WHERE id = #{id}
    </select>
</mapper>

User user = sqlSession.selectOne("com.example.mapper.UserMapper.selectUserById", 1);



  @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.");
    }
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }
    List<E> list;
    try {
      queryStack++;
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } finally {
      queryStack--;
    }
    if (queryStack == 0) {
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      // issue #601
      deferredLoads.clear();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }

删除、修改等,会清除缓存。 比如


BaseExecutor 中update方法


commit、rollback、close都会清空缓存的。

  @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);
  }

思考一下 springboot 中,整合mybatis ,注入Mapper,执行select方法,会走缓存吗,那么什么时候执行,多个浏览器请求,执行Mapper方法,会是不是同一个SqlSession?

1、 在springboot中,直接注入 Mapper 接口,其实是Spring的MapperFactoryBean 通过getObject方法,从sqlSessionTemplate 去获取的。另外注意一下,因为SqlSessionTemplate是线程安全的,所以说所有的controller里面注入Mapper接口,是安全的。

2、 因为获取SqlSessionTemplate涉及到,事务管理,目前是同一个事务,一定是拿到同一个sqlSession。

3、 浏览器一个请求,来到controller,如果该controller,执行两次同一个mapper方法,且开启了事务管理,那么该缓存肯定是生效的。多个浏览器请求肯定不是同一个session。

4、 如果没有开启事务,调用一次mapper里的方法将会新建一个sqlSession来执行方法。

SqlSessionTemplate#SqlSessionInterceptor 中有个getSqlSession()方法。


  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);
        if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
          // release the connection to avoid a deadlock if the translator is no loaded. See issue #22
          closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
          sqlSession = null;
          Throwable translated = SqlSessionTemplate.this.exceptionTranslator
              .translateExceptionIfPossible((PersistenceException) unwrapped);
          if (translated != null) {
            unwrapped = translated;
          }
        }
        throw unwrapped;
      } finally {
        if (sqlSession != null) {
          closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
        }
      }
    }
  }

只有TransactionSynchronizationManager.isSynchronizationActive() 激活才会可以拿到sqlSession。下面是激活的状态,只有事务才会激活。

public interface TransactionSynchronization extends Ordered, Flushable {

   /** Completion status in case of proper commit. */
   int STATUS_COMMITTED = 0;

   /** Completion status in case of proper rollback. */
   int STATUS_ROLLED_BACK = 1;

   /** Completion status in case of heuristic mixed completion or system errors. */
   int STATUS_UNKNOWN = 2;
}  

二级缓存(Second Level Cache)

二级缓存是跨 SqlSession 的缓存机制,它的特点如下:


生命周期:二级缓存是 Mapper 级别的缓存,多个 SqlSession 可以共享同一个 Mapper 的二级缓存。


作用范围:二级缓存的作用范围是跨 SqlSession 的,不同的 SqlSession 对象可以共享同一个 Mapper 的二级缓存。


配置:二级缓存需要在 MyBatis 的配置文件中明确配置开启,并在 Mapper 的 XML 文件或接口中设置适当的缓存策略(如 <cache> 标签或 @CacheNamespace 注解)。


缓存规则:查询结果会被存储在二级缓存中,当同一个 Mapper 的不同 SqlSession 执行相同的查询时,MyBatis 会先从二级缓存中查找结果。如果找到,则直接返回缓存中的数据,不再执行数据库查询。


失效条件:二级缓存会在以下情况下失效:


手动清除缓存(比如调用 clearCache() 方法)

由于配置了缓存刷新策略,比如定时刷新或根据特定条件刷新

SqlSession 执行了对应 Mapper 的增删改操作,这会导致该 Mapper 的二级缓存失效。


总结

SqlSession 在 MyBatis 中通过一级缓存和二级缓存来提高查询性能和减少数据库访问次数。一级缓存适用于单个 SqlSession 内的查询结果共享,而二级缓存则适用于多个 SqlSession 间的 Mapper 结果共享。这两种缓存机制可以根据实际应用场景进行合理配置和使用,以达到最佳的性能优化效果。