• 如何细粒度地控制你的MyBatis二级缓存(mybatis-enhanced-cache插件实现)


    前几天网友chanfish 给我抛出了一个问题,笼统地讲就是如何能细粒度地控制MyBatis的二级缓存问题,酝酿了几天,觉得可以写个插件来实现这个这一功能。本文就是从问题入手,一步步分析现存的MyBatis的二级缓存的不足之处,探讨一点可以改进的地方,并且对不足之处开发一个插件进行弥补。

    本文如下组织结构:

    1. 一个关于MyBatis的二级缓存的实际问题
    2. 当前MyBatis二级缓存的工作机制
    3. mybatis-enhanced-cache插件的设计和工作原理
    4. mybatis-enhanced-cache 插件的使用实例

    1.一个关于MyBatis的二级缓存的实际问题

    网友chanfish 给我抛出的问题

    现有AMapper.xml中定义了对数据库表 ATable 的CRUD操作,BMapper定义了对数据库表BTable的CRUD操作;
    假设 MyBatis 的二级缓存开启,并且 AMapper 中使用了二级缓存,AMapper对应的二级缓存为ACache
    除此之外,AMapper 中还定义了一个跟BTable有关的查询语句,类似如下所述:

    1 <select id="selectATableWithJoin" resultMap="BaseResultMap" useCache="true">
    2       select * from ATable left join BTable on ....
    3 </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插件的设计和工作原理

    该插件主要由两个构件组成:EnhancedCachingExecutorEnhancedCachingManager

    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插件源码

    1. 源码和jar包2合一压缩包

    2. github 地址,直接fork即可:

             https://github.com/LuanLouis/mybatis-enhanced-cache

    4. mybatis-enhanced-cache 插件的使用实例:

    1. 下载 mybatis-enhanced-cache.rar压缩包,解压,将其内的mybatis-enhanced-cache-0.0.1-SNAPSHOT.jar添加到项目的classpath下

    2. 配置MyBatis配置文件如下:

    1 <plugins>
    2        <plugin interceptor="org.luanlouis.mybatis.plugin.cache.EnhancedCachingExecutor">
    3           <property name="dependency" value="dependencys.xml"/>
    4           <property name="cacheEnabled" value="true"/>
    5        </plugin>
    6 </plugins>
    View Code

        其中,<property name="dependency"> 中的value属性是 StatementId之间的依赖关系的配置文件路径。

    3. 配置StatementId之间的依赖关系

    1 <?xml version="1.0" encoding="UTF-8"?>
    2 <dependencies>
    3    <statements>
    4        <statement id="com.louis.mybatis.dao.DepartmentsMapper.updateByPrimaryKey">
    5           <observer id="com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments" />
    6        </statement>
    7    </statements>
    8 </dependencies>
    View Code

     <statement>节点配置的是更新语句的statementId,其内的子节点<observer> 配置的是当更新语句执行后,应当清空缓存的查询语句的StatementId。子节点<observer>可以有多个。

     如上的配置,则说明,如果"com.louis.mybatis.dao.DepartmentsMapper.updateByPrimaryKey" 更新语句执行后,由 “com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments” 语句所产生的放置在Cache缓存中的数据都都会被清空。

    4. 配置DepartmentsMapper.xml 和EmployeesMapper.xml

     1 <?xml version="1.0" encoding="UTF-8" ?>
     2 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
     3 <mapper namespace="com.louis.mybatis.dao.DepartmentsMapper" >
     4    
     5    <cache></cache>
     6 
     7   <resultMap id="BaseResultMap" type="com.louis.mybatis.model.Department" >
     8     <id column="DEPARTMENT_ID" property="departmentId" jdbcType="DECIMAL" />
     9     <result column="DEPARTMENT_NAME" property="departmentName" jdbcType="VARCHAR" />
    10     <result column="MANAGER_ID" property="managerId" jdbcType="DECIMAL" />
    11     <result column="LOCATION_ID" property="locationId" jdbcType="DECIMAL" />
    12   </resultMap>
    13   
    14   
    15   <sql id="Base_Column_List" >
    16     DEPARTMENT_ID, DEPARTMENT_NAME, MANAGER_ID, LOCATION_ID
    17   </sql>
    18   
    19   <update id="updateByPrimaryKey" parameterType="com.louis.mybatis.model.Department" >
    20     update HR.DEPARTMENTS
    21     set DEPARTMENT_NAME = #{departmentName,jdbcType=VARCHAR},
    22       MANAGER_ID = #{managerId,jdbcType=DECIMAL},
    23       LOCATION_ID = #{locationId,jdbcType=DECIMAL}
    24     where DEPARTMENT_ID = #{departmentId,jdbcType=DECIMAL}
    25   </update>
    26     <select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="java.lang.Integer" >
    27     select 
    28     <include refid="Base_Column_List" />
    29     from HR.DEPARTMENTS
    30     where DEPARTMENT_ID = #{departmentId,jdbcType=DECIMAL}
    31   </select>
    32 </mapper>
    View Code
     1 <?xml version="1.0" encoding="UTF-8"?>
     2 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
     3 <mapper namespace="com.louis.mybatis.dao.EmployeesMapper">
     4 
     5   <cache eviction="LRU" flushInterval="100000" size="10000"/>
     6 
     7   <resultMap id="BaseResultMap" type="com.louis.mybatis.model.Employee">
     8     <id column="EMPLOYEE_ID" jdbcType="DECIMAL" property="employeeId" />
     9     <result column="FIRST_NAME" jdbcType="VARCHAR" property="firstName" />
    10     <result column="LAST_NAME" jdbcType="VARCHAR" property="lastName" />
    11     <result column="EMAIL" jdbcType="VARCHAR" property="email" />
    12     <result column="PHONE_NUMBER" jdbcType="VARCHAR" property="phoneNumber" />
    13     <result column="HIRE_DATE" jdbcType="DATE" property="hireDate" />
    14     <result column="JOB_ID" jdbcType="VARCHAR" property="jobId" />
    15     <result column="SALARY" jdbcType="DECIMAL" property="salary" />
    16     <result column="COMMISSION_PCT" jdbcType="DECIMAL" property="commissionPct" />
    17     <result column="MANAGER_ID" jdbcType="DECIMAL" property="managerId" />
    18     <result column="DEPARTMENT_ID" jdbcType="DECIMAL" property="departmentId" />
    19   </resultMap>
    20 
    21   <sql id="Base_Column_List">
    22     EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE, JOB_ID, SALARY, 
    23     COMMISSION_PCT, MANAGER_ID, DEPARTMENT_ID
    24   </sql>
    25   
    26   <select id="selectWithDepartments" parameterType="java.lang.Integer" resultMap="BaseResultMap" useCache="true" >
    27     select 
    28     *
    29     from HR.EMPLOYEES t left join HR.DEPARTMENTS S ON T.DEPARTMENT_ID = S.DEPARTMENT_ID
    30     where EMPLOYEE_ID = #{employeeId,jdbcType=DECIMAL}
    31   </select>
    32   
    33 </mapper>
    View Code

    5. 测试代码:

     1 package com.louis.mybatis.test;
     2 
     3 import java.io.InputStream;
     4 import java.util.Date;
     5 import java.util.HashMap;
     6 import java.util.Iterator;
     7 import java.util.List;
     8 import java.util.Map;
     9 
    10 import org.apache.ibatis.io.Resources;
    11 import org.apache.ibatis.session.SqlSession;
    12 import org.apache.ibatis.session.SqlSessionFactory;
    13 import org.apache.ibatis.session.SqlSessionFactoryBuilder;
    14 import org.apache.log4j.Logger;
    15 
    16 import com.louis.mybatis.model.Department;
    17 import com.louis.mybatis.model.Employee;
    18 
    19 /**
    20  * SqlSession 简单查询演示类
    21  * @author louluan
    22  */
    23 public class SelectDemo3 {
    24 
    25     private static final Logger loger = Logger.getLogger(SelectDemo3.class);
    26     
    27     public static void main(String[] args) throws Exception {
    28         InputStream inputStream = Resources.getResourceAsStream("mybatisConfig.xml");
    29         SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
    30         SqlSessionFactory factory = builder.build(inputStream);
    31         
    32         SqlSession sqlSession = factory.openSession(true);
    33         SqlSession sqlSession2 = factory.openSession(true);
    34         //3.使用SqlSession查询
    35         Map<String,Object> params = new HashMap<String,Object>();
    36         params.put("employeeId",10);
    37         //a.查询工资低于10000的员工
    38         Date first = new Date();
    39         //第一次查询
    40         List<Employee> result = sqlSession.selectList("com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments",params);
    41         sqlSession.commit();
    42         checkCacheStatus(sqlSession);
    43         params.put("employeeId", 11);
    44         result = sqlSession.selectList("com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments",params);
    45         sqlSession.commit();
    46         checkCacheStatus(sqlSession);
    47         params.put("employeeId", 12);
    48         result = sqlSession.selectList("com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments",params);
    49         sqlSession.commit();
    50         checkCacheStatus(sqlSession);
    51         params.put("employeeId", 13);
    52         result = sqlSession.selectList("com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments",params);
    53         sqlSession.commit();
    54         checkCacheStatus(sqlSession);
    55         Department department = sqlSession.selectOne("com.louis.mybatis.dao.DepartmentsMapper.selectByPrimaryKey",10);
    56         department.setDepartmentName("updated");
    57         sqlSession2.update("com.louis.mybatis.dao.DepartmentsMapper.updateByPrimaryKey", department);
    58         sqlSession.commit();
    59         checkCacheStatus(sqlSession);
    60     }
    61     
    62     
    63     public static void checkCacheStatus(SqlSession sqlSession)
    64     {
    65         loger.info("------------Cache Status------------");
    66         Iterator<String> iter = sqlSession.getConfiguration().getCacheNames().iterator();
    67         while(iter.hasNext())
    68         {
    69             String it = iter.next();
    70             loger.info(it+":"+sqlSession.getConfiguration().getCache(it).getSize());
    71         }
    72         loger.info("------------------------------------");
    73         
    74     }
    75 
    76 }
    View Code
    结果输出:

    结果分析:

    从上述的结果可以看出,前四次执行了“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

  • 相关阅读:
    JVM活学活用——GC算法 垃圾收集器
    JVM活学活用——类加载机制
    JVM活学活用——Jvm内存结构
    优化springboot
    Java基础巩固计划
    Java自定义注解
    记一次内存溢出的分析经历
    redis学习笔记-redis的安装
    记一次线程池调优经历
    Python中关于split和splitext的差别和运用
  • 原文地址:https://www.cnblogs.com/yixiu868/p/8143264.html
Copyright © 2020-2023  润新知