2

Mybatis内置了强大的事务性查询缓存机制,正确使用Mybatis的缓存机制可以有效提高应用的性能。因为一般情况下我们应用的大部分性能消耗都和数据库查询有关,如果能够有效命中缓存、适当避免或减少与数据库的交互,一定是改善应用性能的不二选择。

但是缓存机制是一把双刃剑,不恰当的使用缓存可能会导致数据不一致的问题。

决不能为了解决性能问题而引入数据不一致问题,这是每个开发人员都应该具备的一个基本认识。

所以,为了既能够优雅的解决性能问题,又能够安全的规避数据不一致问题。我们有必要对Mybatis的缓存机制做一个透彻的了解。

认识Mybatis缓存机制

Mybatis提供了两种不同的缓存机制,也就是我们经常说的一级缓存、二级缓存。

一级缓存:也叫LocalCache,是基于SqlSession的缓存,也就是说缓存是存储在SqlSession这个级别的,会随着SqlSession的关闭而消失。

二级缓存:是跨SqlSession生命周期的,二级缓存的作用域是定义在namespace级别的,对应着mapper.xml文件的定义。跨作用域生效需要进行专门的定义。

其实程序员应该认真对待的是Mybatis的二级缓存机制,因为一级缓存机制是默认开启的、几乎也不存在数据不一致的问题,二级缓存机制是需要通过配置开启、而且用不好很容易导致数据不一致的问题。

而且,一级缓存使用起来应该非常简单,所以Mybatis官网对于一级缓存的介绍几乎没有,而官网有关缓存的说明文字几乎通篇都是介绍二级缓存的(参考https://mybatis.org/mybatis-3...)。

建议大家对官网文档做认真学习研究,对于官网已经交代过的内容,我们就不再啰嗦了,本篇文章主要补充一些官网文章没有提及的有关Mybatis缓存机制的技术细节。

一级缓存

一级缓存是默认开启的,我们不需要做任何配置就可以使用Mybatis的一级缓存。

如果你不想开启Mybatis的一级缓存,可以通过Settings配置做全局关闭(localCacheScope=STATEMENT)。

如果你只是针对某一句sql不想开启一级缓存,则可以在mapping的xml文件中针对该sql语句配置flushCache为true,注意flushCache对一级缓存和二级缓存都生效。

建议还是采用Mybatis的默认配置,开启一级缓存,尤其是对于那些有初级程序员参与的项目,他们对性能的问题考虑的可能会少一点,在一个交易中对同一条数据可能会多次查询,这个时候我们当然可以通过框架设计、培训、code review等方法尽可能避免这种情况,但是如果无法避免的话,起码Mybatis的一级缓存可以帮助我们在一定程度上避免无谓的数据库请求。

二级缓存#开启

官网说的非常明白,二级缓存需要在mapping配置文件中手动开启,否则Mybatis不会自动开启二级缓存。

摘抄官网的一段话:

MyBatis 内置了一个强大的事务性查询缓存机制,它可以非常方便地配置和定制。 为了使它更加强大而且易于配置,我们对 MyBatis 3 中的缓存实现进行了许多改进。

默认情况下,只启用了本地的会话缓存,它仅仅对一个会话中的数据进行缓存。 要启用全局的二级缓存,只需要在你的 SQL 映射文件中添加一行:
<cache/>
基本上就是这样。这个简单语句的效果如下:

  1. 映射语句文件中的所有 select 语句的结果将会被缓存。
  2. 映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存。
  3. 缓存会使用最近最少使用算法(LRU, Least Recently Used)算法来清除不需要的缓存。
  4. 缓存不会定时进行刷新(也就是说,没有刷新间隔)。
  5. 缓存会保存列表或对象(无论查询方法返回哪种)的 1024 个引用。
  6. 缓存会被视为读/写缓存,这意味着获取到的对象并不是共享的,可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。

不开启二级缓存的情况下,Mybatis底层用BaseExecutor对象执行sql,开启后使用CachingExecutor执行sql,CachingExecutor在创建的时候会持有Mapper初始化过程中以装饰器模式创建好的层层包装的SynchronizedCache对象,sql语句的执行结果最终由这个包装器持有从而实现缓存。

二级缓存#作用范围

我们前面已经说过了二级缓存的作用范围是namespace,Mybatis官网也有一个与句话的提示:

提示 缓存只作用于 cache 标签所在的映射文件中的语句。如果你混合使用 Java API 和 XML 映射文件,在共用接口中的语句将不会被默认缓存。你需要使用 @CacheNamespaceRef 注解指定缓存作用域。

我们一定要对这句话有足够的重视,因为这个地方如果使用不当的话,也是容易引起缓存一致性问题的。

我们前面开篇也说过数据一致性问题,这里做一个简单的解释:

引入缓存的目的是为了尽可能减少数据库I/O从而提高系统性能的,如果第一次查询从数据库中获取到了id=1的用户数据并缓存起来,第二次再执行该查询获取id=1的用户数据的时候,Mybatis就会检查缓存中是否已经有该数据,如果有的话Mybatis就不会再次执行数据库查询了,会直接给请求端返回缓存中的数据。这个时候我们也经常称为缓存命中。

如果在两次查询之间有操作修改了id=1的用户信息怎么办?Mybatis如果不知道本次修改而直接返回了缓存数据的话,就会导致数据不一致的问题,因为应用给前端返回了不正确的数据。

Mybatis当然有自己的解决方案,这个解决方案就是在更新数据的时候刷新缓存,刷新缓存其实就是清空缓存,那么在修改之后的首次查询就会由于无法命中缓存而通过数据库查询来获取数据,这样的话就不会导致数据不一致的问题了。

但是现实情况往往会比你想象中的复杂,比如说你的用户查询的sql语句定义在mapperA.xml中,而用户更新的sql语句定义在mapperB.xml中。

这种情况下,悲剧就发生了,用户更新后根本就不会触发用户信息查询的缓存刷新,因为他们两个作用域不一样,根本不在一个世界中。从技术角度来看,Mybatis底层在进行缓存的时候是以namespace或者说是mapper.xml文件为基础创建缓存对象的,上述的用户查询语句和用户更新语句的缓存对象不同,所以用户更新语句执行完成后根本就不会刷新用户查询语句所在的那个缓存对象!

我们在开发过程中一定要注意这个问题,否则你可能只知道项目中有些奇怪的问题是Mybatis二级缓存机制导致的,但是却不知道具体的底层原因,出问题之后要么就是关掉二级缓存,要么就是一通胡乱配置,flushCache或者userCache,这样有时候可能碰巧解决了问题,但是对于底层的技术原理还是没有掌握,对个人来说也没有什么帮助。

二级缓存#刷新间隔

通过参数flushInterval配置,比如:

<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>

设置Mybatis二级缓存刷新间隔为60秒,意思是两次查询时间间隔如果超过60秒的话,缓存将会被刷新。

建议这个参数要么不做配置,Mybatis默认不刷新缓存,要么就配置大一点,比如3小时。

上一篇 MybatisL拦截器
下一篇 MybatisL事务管理机制


31 声望10 粉丝