# MyBatis源码-缓存

MyBatis 缓存使用及如何实现

# 缓存的作用

MyBatis 支持1级、2级缓存,减少了数据库查询的频次,提升了查询的效率。我们需要了解下 MyBatis 缓存的使用方式及源码逻辑,从而减少在使用缓存过程中出现的不必要的问题出现。

# 缓存效果

# 1级缓存

    @Test
    public void testLocalCache() throws Exception {
        SqlSession sqlSession = factory.openSession(true); // 自动提交事务
        StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);

        System.out.println(studentMapper.getStudentById(1));
        System.out.println(studentMapper.getStudentById(1));
        System.out.println(studentMapper.getStudentById(1));

        sqlSession.close();
    }

输出结果

DEBUG [main] - ==>  Preparing: SELECT id,name,age FROM student WHERE id = ? 
DEBUG [main] - ==> Parameters: 1(Integer)
TRACE [main] - <==    Columns: id, name, age
TRACE [main] - <==        Row: 1, 点点, 16
DEBUG [main] - <==      Total: 1
entity.StudentEntity@f79a760
DEBUG [main] - Cache Hit Ratio [mapper.StudentMapper]: 0.0
entity.StudentEntity@f79a760
DEBUG [main] - Cache Hit Ratio [mapper.StudentMapper]: 0.0
entity.StudentEntity@f79a760

从上面的测试当中可以看出,我们连续执行了3次查询操作,真正走到数据库进行查询的也就1次,后边的2次查询走了缓存,并且从输出的对象内存地址发现是一个对象。

如果我们对数据进行增、删、改操作,然后再次查询是否还会走缓存呢?

    @Test
    public void testLocalCacheClear() throws Exception {
        SqlSession sqlSession = factory.openSession(true); // 自动提交事务
        StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);

        System.out.println(studentMapper.getStudentById(1));
        System.out.println("增加了" + studentMapper.addStudent(buildStudent()) + "个学生");
        System.out.println(studentMapper.getStudentById(1));

        sqlSession.close();
    }

输出结果

DEBUG [main] - ==>  Preparing: SELECT id,name,age FROM student WHERE id = ? 
DEBUG [main] - ==> Parameters: 1(Integer)
TRACE [main] - <==    Columns: id, name, age
TRACE [main] - <==        Row: 1, 点点, 16
DEBUG [main] - <==      Total: 1
entity.StudentEntity@1eb6749b
DEBUG [main] - ==>  Preparing: INSERT INTO student(name,age) VALUES(?, ?) 
DEBUG [main] - ==> Parameters: 明明(String), 20(Integer)
DEBUG [main] - <==    Updates: 1
增加了1个学生
DEBUG [main] - Cache Hit Ratio [mapper.StudentMapper]: 0.0
DEBUG [main] - ==>  Preparing: SELECT id,name,age FROM student WHERE id = ? 
DEBUG [main] - ==> Parameters: 1(Integer)
TRACE [main] - <==    Columns: id, name, age
TRACE [main] - <==        Row: 1, 点点, 16
DEBUG [main] - <==      Total: 1
entity.StudentEntity@24d4d7c9

从上面执行结果可以看出,当对数据进行变更操作时,缓存就会失效。

那如果 SqlSession 不一致时,是否会走缓存呢?

    @Test
    public void testLocalCacheScope() throws Exception {
        SqlSession sqlSession1 = factory.openSession(true); // 自动提交事务
        SqlSession sqlSession2 = factory.openSession(true); // 自动提交事务

       StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
       StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);

        System.out.println("studentMapper读取数据: " + studentMapper.getStudentById(1));
        System.out.println("studentMapper读取数据: " + studentMapper.getStudentById(1));
        System.out.println("studentMapper2更新了" + studentMapper2.updateStudentName("小岑",1) + "个学生的数据");
        System.out.println("studentMapper读取数据: " + studentMapper.getStudentById(1));
        System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentById(1));

    }

输出结果

DEBUG [main] - ==>  Preparing: SELECT id,name,age FROM student WHERE id = ? 
DEBUG [main] - ==> Parameters: 1(Integer)
TRACE [main] - <==    Columns: id, name, age
TRACE [main] - <==        Row: 1, 点点, 16
DEBUG [main] - <==      Total: 1
studentMapper读取数据: entity.StudentEntity@3b220bcb
DEBUG [main] - Cache Hit Ratio [mapper.StudentMapper]: 0.0
studentMapper读取数据: entity.StudentEntity@3b220bcb
DEBUG [main] - ==>  Preparing: UPDATE student SET name = ? WHERE id = ? 
DEBUG [main] - ==> Parameters: 小岑(String), 1(Integer)
DEBUG [main] - <==    Updates: 1
studentMapper2更新了1个学生的数据
DEBUG [main] - Cache Hit Ratio [mapper.StudentMapper]: 0.0
studentMapper读取数据: entity.StudentEntity@3b220bcb
DEBUG [main] - Cache Hit Ratio [mapper.StudentMapper]: 0.0
DEBUG [main] - ==>  Preparing: SELECT id,name,age FROM student WHERE id = ? 
DEBUG [main] - ==> Parameters: 1(Integer)
TRACE [main] - <==    Columns: id, name, age
TRACE [main] - <==        Row: 1, 小岑, 16
DEBUG [main] - <==      Total: 1
studentMapper2读取数据: entity.StudentEntity@618c5d94

SqlSession 不一致时,在1级缓存下会出现读取脏数据的问题。

上面创建了两个 SqlSessionstudentMapper 连续查询了2次,第一次查询数据库,第二次是从缓存中取的,这个时候 studentMapper2 对数据进行变更,studentMapper 再次查询的时候还是走了缓存。

# 小结

1级缓存(默认1级缓存时开启的)只对单个 SqlSession 生效,当对数据进行变更操作时缓存会失效。

# 1级缓存相关源码

image-20250225104820587

Executor : 是执行器接口,对数据库的所有操作都是通过这个入口进行的。

BaseExecutor :抽象类实现了执行器 Executor 接口,具体执行动作委托子类进行如 doUpdatedoQuery

BatchExecutor :批量操作,会缓存 Statement 对象,执行 update 的时候进行批量处理。

ReuseExecutor :重复执行器,会缓存 Statement 对象,其定义了一个Map<String, Statement>,将执行的sql作为key,将执行的Statement作为value保存。

当把 cacheEnabled 设置为 false 可以关闭2级缓存的情况下,这个时候会走 BaseExecutor类。

//BaseExecutor类
//查询相关的操作最终都会走到这里
  @Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    //查询的sql语句及相关参数
    BoundSql boundSql = ms.getBoundSql(parameter);
    //创建一个缓存key
    //组装完成的缓存key 如:-1824555229:1330539143:mapper.StudentMapper.getStudentById:0:2147483647:SELECT id,name,age FROM student WHERE id = ?:1:development
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
 }

//BaseExecutor类
//这个方法主要就是创建缓存的key,然后根据key查询缓存
//key主要有这几部分组成:Statement Id + Offset + Limmit + Sql + Params
@Override
  public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    CacheKey cacheKey = new CacheKey();
      //Statement Id
    cacheKey.update(ms.getId());
      //Offset 分页
    cacheKey.update(rowBounds.getOffset());
      //Limmit 分页
    cacheKey.update(rowBounds.getLimit());
      //Sql
    cacheKey.update(boundSql.getSql());
      //处理参数 Params
    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;
  }
//BaseExecutor类
//执行查询方法 如果缓存有就从缓存中取,下面源码主要关注清除缓存的时机
@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.");
    }
      //如果设置 flushCache="true" 每次查询都会清除之前的缓存
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }
    List<E> list;
    try {
      queryStack++;
        //先从缓存中取 如果没有从数据库中查询
        //PerpetualCache localCache 内部是通过一个简单的hashmap 实现了缓存
      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();
        //如果 设置localCacheScope = STATEMENT ,每次都会把缓存清除,也就是彻底关闭了1级缓存
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }

# 2级缓存

2级缓存是 namespace 层面的,也就是 mapper 对应的相关 sql 。

配置开启方式如下:

<!--在 mybatis 配置文件里边配置开启 2级 缓存-->
<setting name="cacheEnabled" value="true"/>
<!--在 mapper 里边配置 cache 节点-->
<mapper namespace="mapper.StudentMapper">
    <cache/>
</mapper>
    @Test
    public void testCacheWithUpdate() throws Exception {
        SqlSession sqlSession1 = factory.openSession(true); // 自动提交事务
        SqlSession sqlSession2 = factory.openSession(true); // 自动提交事务
        SqlSession sqlSession3 = factory.openSession(true); // 自动提交事务


        StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
        StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
        StudentMapper studentMapper3 = sqlSession3.getMapper(StudentMapper.class);


        System.out.println("studentMapper读取数据: " + studentMapper.getStudentById(1));
        sqlSession1.close();
        System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentById(1));

        studentMapper3.updateStudentName("方方",1);
        sqlSession3.commit();
        System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentById(1));
    }

输出结果

DEBUG [main] - ==>  Preparing: SELECT id,name,age FROM student WHERE id = ? 
DEBUG [main] - ==> Parameters: 1(Integer)
TRACE [main] - <==    Columns: id, name, age
TRACE [main] - <==        Row: 1, 方方, 16
DEBUG [main] - <==      Total: 1
studentMapper读取数据: entity.StudentEntity@619bfe29
DEBUG [main] - Cache Hit Ratio [mapper.StudentMapper]: 0.5
studentMapper2读取数据: entity.StudentEntity@56ace400
DEBUG [main] - ==>  Preparing: UPDATE student SET name = ? WHERE id = ? 
DEBUG [main] - ==> Parameters: 方方(String), 1(Integer)
DEBUG [main] - <==    Updates: 1
DEBUG [main] - Cache Hit Ratio [mapper.StudentMapper]: 0.3333333333333333
DEBUG [main] - ==>  Preparing: SELECT id,name,age FROM student WHERE id = ? 
DEBUG [main] - ==> Parameters: 1(Integer)
TRACE [main] - <==    Columns: id, name, age
TRACE [main] - <==        Row: 1, 方方, 16
DEBUG [main] - <==      Total: 1
studentMapper2读取数据: entity.StudentEntity@3234f74e

上面的实验是开启了2级缓存,创建了3个 SqlSession sqlSession1 查询后关闭,不影响 sqlSession2查询, sqlSession2 并且走了缓存。 sqlSession3 对数据进行更新操作, sqlSession2 查询的时候也重新从数据库查询,缓存失效。

# 小结

2级缓存是对整个 mapper 生效,多个 SqlSession 操作同一个 mapper 会走缓存并且也会影响缓存。

# 2级缓存源码

2级缓存创建基本和1级缓存一样,不同点在于获取缓存。

//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);
    }
    //重点看这里!!! 
    //如果开启2级缓存 就使用CachingExecutor对执行器进行一次装饰,得到一个 CachingExecutor 对象
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }
//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) {
        //判断是否需要清除缓存
      flushCacheIfRequired(ms);
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, parameterObject, boundSql);
        @SuppressWarnings("unchecked")
          //从TransactionalCacheManager缓存管理器中根据key查询是否有缓存信息
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
          list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

参考文章

  1. https://www.cnblogs.com/redwinter/p/16608113.html
  2. https://tech.meituan.com/2018/01/19/mybatis-cache.html (实验demo基本都是参考这个)
  3. https://www.cnblogs.com/wwct/p/12994222.html
上次更新: 2025/2/25