{T}

MyBatis 缓存机制

MyBatis 提供了缓存的的机制,一级缓存和二级缓存

一级缓存

一级缓存是 MyBatis 中的默认提供的缓存的,在使用 Mybatis 的时候本身就在使用,默认开启的. 一级缓存是 SqlSession 级别的缓存,只有在一个 SqlSession 内的查询才能共享缓存的数据,当关闭 sqlSession 的时候或者执行增删改查的操作的时候,缓存就会被清空

查询操作

java
// 测试一级缓存
@Test
public void testCacheOne() throws Exception {
  InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
  SqlSessionFactory builder = new SqlSessionFactoryBuilder().build(inputStream);
  // 第一次获取sqlsession对象
  SqlSession sqlSession = builder.openSession();
  UserMapper mapper = sqlSession.getMapper(UserMapper.class);
  System.out.println("第一次查询....");
  User user = mapper.selectUserById(1);
  System.out.println(user);
  
  // 修改数据
  // user.setName("哈哈");
  // user.setAge(12);
  // int i = mapper.updateUserByUserId(user);
  // sqlSession.commit();


  // 第二次查询
  System.out.println("第一次查询....");
  UserMapper mapper1 = sqlSession.getMapper(UserMapper.class);
  User user1 = mapper1.selectUserById(1);
  System.out.println(user1);
  sqlSession.close();
}

运行结果图

java
第一次查询....
==>  Preparing: select * from t_user where id=?; 
==> Parameters: 1(Integer)
<==    Columns: id, name, age
<==        Row: 1, 嘿嘿, 21
<==      Total: 1
User(id=1, name=嘿嘿, age=21)
第一次查询....
User(id=1, name=嘿嘿, age=21)

在第一次查询的时候,发送了 sql 去数据库中去查询,在第二次查询的时候没有发送任何 sql,并且返回了同样的结果,这就说明在第二次查询的时候,并没有去数据库查询而是去缓存中拿

修改操作

测试清空缓存的操作,放开上面注释的修改数据的操作

java
第一次查询....
==>  Preparing: select * from t_user where id=?; 
==> Parameters: 1(Integer)
<==    Columns: id, name, age
<==        Row: 1, 嘿嘿, 21
<==      Total: 1
User(id=1, name=嘿嘿, age=21)
==>  Preparing: update t_user set name=?,age=? where id=? 
==> Parameters: 哈哈(String), 12(Integer), 1(Integer)
<==    Updates: 1
第二次查询....
==>  Preparing: select * from t_user where id=?; 
==> Parameters: 1(Integer)
<==    Columns: id, name, age
<==        Row: 1, 哈哈, 12
<==      Total: 1
User(id=1, name=哈哈, age=12)

执行了 update 更新语句的时候,MyBatis 清空了一级缓存,第二次查询的时候发送了 Sql 去数据库中查询

一级缓存实现

按照先去查询,在执行 update 操作,然后再去查询的步骤探寻源码的执行。在这里直接找到缓存的执行的逻辑,不在去看其他的执行细节,因为之前源码分析的环节,已经分析过详细的执行流程

第一次查询操作

第一次在执行 openSession 的时候,会创建 Executor 对象,根据执行器的类型 ExecutorType 选择的是 SIMPLE 类型,

java
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;
}

然后创建的 SimpleExecutor 对象,在这个对象创建的过程中,是调用的父类 BaseExecutor 对象,在父类实例化的过程中,创建PerpetualCache 缓存对象

java
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;
}

紧接着又创建了 CachingExecutor 对象,进到这个对象中,这里是处理二级缓存的地方装饰器对象,在执行的查询的时候,MyBatis 会先去二级缓存中去查找

java
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  // Cache 是从 MappedStatement 中获取到的,而 MappedStatement 又和每一个<insert>、<delete>、<update>、<select>绑定并在MyBatis启动的时候存入Configuration中:
  Cache cache = ms.getCache();
  if(cache != null) {
    this.flushCacheIfRequired(ms);
    if(ms.isUseCache() && resultHandler == null) {
      this.ensureNoOutParams(ms, parameterObject, boundSql);/
        List list = (List)this.tcm.getObject(cache, key);
      if(list == null) {
        list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
        this.tcm.putObject(cache, key, list);
      }
      return list;
    }
  }
  //先读取二级缓存,再去查询
  return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);

}

接下来的查询从一级缓存中查找,测试的是第一次查询,一级缓存中是没有数据

java
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  // 删除不必要代码。。。。
  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;
}

到这里一级缓存没有,就要去数据库中查询了

java
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;
}

到这里第一查询就完成了,主要是去数据库中查询中结果,并且放入数据库中

更新数据操作

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

在执行更新操作前 ,会先清空一级缓存,在第二次执行的时候,缓存中已经没有数据了。其他的删除、新增等操作对对缓存的影响,也是同样的逻辑(因为新增和删除底层都是执行的 update() 方法)

二级缓存

二级缓存是 mapper(namespace) 级别的缓存,也就是说多个 sqlsession 对同一个 Mapper 进行查询操作时是可以共享这个namespace 中的缓存数据,但是当对当前这个 mapper 中的进行增、删、改或者 commit 的时候也会清空缓存的

sqlSession1 在去查询数据的时候都会先去二级缓存中去查,如果没有的话在去数据库中查询,同时会存入到二级缓存中,等sqlsession2 再去查询的时候,就可以直接从个缓存中拿了,但是 sqlsession3 去查询之前 sqlsession2 修改了查询的这个数据,那么就会二级缓存就会被清空;这样才不会出现脏数据的情况

开启二级缓存

MyBatis 二级缓存是需要手动开启的,这里需要两步配置

在核心配置文件中(mybatis-config.xml

xml
 <settings>
    <!--全局缓存开关,默认开启-->
    <setting name="cacheEnabled " value="true"/>
  </settings>

映射文件中(UserMapper.xml

java
 <cache/>

测试代码

java
// 使用二级缓存实体类要实现序列化接口
@Test
public void testCache2() throws Exception {
  InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
  SqlSessionFactory build = new SqlSessionFactoryBuilder().build(inputStream);
  SqlSession sqlSession = build.openSession();
  UserMapper mapper = sqlSession.getMapper(UserMapper.class);
  
  System.out.println("===============第一次查询===================");
  User user = mapper.selectUserById(1);
  sqlSession.close();
  System.out.println(user);
  
  // 第二次查询
  System.out.println("===============第二次查询===================");
  SqlSession sqlSession2 = build.openSession();
  UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
  User user2 = mapper2.selectUserById(1);
  System.out.println(user2);

  System.out.println("===============更新数据===================");
  //        user.setName("嘿嘿");
  //        user.setAge(21);
  //        mapper2.updateUserByUserId(user);
  //        sqlSession2.commit();
  //        sqlSession2.close();
  
  // 第三次查询
  System.out.println("===============第三次查询==================");
  SqlSession sqlSession3 = build.openSession();
  UserMapper mapper1 = sqlSession3.getMapper(UserMapper.class);
  User user1 = mapper1.selectUserById(1);
  System.out.println(user1);
  sqlSession3.close();
}

查询操作

只有查询操作测试结果

java
===============第一次查询===================
Cache Hit Ratio [test.UserMapper]: 0.0
==>  Preparing: select * from t_user where id=?; 
==> Parameters: 1(Integer)
<==    Columns: id, name, age
<==        Row: 1, 哈哈1, 12
<==      Total: 1
User{id=1, username='null', age=12}
===============第二次查询===================
Cache Hit Ratio [test.UserMapper]: 0.5
User{id=1, username='null', age=12}
===============更新数据===================
===============第三次查询==================
Cache Hit Ratio [test.UserMapper]: 0.6666666666666666
User{id=1, username='null', age=12}

第一次查询的时候,出现了 Cache Hit Ratio [test.UserMapper]: 0.0 表示命中率的意思,0.0 说明是缓存没有命中,并且发送了一条sql查询语句;

第二查询的时候命中率是 0.5 说明两次查询命中了一次, 50% 的命中率,更新操作被注释没有执行,

第三次在此查询命中率是 0,66666666 说明是三次查询命中了2次,命中率约是 66%,第二次和第三次查询都没有发送 sql 语句;说明二级缓存是存在并且生效的。

修改操作

java
===============第一次查询===================
==>  Preparing: select * from t_user where id=?; 
==> Parameters: 1(Integer)
<==    Columns: id, name, age
<==        Row: 1, 哈哈1, 12
<==      Total: 1
User{id=1, username='null', age=12}
===============第二次查询===================
Cache Hit Ratio [test.UserMapper]: 0.5
User{id=1, username='null', age=12}
===============更新数据===================
==>  Preparing: update t_user set name=?,age=? where id=? 
==> Parameters: 嘿嘿(String), 21(Integer), 1(Integer)
<==    Updates: 1
===============第三次查询==================
Cache Hit Ratio [test.UserMapper]: 0.3333333333333333
==>  Preparing: select * from t_user where id=?; 
==> Parameters: 1(Integer)
<==    Columns: id, name, age
<==        Row: 1, 嘿嘿, 21
<==      Total: 1
User{id=1, username='嘿嘿', age=21}

前两次查询和上次是一样的,命中率是0.5,执行更新操作之后,命中率变成了0.33,而且有发送了一条sql语句,说明三次查询中命中了1次

源码探秘

mybatis 开始解析映射文件的时候,会解析配置的 <cache/> 标签,并把标签中的相关属性保存到Configuration对象中,创建的执行器的时候会判断全局缓存开关,创建本地缓存开关

java
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) {
    // CachingExecutor是对SimpleExecutor类的装饰器类,给其加上二级缓存的功能
    executor = new CachingExecutor(executor);
  }
  // 拦截器链
  executor = (Executor) interceptorChain.pluginAll(executor);
  return executor;
}

第一次查询就不再说了,前面已经说了很多次了,咱么直接进入第二次查询,跟着断点走,在查询方法前会有创建缓存 key 的方法

java
@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
  if (closed) {
    throw new ExecutorException("Executor was closed.");
  }
  // 创建缓存CacheKey对象,
  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());
  }
  // 返回创建的cacheKey
  return cacheKey;
}

最终经过以上多个参数的设置拼接成一个很长的 cacheKey

后需在查询的时候直接先去二级缓存查

java
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事务缓存对象中
        tcm.putObject(cache, key, list); // issue #578 and #116
      }
      return list;
    }
  }
  return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

这次就直接从存储到的存储到二级缓存中直接查找,找到之后返回。这里是从事务对象 tcm 中查找,而这里的事务缓存也是经过层层委托的对象

更新操作

跟着断点走进到 updata 方法

java
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
  // 必要的时候刷新缓存
  flushCacheIfRequired(ms);
  // 执行更新操作
  return delegate.update(ms, parameterObject);
}

进到上面刷新的方法会看到

java
public void clear() {
  // 将这个commit时清空设置成了true
  clearOnCommit = true;
  entriesToAddOnCommit.clear();
}

会发现在 update() 方法中只有清空一级缓存的缓存的方法,而真正清空缓存的方法是在 commit 方法中,所以二级缓存的使用一定注意提交事务,上面咱么说了他是委托了 tcm 对象管理管理缓存的。

在解析完成 <cache/> 标签之后,紧接着会解析 <cache-ref=“namespace2”/> 标签,这个标签就是引用其他的 mapper 的缓存的,也就是可以多个 mapper 共用一个 mapper 内的缓存;但是这里有一问题就是,通常情况下查询都是涉及多张表的,所以这里就很可能出现脏数据的情况,所以这里通常不引用其他 mapper,如果要集成其他第三方缓存或者自定义缓存的时候,可以使用。

MyBatis的缓存是和整个应用运行在同一个 JVM 中的,共享同一块堆内存,所以如果要缓存的数据量很大的话,建议是用其他的缓存框架,如:ReidsMemcache

版本差异(MyBatis 3.x → 3.5.x)

特性旧版(3.4.x 及以前)当前(3.5.x)
一级缓存SqlSession 级别(本地缓存)不变,仍是 SqlSession 生命周期
二级缓存namespace 级别,需手动开启不变,cache 标签配置,注意 commit 后生效
缓存键CacheKey 计算一致3.5.13+ 对 record 类型映射的缓存键支持增强
第三方缓存需实现 Cache 接口不变,可集成 Redis/Memcached(如 mybatis-redis)
虚拟线程不支持3.5.9+ 优化虚拟线程环境下的缓存访问兼容

缓存机制(一级缓存默认开启、二级缓存需配置 <cache/> 并提交事务)在 3.5.x 中完全一致;注意多表查询时二级缓存脏数据风险依然存在,建议仅对单表查询开启。