转载自 https://www.tuicool.com/articles/jqqMbaj
一个关于MyBatis的二级缓存的实际问题
现有 AMapper.xml 中定义了对数据库表 ATable 的 CRUD 操作,BMapper 定义了对数据库表 BTable 的 CRUD 操作;假设 MyBatis 的二级缓存开启,并且 AMapper 中使用了二级缓存, AMapper 对应的二级缓存为 ACache;除此之外, AMapper 中还定义了一个跟 BTable 有关的查询语句,类似如下所述:
1 | <select id="selectATableWithJoin" resultMap="BaseResultMap" useCache="true"> |
执行以下操作:
- 执行 AMapper 中的 selectATableWithJoin 操作,此时会将查询到的结果放置到 AMapper 对应的二级缓存 ACache 中;
- 执行 BMapper 中对 BTable 的更新操作( update、delete、insert )后, BTable 的数据更新;
- 再执行 1 完全相同的查询,这时候会直接从 AMapper 二级缓存 ACache 中取值,将 ACache 中的值直接返回;
好,问题就出现在第3步上:由于 AMapper 的 selectATableWithJoin 对应的 SQL 语句需要和 BTable 进行 join 查找,而在第 2 步 BTable 的数据已经更新了,但是第 3 步查询的值是第 1 步的缓存值,已经极有可能跟真实数据库结果不一样,即 ACache 中缓存数据过期了!
总结来看,就是:
对于某些使用了 join 连接的查询,如果其关联的表数据发生了更新,join 连接的查询由于先前缓存的原因,导致查询结果和真实数据不同步;从MyBatis 的角度来看,这个问题可以这样表述:对于某些表执行了更新(update、delete、insert)操作后,如何去清空跟这些表有关联的查询语句所造成的缓存。
当前的 MyBatis 的缓存机制不能很好地处理这一问题,下面我们将从当前的MyBatis的缓存机制入手,分析这一问题。
当前MyBatis二级缓存的工作机制
MyBatis 二级缓存的一个重要特点:即松散的 Cache 缓存管理和维护。一个 Mapper 中定义的增删改查操作只能影响到自己关联的 Cache 对象。如上图所示的 Mapper namespace1 中定义的若干 CRUD 语句,产生的缓存只会被放置到相应关联的 Cache1 中,即 Mapper namespace2, namespace3, namespace4 中的 CRUD 的语句不会影响到 Cache1。可以看出,Mapper 之间的缓存关系比较松散,相互关联的程度比较弱。
现在再回到上面描述的问题,如果我们将 AMapper 和 BMapper 共用一个 Cache 对象,那么,当 BMapper 执行更新操作时,可以清空对应 Cache 中的所有的缓存数据,这样的话,数据不是也可以保持最新吗?
确实这个也是一种解决方案,不过,它会使缓存的使用效率变的很低!AMapper 和 BMapper 的任意的更新操作都会将共用的 Cache 清空,会频繁地清空 Cache,导致 Cache 实际的命中率和使用率就变得很低了,所以这种策略实际情况下是不可取的。
最理想的解决方案就是:对于某些表执行了更新(update、delete、insert)操作后,如何去清空跟这些表有关联的查询语句所造成的缓存;
这样,就是以很细的粒度管理MyBatis内部的缓存,使得缓存的使用率和准确率都能大大地提升。基于这个思路,我写了一个对应的mybatis-enhanced-cache 缓存插件,可以很好地支持上述的功能。对于上述的例子中,该插件可以实现:当 BMapper 对 BTable 执行了更新操作时,指定清除与 BTable 相关联的 selectATableWithJoin 查询语句在 ACache 中产生的缓存。
接下来就来看看这个 mybatis-enhanced-cache 插件的设计原理吧
mybatis-enhanced-cache 插件的设计和工作原理
该插件主要由两个构件组成: EnhancedCachingExecutor 和 EnhancedCachingManager。
EnhancedCachingExecutor 是针对于Executor的拦截器,拦截Executor的几个关键的方法;
EnhancedCachingExecutor 主要做以下几件事:
每当有 Executor 执行 query 操作时,
1.记录下该查询 StatementId 和 CacheKey,然后将其添加到 EnhancedCachingManager 中;
2.记录下该查询 StatementId 和此 StatementId 所属 Mapper 内的 Cache 缓存对象引用,添加到 EnhancedCachingManager 中;每当 Executor 执行了update 操作时,将此 update 操作的 StatementId 传递给 EnhancedCachingManager ,让 EnhancedCachingManager 根据此 update 的 StatementId 的配置,去清空指定的查询语句所产生的缓存;
另一个构件: EnhancedCachingManager ,它也是本插件的核心,它维护着以下几样东西:
- 整个 MyBatis 的所有查询所产生的 CacheKey 集合(以 statementId 分类);
- 所有的使用过了的查询的 statementId 及其对应的 Cache 缓存对象的引用;
- update 类型的 StatementId 和查询 StatementId 集合的映射,用于当 Update 类型的语句执行时,根据此映射决定应该清空哪些查询语句产生的缓存;
如下图所示:
工作原理:原理很简单,就是当执行了某个 update 操作时,根据配置信息去清空指定的查询语句在 Cache 中所产生的缓存数据。
如何获取mybatis-enhanced-cache插件源码
- 源码和jar包2合一压缩包
- github 地址,直接 fork 即可:https://github.com/LuanLouis/mybatis-enhanced-cache
mybatis-enhanced-cache 插件的使用实例
下载 mybatis-enhanced-cache.rar 压缩包 ,解压,将其内的 mybatis-enhanced-cache-0.0.1-SNAPSHOT.jar 添加到项目的 classpath 下;
配置MyBatis配置文件如下:
1 | <plugins> |
其中,<property name=”dependency”> 中的 value 属性是 StatementId 之间的依赖关系的配置文件路径。
- 配置StatementId之间的依赖关系
1 |
|
<statement> 节点配置的是更新语句的 statementId,其内的子节点 <observer> 配置的是当更新语句执行后,应当清空缓存的查询语句的StatementId。子节点 <observer> 可以有多个。
如上的配置,则说明,如果”com.louis.mybatis.dao.DepartmentsMapper.updateByPrimaryKey”更新语句执行后,由 “com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments”语句所产生的放置在Cache缓存中的数据都都会被清空。
配置DepartmentsMapper.xml 和 EmployeesMapper.xml
1 |
|
1 |
|
测试代码
1 | package com.louis.mybatis.test; |
结果分析:
从上述的结果可以看出,前四次执行了”com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments”语句,EmployeesMapper 对应的 Cache 缓存中存储的结果缓存由 1 个增加到 4 个。
当执行了”com.louis.mybatis.dao.DepartmentsMapper.updateByPrimaryKey”后,EmployeeMapper 对应的缓存 Cache 结果被清空了,即”com.louis.mybatis.dao.DepartmentsMapper.updateByPrimaryKey”更新语句引起了 EmployeeMapper 中的” com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments”缓存的清空。
作者的话
该插件的实现周期比较短,尚未经过性能方面的测试,如果果您对此插件有任何意见或者看法,可以留言一起交流和探讨。该插件源码已经放到了Github上,可供大家自由修改,github地址:https://github.com/LuanLouis/mybatis-enhanced-cache