Mybatis缓存

关于Mybatis的执行流程,点这里

一级缓存

一级缓存配置

在MyBatis的配置文件中,添加如下语句,就可以使用一级缓存。共有两个选项,SESSION或者STATEMENT,默认是SESSION级别,即在一个MyBatis会话中执行的所有语句,都会共享这一个缓存。一种是STATEMENT级别,可以理解为缓存只对当前执行的这一个Statement有效。

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

一级缓存执行流程

下图来自美团技术团队文章
一级缓存执行流程

验证一级缓存

数据库为MySQL8.0.19
数据表来自MySQL官方数据库employees

同一会话中的同一sql使用缓存

@Test
public void testFirstLevelCache() {
    try (SqlSession sqlSession = sessionFactory.openSession()){
        final DepartmentMapper mapper = sqlSession.getMapper(DepartmentMapper.class);
        System.out.println(mapper.getById("d002") == mapper.getById("d002"));
    }
}

执行结果,只发起一次数据库请求,两次查询结果为同一对象
一级缓存.png

执行修改操作后,一级缓存失效

try (SqlSession sqlSession = sessionFactory.openSession()) {
    final DepartmentMapper mapper = sqlSession.getMapper(DepartmentMapper.class);
    final Department department1 = mapper.getById("d002");
    final Department department = new Department();
    department.setDeptNo("d002");
    department.setDeptName("Finance");
    mapper.updateById(department);
    final Department department2 = mapper.getById("d002");
    System.out.println(department2 == department1);
}

执行结果,发起了两次数据库请求
一级缓存修改操作后失效.png

基于sql语句的缓存

try (SqlSession sqlSession = sessionFactory.openSession()) {
    final DepartmentMapper mapper = sqlSession.getMapper(DepartmentMapper.class);
    mapper.getAll();
    mapper.getById("d002");
}

基于sql语句的缓存.png

一级缓存同一会话有效

try (SqlSession sqlSession1 = sessionFactory.openSession();
     SqlSession sqlSession2 = sessionFactory.openSession()) {
    final DepartmentMapper mapper1 = sqlSession1.getMapper(DepartmentMapper.class);
    final DepartmentMapper mapper2 = sqlSession2.getMapper(DepartmentMapper.class);
    System.out.println(mapper1.getById("d002"));
    final Department department = new Department();
    department.setDeptNo("d002");
    department.setDeptName("我被修改了");
    mapper2.updateById(department);
    System.out.println(mapper1.getById("d002"));
    System.out.println(mapper2.getById("d002"));
}

一级缓存同一会话有效.png
session2修改了数据,session1读取了自己缓存中的脏数据

手动清除缓存

try (SqlSession sqlSession = sessionFactory.openSession()){
    final DepartmentMapper mapper = sqlSession.getMapper(DepartmentMapper.class);
    mapper.getById("d002");
    sqlSession.clearCache();
    mapper.getById("d002");
}

清除一级缓存.png

Mybatis缓存的实现类

mybatis缓存实现.png

总结

  • MyBatis一级缓存的生命周期和SqlSession一致。
  • MyBatis的一级缓存最大范围是SqlSession内部,有多个SqlSession或者分布式的环境下,数据库写操作会引起脏数据,建议设定缓存级别为Statement。
  • 一级缓存失效四种情况
    1. 查询条件不同
    2. 执行了修改操作
    3. 不同的sqlSession
    4. 手动清除一级缓存(sqlSession的clearCache方法)

二级缓存

一级缓存中,其最大的共享范围就是一个SqlSession内部,如果多个SqlSession之间需要共享缓存,则需要使用到二级缓存。进入一级缓存的查询流程前,先在CachingExecutor进行二级缓存的查询
具体流程如下(下图来自美团技术团队文章)

二级缓存开启执行流程

二级缓存开启后,同一个namespace下的所有操作语句,都影响着同一个Cache,即二级缓存被多个SqlSession共享,是一个全局的变量。

当开启缓存后,数据的查询执行的流程就是 二级缓存 -> 一级缓存 -> 数据库。

二级缓存配置

  1. 在主配置文件中
<setting name="cacheEnabled" value="true"/>
  1. 在需要开启二级缓存的mapper中
<cache/>

cache标签有以下选项

  • type:cache使用的类型,默认是PerpetualCache,简单的Map实现。
  • eviction: 定义回收的策略,常见的有FIFO,LRU。
  • flushInterval: 配置一定时间自动刷新缓存,单位是毫秒。
  • size: 最多缓存对象的个数。
  • readOnly: 是否只读,若配置可读写,则需要对应的实体类能够序列化。
  • blocking: 若缓存中找不到对应的key,是否会一直blocking,直到有对应的数据进入缓存。
    cache-ref代表引用别的命名空间的Cache配置,两个命名空间的操作使用的是同一个Cache。
<cache-ref namespace="com.rufeng.mapper.SalaryMapper"/>

二级缓存测试

提交事务之前二级缓存无效

SqlSession sqlSession1 = sessionFactory.openSession(false);
        SqlSession sqlSession2 = sessionFactory.openSession(false);
        final DepartmentMapper mapper1 = sqlSession1.getMapper(DepartmentMapper.class);
        final DepartmentMapper mapper2 = sqlSession2.getMapper(DepartmentMapper.class);
        mapper1.getById("d002");
//        sqlSession1.close();
        mapper2.getById("d002");

        sqlSession1.close();
        sqlSession2.close();

走两次数据库
提交事务前无效.png
二级缓存的默认实现是由TansactionalCacheManager管理的TansactionCache委托PerpetualCache实现,可以配置为上文提到的其他实现

TransactionalCacheManage主要方法

private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
  ...
  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 MapUtil.computeIfAbsent(transactionalCaches, cache, TransactionalCache::new);
  }

TransactionalCahceManager的方法,其最终还是调用的TransactionalCache的方法,完全是管理中介的作用

TransactionalCache主要属性和方法

private final Cache delegate;
private boolean clearOnCommit;
private final Map<Object, Object> entriesToAddOnCommit;
private final Set<Object> entriesMissedInCache;

@Override
public Object getObject(Object key) {
	// issue #116
	Object object = delegate.getObject(key);
	if (object == null) {
	  entriesMissedInCache.add(key);
	}
	// issue #146
	if (clearOnCommit) {
	  return null;
	} else {
	  return object;
	}
}

@Override
public void putObject(Object key, Object object) {
    entriesToAddOnCommit.put(key, object);
}

public void commit() {
    if (clearOnCommit) {
      delegate.clear();
    }
    flushPendingEntries();
    reset();
  }

  public void rollback() {
    unlockMissedEntries();
    reset();
  }

  private void reset() {
    clearOnCommit = false;
    entriesToAddOnCommit.clear();
    entriesMissedInCache.clear();
  }

  private void flushPendingEntries() {
    for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
      delegate.putObject(entry.getKey(), entry.getValue());
    }
    for (Object entry : entriesMissedInCache) {
      if (!entriesToAddOnCommit.containsKey(entry)) {
        delegate.putObject(entry, null);
      }
    }
  }
  • 调用put时,往entriesToaddOnCommit中添加
  • 调用get时,从delegate中取,delegate是真正存储缓存数据的对象
  • flushPendingEntries方法真正向缓存中添加数据
  • commit,如果每次缓存只对一次事务有效(clearOnCommit),清空已有缓存,将暂存数据添加到缓存

提交事务后,二级缓存生效

SqlSession sqlSession1 = sessionFactory.openSession(false);
SqlSession sqlSession2 = sessionFactory.openSession(false);
final DepartmentMapper mapper1 = sqlSession1.getMapper(DepartmentMapper.class);
final DepartmentMapper mapper2 = sqlSession2.getMapper(DepartmentMapper.class);
mapper1.getById("d002");
sqlSession1.close();
mapper2.getById("d002");
sqlSession2.close();

提交事务后,二级缓存生效.png
提交之后,数据在delegate缓存中,getObject才能拿到缓存数据

修改数据后,缓存失效

SqlSession sqlSession1 = sessionFactory.openSession(false);
SqlSession sqlSession2 = sessionFactory.openSession(false);
final DepartmentMapper mapper1 = sqlSession1.getMapper(DepartmentMapper.class);
final DepartmentMapper mapper2 = sqlSession2.getMapper(DepartmentMapper.class);
mapper1.getById("d002");
final Department department = new Department();
department.setDeptNo("d002");
department.setDeptName("Finance");
mapper1.updateById(department);
sqlSession1.close();
mapper2.getById("d002");
sqlSession2.close();

修改数据后,缓存失效.png

不适用于多表情况

验证MyBatis的二级缓存不适应用于映射文件中存在多表查询的情况。

通常我们会为每个单表创建单独的映射文件,由于MyBatis的二级缓存是基于namespace的,多表查询语句所在的namspace无法感应到其他namespace中的语句对多表查询中涉及的表进行的修改,引发脏数据问题。

为了解决这个问题,可以使用Cache ref,让多个Mapper指向同一命名空间,这样两个映射文件对应的SQL操作都使用的是同一块缓存了。

不过这样做的后果是,缓存的粒度变粗了,多个Mapper namespace下的所有操作都会对缓存使用造成影响。

总结

总结
MyBatis的二级缓存相对于一级缓存来说,实现了SqlSession之间缓存数据的共享,同时粒度更加的细,能够到namespace级别,通过Cache接口实现类不同的组合,对Cache的可控性也更强。
MyBatis在多表查询时,极大可能会出现脏数据,有设计上的缺陷,安全使用二级缓存的条件比较苛刻。
在分布式环境下,由于默认的MyBatis Cache实现都是基于本地的,分布式环境下必然会出现读取到脏数据,需要使用集中式缓存将MyBatis的Cache接口实现,有一定的开发成本,直接使用Redis、Memcached等分布式缓存可能成本更低,安全性也更高。
建议MyBatis缓存特性在生产环境中进行关闭,单纯作为一个ORM框架使用可能更为合适。

参考

聊聊Mybatis的缓存机制

Q.E.D.


一切很好,不缺烦恼。