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問題也一樣會出現; 多是我打開的方式不對, 所以很不理解文檔中的 "不能被修改" 是什麼意思.

相關文章
相關標籤/搜索