MyBatis原理系列(八)-手把手带你了解一级缓存和二级缓存

MyBatis原理系列(一)-手把手带你阅读MyBatis源码
MyBatis原理系列(二)-手把手带你了解MyBatis的启动流程
MyBatis原理系列(三)-手把手带你了解SqlSession,SqlSessionFactory,SqlSessionFactoryBuilder的关系
MyBatis原理系列(四)-手把手带你了解MyBatis的Executor执行器
MyBatis原理系列(五)-手把手带你了解Statement、StatementHandler、MappedStatement间的关系
MyBatis原理系列(六)-手把手带你了解BoundSql的创建过程
MyBatis原理系列(七)-手把手带你了解如何自定义插件
MyBatis原理系列(八)-手把手带你了解一级缓存和二级缓存
MyBatis原理系列(九)-手把手带你了解MyBatis事务管理机制

缓存在硬件和软件应用广泛,我们在大学学过计算机与操作系统中接触过高速缓存,闪存等。在工作中,我们也接触过一些缓存中间件,比如Redis,MemCache。MyBatis作为一款优秀的ORM框架,也提供了缓存的功能,减少访问数据库的次数,从而提高性能。本文将和大家介绍MyBatis的实现和原理。

1. 初识缓存

MyBatis提供的缓存功能包含一级缓存和二级缓存,都是默认开启的,它们的作用范围也是不同的。MyBatis的缓存是基于cache接口的。cache接口的继承关系如下

cache的继承关系

cache作为顶层接口,定义了缓存的基本操作,比如设置缓存,获取缓存的方法。

public interface Cache {

  /**
   * 唯一标示缓存
   * @return
   */
  String getId();

  /**
   * 以key value形式设置缓存
   * @param key
   * @param value
   */
  void putObject(Object key, Object value);

  /**
   * 获取缓存
   * @param key
   * @return
   */
  Object getObject(Object key);

  /**
   * 删除缓存
   */
  Object removeObject(Object key);

  /**
   * 清空缓存实例
   */
  void clear();

  /**
   * 缓存中元素的数量
   * @return
   */
  int getSize();

  /**
   * 读写锁
   * @return
   */
  default ReadWriteLock getReadWriteLock() {
    return null;
  }

}

PerpetualCache 是cache的默认实现,也是最简单的实现,它以HashMap作为缓存容器,存储缓存。其它类型的缓存是对PerpetualCache的包装。

public class PerpetualCache implements Cache {
  
  private final String id;

  // 以map存储缓存
  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();
  }

  @Override
  public boolean equals(Object o) {
    if (getId() == null) {
      throw new CacheException("Cache instances require an ID.");
    }
    if (this == o) {
      return true;
    }
    if (!(o instanceof Cache)) {
      return false;
    }

    Cache otherCache = (Cache) o;
    return getId().equals(otherCache.getId());
  }

  @Override
  public int hashCode() {
    if (getId() == null) {
      throw new CacheException("Cache instances require an ID.");
    }
    return getId().hashCode();
  }

}

2. 一级缓存

2.1 一级缓存开启

MyBatis一级缓存是默认开启的,并且它的作用范围是SqlSession级别的。我么知道SqlSession是顶层的接口,最终的数据库操作都是交由给执行器进行操作的。了解前面的Executor的同学可知,缓存就是在执行Executor中进行维护的,其中localCache成员变量就是一级缓存对象,其类型就是PerpetualCache。

public abstract class BaseExecutor implements Executor {

  private static final Log log = LogFactory.getLog(BaseExecutor.class);

  protected Transaction transaction;
  protected Executor wrapper;

  protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
  protected PerpetualCache localCache;
  protected PerpetualCache localOutputParameterCache;
  protected Configuration configuration;

  protected int queryStack;
  private boolean closed;

  protected BaseExecutor(Configuration configuration, Transaction transaction) {
    this.transaction = transaction;
    this.deferredLoads = new ConcurrentLinkedQueue<>();
    this.localCache = new PerpetualCache("LocalCache");
    this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
    this.closed = false;
    this.configuration = configuration;
    this.wrapper = this;
  }
}

一级缓存是默认开启的,Configuration的成员变量localCacheScope的默认就是Sesssion级别的。

// Configuration类
protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;

如果要关闭,我们可以在mybatis-config.xml中的settings标签中将这个配置设置成Statement类型的

<setting name="localCacheScope" value="STATEMENT"/>

如果某个select标签查询不需要缓存,在select标签加上flushCache="true"也可以设置单个查询关闭缓存

  <select id="selectByPrimaryKey" parameterType="java.lang.Long" 
          resultMap="BaseResultMap" flushCache="true">
    select 
    <include refid="Base_Column_List" />
    from t_test_user
    where id = #{id,jdbcType=BIGINT}
  </select>
2.1 一级缓存存取

缓存在查询中才会用到,例如我们用同一个sql语句反复去查询数据库,并且在此期间没有进行过数据修改操作,预期是返回相同的结果。如果没有缓存,我们将每次都要访问数据库返回结果,这个过程无疑是浪费资源和消耗性能的。因此我们可以将第一次查询的结果缓存在内存中,第二次用相同的sql语句查询的时候,先去缓存中查询,如果命中则直接返回,否则去数据库查询并放到缓存中返回。我们接下来看看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());
    // Executor是否关闭
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    // select标签是否配置了flushCache=true
    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();
      }
    }
    return list;
  }

第一次查询肯定从缓存中查询不到东西,于是走向了queryFromDatabase分支,这个方法就直接从数据库中去查询

private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    // 添加占位符,标示正在执行
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
      // 调用子类的查询方法获取结果
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      localCache.removeObject(key);
    }
    // 将查询结果放到缓存中
    localCache.putObject(key, list);
    // 如果是存储过程则需要处理输出参数
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }

注意这个缓存真的是查询sql完全一样,这个一样还包括参数的一致,才会从缓存中获取到结果,那么如何判断两个查询sql是否一样呢。createCacheKey就帮忙解答了这个疑惑,它会给每个sql都生成一个key,如果两个生成的key一致,那就表明不管是sql还是参数都是一致的。

 @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;
  }
2.3 一级缓存清除

在执行update,commit,或者rollback操作的时候都会进行清除缓存操作,所有的缓存都将失效。

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

3. 二级缓存

一级缓存的作用范围是SqlSession级别的,但是SqlSession是单线程的,不同线程间的操作会有一些脏数据的问题。二级缓存的范围更大,是Mapper级别的缓存,因此不同sqlSession间可以共享缓存。

3.1 二级缓存开启
  1. 开启二级缓存需要配置cacheEnabled为true,这个属性默认为true。
<setting name="cacheEnabled" value="true"/>
  1. 在需要进行开启二级缓存的mapper中新增cache配置,cache配置有很多属性。
  • type : 缓存实现类,默认是PerpetualCache,也可以是第三方缓存的实现

  • size:最多缓存对象的个数

  • eviction:缓存回收策略,默认是LRU
    LRU:最近最少使用策略,回收最长时间不被使用的缓存
    FIFO:先进先出策略,回收最新进入的缓存
    SOFT - 软引用:移除基于垃圾回收器状态和软引用规则的对象
    WEAK - 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象

  • flushInterval:缓存刷新的间隔时间,默认是不刷新的

  • readOnly : 是否只读,true 只会进行读取操作,修改操作交由用户处理
    false 可以进行读取操作,也可以进行修改操作

  <cache type="org.apache.ibatis.cache.impl.PerpetualCache"
         size="1024"
         eviction="LRU"
         flushInterval="120000"
         readOnly="false"/>
  1. 也可以对单个Statement标签进行关闭和开启操作,通过配置useCache="true"来开启缓存
  <select id="selectByPrimaryKey" parameterType="java.lang.Long"
          resultMap="BaseResultMap" useCache="true">
    select 
    <include refid="Base_Column_List" />
    from t_test_user
    where id = #{id,jdbcType=BIGINT}
  </select>
3.2 二级缓存存取

二级缓存是Mapper级别的缓存,因此SqlSession是不可以管理的,我们再把目光转向Executor,Executor在介绍的时候涉及到了CachingExecutor,在Configuration创建Executor的时候,如果开启了二级缓存,就使用到了CachingExecutor进行了包装。

// Configuration
  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;
  }

CachingExecutor 中只有两个成员变量,其中一个就是TransactionalCacheManager用来管理缓存。

 // 1. 委托执行器,也就是被包装的三种执行器的中的一种
  private final Executor delegate;
  // 2. 缓存管理类,用来管理TransactionalCache
  private final TransactionalCacheManager tcm = new TransactionalCacheManager();

TransactionalCacheManager 结构也比较简单,内部也维护着一个HashMap缓存,其中TransactionalCache实现了Cache接口。

public class TransactionalCacheManager {

  // 缓存,TransactionalCache实现了Cache接口
  private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();

  public void clear(Cache cache) {
    getTransactionalCache(cache).clear();
  }

  public Object getObject(Cache cache, CacheKey key) {
    return getTransactionalCache(cache).getObject(key);
  }

  public void putObject(Cache cache, CacheKey key, Object value) {
    getTransactionalCache(cache).putObject(key, value);
  }

  // 提交
  public void commit() {
    for (TransactionalCache txCache : transactionalCaches.values()) {
      txCache.commit();
    }
  }
  // 回滚
  public void rollback() {
    for (TransactionalCache txCache : transactionalCaches.values()) {
      txCache.rollback();
    }
  }

  // 获取缓存
  private TransactionalCache getTransactionalCache(Cache cache) {
    return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
  }

}

二级缓存的的存取过程是怎么样的呢,我们可以看看CachingExecutor的query方法。如果Statement标签配置了开启缓存,则从缓存中去取,否则执行执行一级缓存的查询逻辑。如果开启了缓存,则先从二级缓存中查找,如果命中直接返回,否则执行一级缓存的逻辑。因此当二级缓存开启时,优先从二级缓存中查找,再去从一级缓存中查找,最后从数据库查找。

// CachingExecutor
@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) {
      // select标签是否配置了flushCache属性
      flushCacheIfRequired(ms);
      // 如果select标签配置了useCache属性
      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);
  }
3.3 二级缓存清除

清空缓存也是在执行更新操作的时候进行删除缓存

  @Override
  public int update(MappedStatement ms, Object parameterObject) throws SQLException {
    // 清空缓存
    flushCacheIfRequired(ms);
    // 调用实际执行器的update方法
    return delegate.update(ms, parameterObject);
  }

4. 例子

接下来我们将以两个例子来更加清晰的介绍下一级缓存和二级缓存

4.1 一级缓存

一级缓存是SqlSession级别的缓存,如果用同一个sql执行两次相同的sql,第一次会执行查询打印sql,第二次则是直接从缓存中去获取,不会打印sql,从日志可以看出来只打印了一次sql,说明第二次是从缓存中获取的。

先将二级缓存关闭

<setting name="cacheEnabled" value="false"/>

然后执行两次相同的语句

    public static void main(String[] args) {
        try {
            // 1. 读取配置
            InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
            // 2. 创建SqlSessionFactory工厂
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
            // 3. 获取sqlSession
            SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.SIMPLE);
            // 4. 获取Mapper
            TTestUserMapper userMapper = sqlSession.getMapper(TTestUserMapper.class);
            // 5. 执行接口方法
            TTestUser user = userMapper.selectByPrimaryKey(1000L);
            TTestUser user1 = userMapper.selectByPrimaryKey(1000L);
            // 6. 提交事物
            sqlSession.commit();
            // 7. 关闭资源
            sqlSession.close();
            inputStream.close();
        } catch (Exception e){
            log.error(e.getMessage(), e);
        }
    }

最后打印了一次sql,说明第二次是从缓存中获取的

16:37:33.088 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Opening JDBC Connection
16:37:35.027 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Created connection 1995250556.
16:37:35.028 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@76ed1b7c]
16:37:35.050 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPrimaryKey - ==>  Preparing: select id, member_id, real_name, nickname, date_create, date_update, deleted from t_test_user where id = ? 
16:37:35.108 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPrimaryKey - ==> Parameters: 1000(Long)
16:37:35.171 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPrimaryKey - <==      Total: 1
16:37:35.174 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@76ed1b7c]
16:37:35.191 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@76ed1b7c]
16:37:35.191 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Returned connection 1995250556 to pool.

因为是SqlSession级别的,如果不同的SqlSession级别的执行相同的sql,应该互不影响,应该会打印两次sql,我们将上面的代码稍微修改下

public static void main(String[] args) {
        try {
            // 1. 读取配置
            InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
            // 2. 创建SqlSessionFactory工厂
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
            // 3. 获取sqlSession
            SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.SIMPLE);
            // 4. 获取Mapper
            TTestUserMapper userMapper = sqlSession.getMapper(TTestUserMapper.class);
            // 5. 执行接口方法
            TTestUser user = userMapper.selectByPrimaryKey(1000L);

            // 开启新的sqlSession
            SqlSession sqlSession2 = sqlSessionFactory.openSession();
            TTestUserMapper userMapper2 = sqlSession2.getMapper(TTestUserMapper.class);
            TTestUser user2 = userMapper2.selectByPrimaryKey(1000L);
            // 6. 提交事物
            sqlSession.commit();
            // 7. 关闭资源
            sqlSession.close();
            inputStream.close();
        } catch (Exception e){
            log.error(e.getMessage(), e);
        }
    }

打印了两次sql,证明了一级缓存是SqlSession的级别的,不同的SqlSession间不能共享缓存。

16:44:06.871 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Opening JDBC Connection
16:44:08.297 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Created connection 34073107.
16:44:08.297 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@207ea13]
16:44:08.316 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPrimaryKey - ==>  Preparing: select id, member_id, real_name, nickname, date_create, date_update, deleted from t_test_user where id = ? 
16:44:08.365 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPrimaryKey - ==> Parameters: 1000(Long)
16:44:08.447 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPrimaryKey - <==      Total: 1
16:44:08.448 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Opening JDBC Connection
16:44:08.717 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Created connection 1527254842.
16:44:08.718 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@5b080f3a]
16:44:08.740 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPrimaryKey - ==>  Preparing: select id, member_id, real_name, nickname, date_create, date_update, deleted from t_test_user where id = ? 
16:44:08.741 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPrimaryKey - ==> Parameters: 1000(Long)
16:44:08.764 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPrimaryKey - <==      Total: 1
16:44:08.764 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@207ea13]
16:44:08.788 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@207ea13]
16:44:08.789 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Returned connection 34073107 to pool.

4.1 二级缓存

先开启二级缓存

<setting name="cacheEnabled" value="true"/>

然后对应的mapper中开启缓存

  <select id="selectByPrimaryKey" parameterType="java.lang.Long"
          resultMap="BaseResultMap" useCache="true">
    select 
    <include refid="Base_Column_List" />
    from t_test_user
    where id = #{id,jdbcType=BIGINT}
  </select>

  <cache type="org.apache.ibatis.cache.impl.PerpetualCache"
         size="1024"
         eviction="LRU"
         flushInterval="120000"
         readOnly="false"/>

复用上面的代码,我们看看不同SqlSession间是否能够共享缓存。
发现还是打印了2次sql,说明缓存没生效,配置都配置正确了,会有其它原因吗

16:56:34.043 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Opening JDBC Connection
16:56:35.278 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Created connection 316335490.
16:56:35.279 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@12dae582]
16:56:35.292 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPrimaryKey - ==>  Preparing: select id, member_id, real_name, nickname, date_create, date_update, deleted from t_test_user where id = ? 
16:56:35.341 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPrimaryKey - ==> Parameters: 1000(Long)
16:56:35.386 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPrimaryKey - <==      Total: 1
16:56:35.387 [main] DEBUG com.example.demo.dao.TTestUserMapper - Cache Hit Ratio [com.example.demo.dao.TTestUserMapper]: 0.0
16:56:35.387 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Opening JDBC Connection
16:56:35.544 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Created connection 375074687.
16:56:35.544 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@165b2f7f]
16:56:35.560 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPrimaryKey - ==>  Preparing: select id, member_id, real_name, nickname, date_create, date_update, deleted from t_test_user where id = ? 
16:56:35.560 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPrimaryKey - ==> Parameters: 1000(Long)
16:56:35.571 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPrimaryKey - <==      Total: 1
16:56:35.583 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@12dae582]
16:56:35.602 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@12dae582]
16:56:35.602 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Returned connection 316335490 to pool.

再看看CachingExecutor中的query方法,有这一行代码

// CachingExecutor
// 二级缓存存储时先保存在临时属性中,等事务提交再保存到真实的二级缓存
   tcm.putObject(cache, key, list); // issue #578 and #116

再看看CachingExecutor的commit方法,在commit的时候才会将缓存放到真正的缓存中,这样做的目的就是为了防止不通SqlSession间的脏读,一个SqlSession读取了另一个SqlSession还未提交的数据。

  @Override
  public void commit(boolean required) throws SQLException {
    delegate.commit(required);
    tcm.commit();
  }

接下来修改上述代码为如下

 public static void main(String[] args) {
        try {
            // 1. 读取配置
            InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
            // 2. 创建SqlSessionFactory工厂
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
            // 3. 获取sqlSession
            SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.SIMPLE);
            // 4. 获取Mapper
            TTestUserMapper userMapper = sqlSession.getMapper(TTestUserMapper.class);
            // 5. 执行接口方法
            TTestUser user = userMapper.selectByPrimaryKey(1000L);
            sqlSession.commit();

            // 开启新的sqlSession
            SqlSession sqlSession2 = sqlSessionFactory.openSession();
            TTestUserMapper userMapper2 = sqlSession2.getMapper(TTestUserMapper.class);
            TTestUser user2 = userMapper2.selectByPrimaryKey(1000L);
            sqlSession2.commit();

            // 7. 关闭资源
            sqlSession.close();
            sqlSession2.close();
            inputStream.close();
        } catch (Exception e){
            log.error(e.getMessage(), e);
        }
    }

第一次查询提交了事务后,第二次直接命中了缓存,从而印证了事务提交才会将查询结果放到缓存中。

17:08:20.993 [main] DEBUG com.example.demo.dao.TTestUserMapper - Cache Hit Ratio [com.example.demo.dao.TTestUserMapper]: 0.0
17:08:21.011 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Opening JDBC Connection
17:08:22.568 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Created connection 316335490.
17:08:22.568 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@12dae582]
17:08:22.589 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPrimaryKey - ==>  Preparing: select id, member_id, real_name, nickname, date_create, date_update, deleted from t_test_user where id = ? 
17:08:22.643 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPrimaryKey - ==> Parameters: 1000(Long)
17:08:22.692 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPrimaryKey - <==      Total: 1
17:08:22.706 [main] DEBUG com.example.demo.dao.TTestUserMapper - Cache Hit Ratio [com.example.demo.dao.TTestUserMapper]: 0.5
17:08:22.707 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@12dae582]
17:08:22.733 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@12dae582]
17:08:22.733 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Returned connection 316335490 to pool.

5. 总结

  • MyBatis 中包含一级缓存和二级缓存,一级缓存的作用范围是SqlSession级别的,二级缓存是Mapper级别的。
  • MyBatis 中的一级缓存和二级缓存都是默认开启的,不过二级缓存还要额外在mapper和statement中配置缓存属性
  • 一级缓存和二级缓存适用于读多写少的场景,如果频繁的更新数据,将降低查询性能。

参考 给我五分钟,带你彻底掌握 MyBatis 缓存的工作原理

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341

推荐阅读更多精彩内容