zoukankan      html  css  js  c++  java
  • 如何细粒度地控制你的MyBatis二级缓存(mybatis-enhanced-cache插件实现)

    本文如下组织结构:

    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 有关的查询语句,类似如下所述:

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

    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配置文件如下:

    <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 "缓存的清空。

    作者的话

    该插件的实现周期比较短,尚未经过性能方面的测试,如果果您对此插件有任何意见或者看法,可以留言一起交流和探讨。

    该插件源码已经放到了Github上,可供大家自由修改,github地址:

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

  • 相关阅读:
    关于JSON可能出现的错误,待更/todo
    mongoose的安装与使用(书签记录) 2017
    HTTP的学习记录3--HTTPS和HTTP
    HTTP的学习记录(二)头部
    HTTP(一)概述
    LeetCode 455. Assign Cookies
    LeetCode 453. Minimum Moves to Equal Array Elements
    LeetCode 448. Find All Numbers Disappeared in an Array
    LeetCode 447. Number of Boomerangs
    LeetCode 416. Partition Equal Subset Sum
  • 原文地址:https://www.cnblogs.com/shiyalong/p/4501052.html
Copyright © 2011-2022 走看看