如何细粒度地控制你的MyBatis二级缓存(mybatis-enhanced-cache插件实现)
前几天网友chanfish 给我抛出了一个问题,笼统地讲就是如何能细粒度地控制MyBatis的二级缓存问题,酝酿了几天,觉得可以写个插件来实现这个这一功能。本文就是从问题入手,一步步分析现存的MyBatis的二级缓存的不足之处,探讨一点可以改进的地方,并且对不足之处开发一个插件进行弥补。
本文如下组织结构:
- 一个关于MyBatis的二级缓存的实际问题
 - 当前MyBatis二级缓存的工作机制
 - mybatis-enhanced-cache插件的设计和工作原理
 - mybatis-enhanced-cache 插件的使用实例
 
1.一个关于MyBatis的二级缓存的实际问题
网友chanfish 给我抛出的问题
现有AMapper.xml中定义了对数据库表
ATable的CRUD操作,BMapper定义了对数据库表BTable的CRUD操作;
假设 MyBatis 的二级缓存开启,并且
AMapper 中使用了二级缓存,AMapper对应的二级缓存为ACache;
除此之外,AMapper 中还定义了一个跟BTable有关的查询语句,类似如下所述:<select id="selectATableWithJoin" resultMap="BaseResultMap" useCache="true">
select * from ATable left join BTable on ....
</select>执行以下操作:
1. 执行AMapper中的"selectATableWithJoin" 操作,此时会将查询到的结果放置到AMapper对应的二级缓存
ACache中;2. 执行BMapper中对
BTable的更新操作(update、delete、insert)后,BTable的数据更新;3. 再执行1完全相同的查询,这时候会直接从AMapper二级缓存
ACache中取值,将ACache中的值直接返回;好,问题就出现在第3步上:
由于AMapper的“selectATableWithJoin” 对应的SQL语句需要和
BTable进行join查找,而在第 2 步BTable的数据已经更新了,但是第
3 步查询的值是第 1 步的缓存值,已经极有可能跟真实数据库结果不一样,即ACache中缓存数据过期了!总结来看,就是:
对于某些使用了 join连接的查询,如果其关联的表数据发生了更新,join连接的查询由于先前缓存的原因,导致查询结果和真实数据不同步;
从MyBatis的角度来看,这个问题可以这样表述:
对于某些表执行了更新(update、delete、insert)操作后,如何去清空跟这些表有关联的查询语句所造成的缓存;
当前的MyBatis的缓存机制不能很好地处理这一问题,下面我们将从当前的MyBatis的缓存机制入手,分析这一问题:
2. 当前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插件的设计原理吧:
3.mybatis-enhanced-cache插件的设计和工作原理
mybatis-enhanced-cache插件的设计和工作原理
该插件主要由两个构件组成:EnhancedCachingExecutor和EnhancedCachingManager。
EnhancedCachingExecutor是针对于Executor的拦截器,拦截Executor的几个关键的方法;
EnhancedCachingExecutor主要做以下几件事:
1. 每当有Executor执行query操作时,
1.1 记录下该查询StatementId和CacheKey,然后将其添加到EnhancedCachingManager中;
1.2 记录下该查询StatementId 和此StatementId所属Mapper内的Cache缓存对象引用,添加到EnhancedCachingManager中;
2. 每当Executor执行了update操作时,将此 update操作的StatementId传递给EnhancedCachingManager,让EnhancedCachingManager根据此update的StatementId的配置,去清空指定的查询语句所产生的缓存;
另一个构件:EnhancedCachingManager,它也是本插件的核心,它维护着以下几样东西:
1. 整个MyBatis的所有查询所产生的CacheKey集合(以statementId分类);
2. 所有的使用过了的查询的statementId 及其对应的Cache缓存对象的引用;
3. update类型的StatementId和查询StatementId集合的映射,用于当Update类型的语句执行时,根据此映射决定应该清空哪些查询语句产生的缓存;
如下图所示:
工作原理:
原理很简单,就是 当执行了某个update操作时,根据配置信息去清空指定的查询语句在Cache中所产生的缓存数据。
如何获取mybatis-enhanced-cache插件源码
2. github 地址,直接fork即可:
4. mybatis-enhanced-cache 插件的使用实例:
1.下载 mybatis-enhanced-cache.rar压缩包,解压,将其内的mybatis-enhanced-cache-0.0.1-SNAPSHOT.jar添加到项目的classpath下
2. 配置MyBatis配置文件如下:
<plugins>
<plugin interceptor="org.luanlouis.mybatis.plugin.cache.EnhancedCachingExecutor">
<property name="dependency" value="dependencys.xml"/>
<property name="cacheEnabled" value="true"/>
</plugin>
</plugins>其中,<property name="dependency"> 中的value属性是 StatementId之间的依赖关系的配置文件路径。
3. 配置StatementId之间的依赖关系
<?xml version="1.0" encoding="UTF-8"?>
<dependencies>
<statements>
<statement id="com.louis.mybatis.dao.DepartmentsMapper.updateByPrimaryKey">
<observer id="com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments" />
</statement>
</statements>
</dependencies><statement>节点配置的是更新语句的statementId,其内的子节点<observer> 配置的是当更新语句执行后,应当清空缓存的查询语句的StatementId。子节点<observer>可以有多个。
如上的配置,则说明,如果"com.louis.mybatis.dao.DepartmentsMapper.updateByPrimaryKey" 更新语句执行后,由 “com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments” 语句所产生的放置在Cache缓存中的数据都都会被清空。
4. 配置DepartmentsMapper.xml 和EmployeesMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.louis.mybatis.dao.DepartmentsMapper" > <cache></cache> <resultMap id="BaseResultMap" type="com.louis.mybatis.model.Department" >
<id column="DEPARTMENT_ID" property="departmentId" jdbcType="DECIMAL" />
<result column="DEPARTMENT_NAME" property="departmentName" jdbcType="VARCHAR" />
<result column="MANAGER_ID" property="managerId" jdbcType="DECIMAL" />
<result column="LOCATION_ID" property="locationId" jdbcType="DECIMAL" />
</resultMap> <sql id="Base_Column_List" >
DEPARTMENT_ID, DEPARTMENT_NAME, MANAGER_ID, LOCATION_ID
</sql> <update id="updateByPrimaryKey" parameterType="com.louis.mybatis.model.Department" >
update HR.DEPARTMENTS
set DEPARTMENT_NAME = #{departmentName,jdbcType=VARCHAR},
MANAGER_ID = #{managerId,jdbcType=DECIMAL},
LOCATION_ID = #{locationId,jdbcType=DECIMAL}
where DEPARTMENT_ID = #{departmentId,jdbcType=DECIMAL}
</update>
<select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="java.lang.Integer" >
select
<include refid="Base_Column_List" />
from HR.DEPARTMENTS
where DEPARTMENT_ID = #{departmentId,jdbcType=DECIMAL}
</select>
</mapper><?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.louis.mybatis.dao.EmployeesMapper"> <cache eviction="LRU" flushInterval="100000" size="10000"/> <resultMap id="BaseResultMap" type="com.louis.mybatis.model.Employee">
<id column="EMPLOYEE_ID" jdbcType="DECIMAL" property="employeeId" />
<result column="FIRST_NAME" jdbcType="VARCHAR" property="firstName" />
<result column="LAST_NAME" jdbcType="VARCHAR" property="lastName" />
<result column="EMAIL" jdbcType="VARCHAR" property="email" />
<result column="PHONE_NUMBER" jdbcType="VARCHAR" property="phoneNumber" />
<result column="HIRE_DATE" jdbcType="DATE" property="hireDate" />
<result column="JOB_ID" jdbcType="VARCHAR" property="jobId" />
<result column="SALARY" jdbcType="DECIMAL" property="salary" />
<result column="COMMISSION_PCT" jdbcType="DECIMAL" property="commissionPct" />
<result column="MANAGER_ID" jdbcType="DECIMAL" property="managerId" />
<result column="DEPARTMENT_ID" jdbcType="DECIMAL" property="departmentId" />
</resultMap> <sql id="Base_Column_List">
EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE, JOB_ID, SALARY,
COMMISSION_PCT, MANAGER_ID, DEPARTMENT_ID
</sql> <select id="selectWithDepartments" parameterType="java.lang.Integer" resultMap="BaseResultMap" useCache="true" >
select
*
from HR.EMPLOYEES t left join HR.DEPARTMENTS S ON T.DEPARTMENT_ID = S.DEPARTMENT_ID
where EMPLOYEE_ID = #{employeeId,jdbcType=DECIMAL}
</select> </mapper>5. 测试代码:
package com.louis.mybatis.test; import java.io.InputStream;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map; import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.apache.log4j.Logger; import com.louis.mybatis.model.Department;
import com.louis.mybatis.model.Employee; /**
* SqlSession 简单查询演示类
* @author louluan
*/
public class SelectDemo3 { private static final Logger loger = Logger.getLogger(SelectDemo3.class); public static void main(String[] args) throws Exception {
InputStream inputStream = Resources.getResourceAsStream("mybatisConfig.xml");
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(inputStream); SqlSession sqlSession = factory.openSession(true);
SqlSession sqlSession2 = factory.openSession(true);
//3.使用SqlSession查询
Map<String,Object> params = new HashMap<String,Object>();
params.put("employeeId",10);
//a.查询工资低于10000的员工
Date first = new Date();
//第一次查询
List<Employee> result = sqlSession.selectList("com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments",params);
sqlSession.commit();
checkCacheStatus(sqlSession);
params.put("employeeId", 11);
result = sqlSession.selectList("com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments",params);
sqlSession.commit();
checkCacheStatus(sqlSession);
params.put("employeeId", 12);
result = sqlSession.selectList("com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments",params);
sqlSession.commit();
checkCacheStatus(sqlSession);
params.put("employeeId", 13);
result = sqlSession.selectList("com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments",params);
sqlSession.commit();
checkCacheStatus(sqlSession);
Department department = sqlSession.selectOne("com.louis.mybatis.dao.DepartmentsMapper.selectByPrimaryKey",10);
department.setDepartmentName("updated");
sqlSession2.update("com.louis.mybatis.dao.DepartmentsMapper.updateByPrimaryKey", department);
sqlSession.commit();
checkCacheStatus(sqlSession);
} public static void checkCacheStatus(SqlSession sqlSession)
{
loger.info("------------Cache Status------------");
Iterator<String> iter = sqlSession.getConfiguration().getCacheNames().iterator();
while(iter.hasNext())
{
String it = iter.next();
loger.info(it+":"+sqlSession.getConfiguration().getCache(it).getSize());
}
loger.info("------------------------------------"); } }结果输出:
结果分析:
从上述的结果可以看出,前四次执行了“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"缓存的清空。
转自:http://blog.csdn.net/luanlouis/article/details/41800511
如何细粒度地控制你的MyBatis二级缓存(mybatis-enhanced-cache插件实现)的更多相关文章
- mybatis二级缓存应用及与ehcache整合
		
mybaits的二级缓存是mapper范围级别,除了在SqlMapConfig.xml设置二级缓存的总开关,还要在具体的mapper.xml中开启二级缓存. 1.开启mybatis的二级缓存 在核心配 ...
 - 深入了解MyBatis二级缓存
		
深入了解MyBatis二级缓存 标签: mybatis二级缓存 2015-03-30 08:57 41446人阅读 评论(13) 收藏 举报 分类: Mybatis(51) 版权声明:版权归博主所 ...
 - mybatis二级缓存
		
二级缓存区域是根据mapper的namespace划分的,相同namespace的mapper查询数据放在同一个区域,如果使用mapper代理方法每个mapper的namespace都不同,此时可以理 ...
 - MyBatis二级缓存配置
		
正如大多数持久层框架一样,MyBatis 同样提供了一级缓存和二级缓存的支持 Mybatis二级缓存是SessionFactory,如果两次查询基于同一个SessionFactory,那么就从二级缓存 ...
 - MyBatis 二级缓存全详解
		
目录 MyBatis 二级缓存介绍 二级缓存开启条件 探究二级缓存 二级缓存失效的条件 第一次SqlSession 未提交 更新对二级缓存影响 探究多表操作对二级缓存的影响 二级缓存源码解析 二级缓存 ...
 - Springboot整合Ehcache 解决Mybatis二级缓存数据脏读 -详细
		
前面有写了一篇关于这个,但是这几天又改进了一点,就单独一篇在详细说明一下 配置 application.properties ,启用Ehcache # Ehcache缓存 spring.cache.t ...
 - Spring Boot 入门(十):集成Redis哨兵模式,实现Mybatis二级缓存
		
本片文章续<Spring Boot 入门(九):集成Quartz定时任务>.本文主要基于redis实现了mybatis二级缓存.较redis缓存,mybaits自带缓存存在缺点(自行谷歌) ...
 - Mybatis 二级缓存应用 (21)
		
[MyBatis 二级缓存] 概述:一级缓存作用域为同一个SqlSession对象,而二级缓存用来解决一级缓存不能夸会话共享,作用范围是namespace级,可以被多个SqlSession共享(只要是 ...
 - Mybatis二级缓存的简单应用
		
1.接口 public interface MemberMapperCache { public Members selectMembersById(Integer id); } 2.POJO类 实现 ...
 
随机推荐
- shell脚本,awk数组之如何处理多个文件。
			
[root@localhost | > file [root@localhost - | > file1 [root@localhost awk]# cat file [root@loca ...
 - passive event 解决方法
			
为了让页面滚动的效果如丝般顺滑,从 chrome56 开始,在 window.document 和 body 上注册的 touchstart 和 touchmove 事件处理函数,会默认为是 pass ...
 - CSS盒模型总结(一)
			
一.基本概念 盒子模型是css中一个重要的概念,理解了盒子模型才能更好的排版,盒模型的组成:content padding border margin 二.盒模型的分类 盒子模型有两种,分别是 ie ...
 - C++系统学习之六:函数
			
1.函数基础 典型的函数定义包括:返回类型.函数名.由0个或多个形参组成的列表以及函数体. 2.参数传递 形参初始化的机理和变量初始化一样. 有两种方式:引用传递和值传递 2.1 传值参数 当形参是非 ...
 - NOIP 2018数据点
			
链接: https://pan.baidu.com/s/14jXQGPSac3b9_m5h5x2wGQ 提取码: 1cbk 好文别忘点赞!!!
 - opencast的docker安装
			
在之前的从源安装和从包安装opencast,都遇到较多环境问题导致失败.所有采用docker安装. Dockers是有能力打包应用程序及其虚拟容器,可以在任何Linux服务器上运行的依赖性工具,这有助 ...
 - 【php】类型转换
			
$a = 9; print_r((array) $a) ; 输出: [0=>9] print_r((array) null); 输出: []
 - day12-图
 - Web Best Practices
			
Web Best Practices General Google WebFundamentals - Github JavaScript Style Guide - Github Google In ...
 - luogu3231 [HNOI2013]消毒
			
前置技能:poj3041 如果是二维平面有一些方块,这些方块被染了黑色,你每次可以选择 \((x,y)\) 的区域染成白色,代价是 \(\min(x,y)\),问你付出的最小代价 显然我们不会这么染 ...