MyBatis cache缓存机制的简单应用(1)

 

      今天在作项目中遇到了一个很是有意思的BUG,写下来分享一下,但愿遇到相同问题的同窗能更快的解决;这个BUG是这样的:html

      BUG1: 在项目启动运行后,用户第一次登陆的时候,我根据用户输入的帐号从数据库查找到用户对象User, 当用户登陆成功后, 返回User对象,为了不敏感数据,我将User进行隐藏密码处理,代码以下:java

Admin admin = selectOne(new EntityWrapper<Admin>().eq("account", account));
        if(admin==null){
            throw new MyException("用户名不存在");
        }
        password = OftenTool.md5Encode(password);
        System.out.println("用 户 数 据:"+admin);
        System.out.println("用户输入密码:"+password);
        System.out.println("数据库存密码:"+admin.getPassword());
        if(!password.equals(admin.getPassword())){
            throw new MyException("密码错误");
        }
        admin.setPassword(null);
        return admin;

那么问题来了,第一次登陆状况是正常的,但退出后,从新登陆,控制台输出的admin其余信息正常,就是密码为空;因此就会一直报 密码错误 ,固然由于我使用了MyBatis的二级缓存,因此用户第二次登陆时从缓存中拿取数据的;因此当输出密码为null的时候,我就知道是缓存出现了问题,而后,我又作了几回实验: 将代码算法

admin.setPassword(null);

去掉; 那样全部的登陆就会正常.....;晕, Mapper 缓存不是缓存的数据库查出的那个对象吗? 怎么开始缓存我操做后的数据了,这已经与SQL没有关系了啊?  sql

    还有更奇葩的BUG:数据库

    BUG2: 这也是关于缓存的BUG,状况是这样的:apache

RepairOrder repairOrder = repairOrderMapper.selectById(repairStatus.getRepairOrderId());
if(repairOrder==null){
    throw new MyException("订单不存在");
}
if(repairOrder.getCompanyId()!= repairStatus.getCompanyId()){
    throw new MyException("参数错误");
}

 第一次执行这段代码的时候是成功的,可是第二次执行的时候,就出错了,抛出了参数错误,当时我就凌乱了,明明同样的参数,第二次执行的时候就发生错误,固然有了上面问题的解决经验,就知道必定又是缓存在作怪了,而后开启DeBug模式,来在跑一遍,结果我更凌乱了: 上面显示,repairOrder.getCompanyId()为1, repairStatus.getCompanyId()也为1,但repairOrder.getCompanyId()!= repairStatus.getCompanyId()为true; 我靠, 1!=1 ==> 为true; (数据类型都是Integer), 又懵圈了; 不甘心的我有修改一下:缓存

boolean b = repairOrder.getCompanyId()==repairStatus.getCompanyId();
if(!b){
    throw new MyException("参数错误");
}

 这样更直观一点,直接看变量b的值就行了, 结果在DeBug模式下 b居然为 false, 但1==1 为 false, 晕;安全

我最后从新定义变量,才解决了问题:session

RepairOrder repairOrder = repairOrderMapper.selectById(repairStatus.getRepairOrderId());
if(repairOrder==null){
    throw new MyException("订单不存在");
}
int repairOrderCompanyId = repairOrder.getCompanyId();
int repairStatusCompanyId = repairStatus.getCompanyId();
boolean b = repairOrderCompanyId==repairStatusCompanyId;
if(!b){
    throw new MyException("参数错误");
}

上面这两种状况截至如今依然不知道具体哪里出了问题,只知道是关于MyBatis缓存的问题;mybatis

平时一直都在用,而具体的实现原理倒是不太清楚,那么究竟是什么缘由呢?就像第一个问题,我还专门写了一个测试,在各中环境下测试发现:

      只有在Service实现层中查询使用的mapper会缓存操做后的对象(user1),即便你new一个新的对象(user2),将缓存中的对象直接赋值给新对象(user2=user1),而后操做新的对象(user2.setName(null)),那么他也会缓存对新对象的操做(user2); 除非采用get,set方法或构造器将user1中属性的值赋给user2中属性,则不会修改缓存;

因此你能够在须要的实体中建立一个这样的构造器:

public User (User user) {
	    this.age = user.getAge();
	    this.consumeIntegral = user.getConsumeIntegral();
	    this.creationTime = user.getCreationTime();
	    this.email = user.getEmail();
	    this.id = user.getId();
	    this.isAdmin = user.getIsAdmin();
	    this.nickname = user.getNickname();
	    this.phone = user.getPhone();
	    this.roleId = user.getRoleId();
	    this.sex = user.getSex();
	    this.state = user.getState();
	    this.surplusIntegral = user.getSurplusIntegral();
	}

或者咱们能够经过反射来复制对象:

package com.gy.demo.common.utils.object;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

/**
 *@description  TODO 复制对象工具类
 *@date  2018年1月11日
 *@author  geYang
 **/
public class CopyObject {

    public static Object copyEntity(Object t) throws Exception{
        //获取对象Class
        Class<? extends Object> clazz = t.getClass();
        //获取对象默认构造器
        Constructor<? extends Object> constructor = clazz.getDeclaredConstructor(new Class[]{});
        //建立复制对象
        Object object = constructor.newInstance(new Object[]{});
        //获取对象所有属性;
        Field[] fields = clazz.getDeclaredFields();
        for(Field field : fields){
            //获取属性名称,类型
            String fieldName = field.getName();
            if("serialVersionUID".equals(fieldName)){
                continue;
            }
            Class<?> type = field.getType();
            //获取get,set方法名
            String getMethodName;
            String setMethodName;
            if(type==boolean.class){
                getMethodName = fieldName;
                setMethodName = "set"+fieldName.substring(2);
            } else {
                getMethodName = "get"+fieldName.substring(0, 1).toUpperCase()+fieldName.substring(1);
                setMethodName = "set"+fieldName.substring(0, 1).toUpperCase()+fieldName.substring(1);
            }
            //获取get,set方法
            Method getMethod = clazz.getDeclaredMethod(getMethodName, new Class[]{});
            Method setMethod = clazz.getDeclaredMethod(setMethodName, type);
            //获取被复制对象的属性值
            Object value = getMethod.invoke(t, new Object[]{});
            //复制对象赋值
            setMethod.invoke(object, new Object[]{value});
        }
        return object;
    }
    
}

而在Controller中调用查询则不会缓存操做后的对象,会缓存直接查出来的对象; 

     而第二个问题缘由尚未找到,就像灵异了同样;

     终于BUG2的解决办法找到了, 具体代码以下:

//打印false
System.out.println(repairOrder.getCompanyId()==repairStatus.getCompanyId());
//打印true
System.out.println(repairOrder.getCompanyId().equals(repairStatus.getCompanyId()));

    在这里既然都是用 Integer 对象来作比对,因此用 equals 函数来作具体判断更好, 而当作int来处理显然是不行的,因此在之后的编码中还须要注意注意在注意                              

     因此有必要来认真学习一下MyBatis的缓存机制:

    那么就来认真看一下MyBatis的文档: http://www.mybatis.org/mybatis-3/zh/configuration.html

    MyBatis 包含一个强大的,可配置,可定制的缓存;
    在MyBatis中缓存分为一级缓存(会话缓存)和二级缓存;

    使用二级缓存时首先打开全局缓存开关: mybatis-config.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">       
<configuration>
    <settings>
        <!-- 全局映射器启用二级缓存 开关 -->
        <setting name="cacheEnabled" value="true"/>
        
        <!-- 延迟加载的全局开关. 当开启时,全部关联对象都会延迟加载. 特定关联关系中可经过设置fetchType属性来覆盖该项的开关状态. 默认false-->
        <setting name="lazyLoadingEnabled" value="false"/>
        
        <!-- 当开启时,任何方法的调用都会加载该对象的全部属性.不然,每一个属性会按需加载(参考lazyLoadTriggerMethods). 默认false (true in ≤3.4.1)-->
        <setting name="aggressiveLazyLoading" value="false"/>
        
        <!-- 是否容许单一语句返回多结果集(须要兼容驱动). 默认 true -->
        <setting name="multipleResultSetsEnabled" value="true"/>
        
        <!-- 使用列标签代替列名.不一样的驱动在这方面会有不一样的表现, 具体可参考相关驱动文档或经过测试这两种不一样的模式来观察所用驱动的结果. 默认true-->
        <setting name="useColumnLabel" value="true"/>
        
        <!-- 容许 JDBC 支持自动生成主键,须要驱动兼容. 若是设置为 true 则这个设置强制使用自动生成主键,尽管一些驱动不能兼容但仍可正常工做(好比 Derby) 默认false-->
        <setting name="useGeneratedKeys" value="false"/>
        
        <!-- 指定 MyBatis 应如何自动映射列到字段或属性. NONE 表示取消自动映射;PARTIAL 只会自动映射没有定义嵌套结果集映射的结果集. FULL 会自动映射任意复杂的结果集(不管是否嵌套). 默认 PARTIAL-->
        <setting name="autoMappingBehavior" value="PARTIAL"/>
        
        <!-- 指定发现自动映射目标未知列(或者未知属性类型)的行为. NONE: 不作任何反应; WARNING: 输出提醒日志 ('org.apache.ibatis.session.AutoMappingUnknownColumnBehavior' 的日志等级必须设置为 WARN); FAILING: 映射失败 . 默认NONE-->
        <setting name="autoMappingUnknownColumnBehavior" value="NONE"/>
        
        <!-- 配置默认的执行器, SIMPLE:就是普通的执行器; REUSE:执行器会重用预处理语句(prepared statements); BATCH:执行器将重用语句并执行批量更新, 默认SIMPLE-->
        <setting name="defaultExecutorType" value="SIMPLE" />
        
        <!-- 设置超时时间,它决定驱动等待数据库响应的秒数. -->
        <setting name="defaultStatementTimeout" value="6000" />
        
        <!-- 为驱动的结果集获取数量(fetchSize)设置一个提示值.此参数只能够在查询设置中被覆盖. -->
        <setting name="defaultFetchSize" value="600" />
        
        <!-- 容许在嵌套语句中使用分页(RowBounds).若是容许使用则设置为false. 默认false -->
        <setting name="safeRowBoundsEnabled" value="false"/>
        
        <!-- 容许在嵌套语句中使用分页(ResultHandler),若是容许使用则设置为false. 默认true -->
        <setting name="safeResultHandlerEnabled" value="false"/>
        
        <!-- 是否开启自动驼峰命名规则(camel case)映射,即从经典数据库列名 A_COLUMN 到经典 Java 属性名 aColumn 的相似映射. 默认:false-->
        <setting name="mapUnderscoreToCamelCase" value="true"/>
        
        <!-- MyBatis 利用本地缓存机制(Local Cache)防止循环引用(circular references)和加速重复嵌套查询. SESSION: 这种状况下会缓存一个会话中执行的全部查询. 
        	 STATEMENT: 本地会话仅用在语句执行上, 对相同  SqlSession 的不一样调用将不会共享数据. 默认SESSION -->
        <setting name="localCacheScope" value="SESSION"/>
        
        <!-- 当没有为参数提供特定的 JDBC 类型时,为空值指定 JDBC 类型. 某些驱动须要指定列的 JDBC 类型,多数状况直接用通常类型便可,好比 NULL、VARCHAR 或 OTHER. 默认OTHER-->
        <setting name="jdbcTypeForNull" value="OTHER"/>
        
        <!-- 指定哪一个对象的方法触发一次延迟加载. 默认 equals,clone,hashCode,toString -->
        <setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
        
        <!-- 指定动态 SQL 生成的默认语言. 默认 org.apache.ibatis.scripting.xmltags.XMLLanguageDriver -->
        <setting name="defaultScriptingLanguage" value="org.apache.ibatis.scripting.xmltags.XMLLanguageDriver"/>
        
        <!-- 指定默认枚举. 默认 org.apache.ibatis.type.EnumTypeHandler -->
        <!-- <setting name="defaultEnumTypeHandler" value=""/> -->
        
        <!-- 指定当结果集中值为 null 的时候是否调用映射对象的 setter(map 对象时为 put)方法,这对于有 Map.keySet()依赖或 null值初始化的时候是有用的.
        	注意基本类型(int,boolean等)是不能设置成 null 的 . 默认 false-->
        <setting name="callSettersOnNulls" value="false"/>
        
        <!-- 当返回行的全部列都是空时,MyBatis默认返回null. 当开启这个设置时,MyBatis会返回一个空实例.
        	请注意,它也适用于嵌套的结果集 (i.e. collectioin and association)  默认 false-->
        <setting name="returnInstanceForEmptyRow" value="false"/>
        
        <!-- 指定 MyBatis 增长到日志名称的前缀. -->
        <setting name="logPrefix" value=""/>
        
        <!-- 指定 MyBatis 所用日志的具体实现,未指定时将自动查找. SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING-->
        <setting name="logImpl" value="SLF4J"/>
        
        <!-- 指定VFS的实现.自定义VFS的实现的类全限定名,以逗号分隔. -->
        <!-- <setting name="vfsImpl" value=""/> -->
        
        <!-- 容许使用方法签名中的名称做为语句参数名称.为了使用该特性,你的工程必须采用Java8编译,而且加上-parameters选项.(从3.4.1开始) 默认 true -->
        <setting name="useActualParamName" value="true"/>
        
        <!-- 指定一个提供Configuration实例的类. 这个被返回的Configuration实例是用来加载被反序列化对象的懒加载属性值. 
        	这个类必须包含一个签名方法static Configuration getConfiguration(). (从 3.2.3 版本开始) ,值: 类型别名或者全类名 -->
        <!-- <setting name="configurationFactory" value=""/> -->
        
    </settings>
    
</configuration>

    1, 一级缓存是默认支持的,不过是做用于同一个sqlSession中,我这里就不作过多的说明,详细可参考:            http://www.360doc.com/content/15/1205/07/29475794_518018352.shtml

    2, 主要来看看二级缓存: 二级缓存在默认状态下是不会开启的,须要咱们去设置,固然也是很是的简单:在SQL映射文件(*Mapper.xml)文件中,只须要加上:

<!-- 开启二级缓存 -->
	<cache/>

它的做用以下:

� 全部在映射文件里的select语句都将被缓存;
� 全部在映射文件里insert,update和delete语句会清空缓存;
� 缓存使用 "最近不多使用" 算法来回收;
� 缓存不会被设定的时间所清空;
� 每一个缓存能够存储1024个列表或对象的引用(无论查询出来的结果是什么);
� 缓存将做为 "读/写" 缓存,意味着获取的对象不是共享的且对调用者是安全的.不会有其它的调用者或线程潜在修改.

2, 缓存元素的全部特性均可以经过属性来修改:

<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
<!--  
高级的配置建立一个FIFO缓存;
让60秒就清空一次;
存储512个对象结果或列表引用;
而且返回的结果是只读;
所以在不用的线程里的两个调用者修改它们可能会引用冲突
-->

<!--
清除规则以下:
� LRU- 最近最少使用法:移出最近较长周期内都没有被使用的对象;
� FIFO- 先进先出:移出队列里较早的对象;
� SOFT- 软引用:基于软引用规则,使用垃圾回收机制来移出对象;
� WEAK- 弱引用:基于弱引用规则,使用垃圾回收机制来强制性地移出对象;
� 默认值是LRU;
-->
<!--
flushInterval: 属性能够被设置为一个正整数,表明一个合理的毫秒总计时间.默认是不设置,所以使用无间隔清空即只能调用语句来清空;
size : 属性能够设置为一个正整数,你须要留意你要缓存的对象和你的内在环境,默认值是1024;
readOnly : 属性能够被设置为true或false.只读缓存将对全部调用者返回同一个实例.所以都不能被修改,这能够极大的提升性能.可写的缓存将经过序列化来返回一个缓存对象的拷贝.这会比较慢,可是比较安全.因此默认值是false
-->

注意: readOnly 只读属性,在我测试的过程当中,根本没有发现什么区别,有没有好像都同样, 更新后都后从新查询, 上面的BUG1问题也一样会出现; 多是我打开的方式不对, 所以很不理解文档中的 "不能被修改" 是什么意思.

相关文章
相关标签/搜索