本文如下组织结构:
1.一个关于MyBatis的二级缓存的实际问题
网友 chanfish 给我抛出的问题 现有 AMapper.xml 中定义了对数据库表 <select id="selectATableWithJoin" resultMap="BaseResultMap" useCache="true"> select * from ATable left join BTable on .... </select> 执行以下操作:
1. 执行 AMapper 中的" selectATableWithJoin " 操作,此时会将查询到的结果放置到 AMapper 对应的二级缓存 2. 执行 BMapper 中对 3. 再执行1完全相同的查询,这时候会直接从 AMapper 二级缓存 好,问题就出现在第3步上: 由于AMapper的“ selectATableWithJoin ” 对应的SQL语句需要和 总结来看,就是: 对于某些使用了 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 "-////DTD Mapper 3.0//EN" "http:///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 "-////DTD Mapper 3.0//EN" "http:///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 "缓存的清空。
作者的话 该插件的实现周期比较短,尚未经过性能方面的测试,如果果您对此插件有任何意见或者看法,可以留言一起交流和探讨。 该插件源码已经放到了Github上,可供大家自由修改,github地址: |
|