Mybatis源碼分析——一級緩存和二級緩存分析

前言

像Mybatis、Hibernate這樣的ORM框架,封裝了JDBC的大部分操作,極大的簡化了我們對數(shù)據(jù)庫的操作。

在實際項目中,我們發(fā)現(xiàn)在一個事務(wù)中查詢同樣的語句兩次的時候,第二次沒有進行數(shù)據(jù)庫查詢,直接返回了結(jié)果,實際這種情況我們就可以稱為緩存。

Mybatis的緩存級別

一級緩存

  • MyBatis的一級查詢緩存(也叫作本地緩存)是基于org.apache.ibatis.cache.impl.PerpetualCache 類的 HashMap本地緩存,其作用域是SqlSession,myBatis 默認(rèn)一級查詢緩存是開啟狀態(tài),且不能關(guān)閉。

  • 在同一個SqlSession中兩次執(zhí)行相同的 sql查詢語句,第一次執(zhí)行完畢后,會將查詢結(jié)果寫入到緩存中,第二次會從緩存中直接獲取數(shù)據(jù),而不再到數(shù)據(jù)庫中進行查詢,這樣就減少了數(shù)據(jù)庫的訪問,從而提高查詢效率。

  • 基于PerpetualCache 的 HashMap本地緩存,其存儲作用域為 Session,PerpetualCache 對象是在SqlSession中的Executor的localcache屬性當(dāng)中存放,當(dāng) Session flush 或 close 之后,該Session中的所有 Cache 就將清空。

一級緩存區(qū)域是根據(jù) SqlSession 為單位劃分的。 每次查詢會先從緩存區(qū)域找,如果找不到從數(shù)據(jù)庫查詢,查詢到數(shù)據(jù)將數(shù)據(jù)寫入緩存。 Mybatis 內(nèi)部存儲緩存使用一個 HashMap,key 為 hashCode+sqlId+Sql 語句。value 為 從查詢出來映射生成的 java 對象 sqlSession 執(zhí)行 insert、update、delete 等操作 commit 提交后會清空緩存區(qū)域。

二級緩存

  • 二級緩存與一級緩存其機制相同,默認(rèn)也是采用 PerpetualCache,HashMap存儲,不同在于其存儲作用域為 Mapper(Namespace),每個Mapper中有一個Cache對象,存放在Configration中,并且將其放進當(dāng)前Mapper的所有MappedStatement當(dāng)中,并且可自定義存儲源,如 Ehcache。

  • Mapper級別緩存,定義在Mapper文件的<cache>標(biāo)簽并需要開啟此緩存。

用下面這張圖描述一級緩存和二級緩存的關(guān)系。


CacheKey

在 MyBatis 中,引入緩存的目的是為提高查詢效率,降低數(shù)據(jù)庫壓力。既然 MyBatis 引入了緩存,那么大家思考過緩存中的 key 和 value 的值分別是什么嗎?大家可能很容易能回答出 value 的內(nèi)容,不就是 SQL 的查詢結(jié)果嗎。那 key 是什么呢?是字符串,還是其他什么對象?如果是字符串的話,那么大家首先能想到的是用 SQL 語句作為 key。但這是不對的,比如:

SELECT * FROM user where id > ?

id > 1 和 id > 10 查出來的結(jié)果可能是不同的,所以我們不能簡單的使用 SQL 語句作為 key。從這里可以看出來,運行時參數(shù)將會影響查詢結(jié)果,因此我們的 key 應(yīng)該涵蓋運行時參數(shù)。除此之外呢,如果進行分頁查詢也會導(dǎo)致查詢結(jié)果不同,因此 key 也應(yīng)該涵蓋分頁參數(shù)。綜上,我們不能使用簡單的 SQL 語句作為 key。應(yīng)該考慮使用一種復(fù)合對象,能涵蓋可影響查詢結(jié)果的因子。在 MyBatis 中,這種復(fù)合對象就是 CacheKey。下面來看一下它的定義。

public class CacheKey implements Cloneable, Serializable {

    public static final CacheKey NULL_CACHE_KEY = new NullCacheKey();

    private static final int DEFAULT_MULTIPLYER = 37;
    private static final int DEFAULT_HASHCODE = 17;
    // 乘子,默認(rèn)為37
    private final int multiplier;
    // CacheKey 的 hashCode,綜合了各種影響因子
    private int hashcode;
    // 校驗和
    private long checksum;
    // 影響因子個數(shù)
    private int count;
    // 影響因子集合
    private List<Object> updateList;
    
    public CacheKey() {
        this.hashcode = DEFAULT_HASHCODE;
        this.multiplier = DEFAULT_MULTIPLYER;
        this.count = 0;
        this.updateList = new ArrayList<Object>();
    }

    /*
     * 每當(dāng)執(zhí)行更新操作時,表示有新的影響因子參與計算 
     *  當(dāng)不斷有新的影響因子參與計算時,hashcode 和 checksum 將會變得愈發(fā)復(fù)雜和隨機。這樣可降低沖突率,使 CacheKey 可在緩存中更均勻的分布。
     */ 
    public void update(Object object) {
        int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object); 
        // 自增 count
        count++;
        // 計算校驗和
        checksum += baseHashCode;
        // 更新 baseHashCode
        baseHashCode *= count;
        // 計算 hashCode
        hashcode = multiplier * hashcode + baseHashCode;
        // 保存影響因子
        updateList.add(object);
    }   

    /**
     *  CacheKey 最終要作為鍵存入 HashMap,因此它需要覆蓋 equals 和 hashCode 方法
     */ 
    @Override
    public boolean equals(Object object) {
        // 檢測是否為同一個對象
        if (this == object) {
            return true;
        }
        // 檢測 object 是否為 CacheKey
        if (!(object instanceof CacheKey)) {
            return false;
        }
        
        final CacheKey cacheKey = (CacheKey) object;
        // 檢測 hashCode 是否相等
        if (hashcode != cacheKey.hashcode) {
            return false;
        }
        // 檢測校驗和是否相同
        if (checksum != cacheKey.checksum) {
            return false;
        }
        // 檢測 coutn 是否相同
        if (count != cacheKey.count) {
            return false;
        }
        // 如果上面的檢測都通過了,下面分別對每個影響因子進行比較
        for (int i = 0; i < updateList.size(); i++) {
            Object thisObject = updateList.get(i);
            Object thatObject = cacheKey.updateList.get(i);
            if (!ArrayUtil.equals(thisObject, thatObject)) {
                return false;
            }
        }
        return true;
    }   
    
    @Override
    public int hashCode() {
        // 返回 hashcode 變量
        return hashcode;
    }   
}

當(dāng)不斷有新的影響因子參與計算時,hashcode 和 checksum 將會變得愈發(fā)復(fù)雜和隨機。這樣可降低沖突率,使 CacheKey 可在緩存中更均勻的分布。CacheKey 最終要作為鍵存入 HashMap,因此它需要覆蓋 equals 和 hashCode 方法。

一級緩存源碼解析

一級緩存的測試

同一個session查詢

public static void main(String[] args) {
    SqlSession session = sqlSessionFactory.openSession();
    try {
        Blog blog = (Blog)session.selectOne("queryById",1);
        Blog blog2 = (Blog)session.selectOne("queryById",1);
    } finally {
        session.close();
    }
}

結(jié)論:只有一個DB查詢

兩個session分別查詢

public static void main(String[] args) {
    SqlSession session = sqlSessionFactory.openSession();
    SqlSession session1 = sqlSessionFactory.openSession();
    try {
        Blog blog = (Blog)session.selectOne("queryById",17);
        Blog blog2 = (Blog)session1.selectOne("queryById",17);
    } finally {
        session.close();
    }
}

結(jié)論:進行了兩次DB查詢

同一個session,進行update之后再次查詢

public static void main(String[] args) {
    SqlSession session = sqlSessionFactory.openSession();
    try {
        Blog blog = (Blog)session.selectOne("queryById",17);
        blog.setName("llll");
        session.update("updateBlog",blog);
        
        Blog blog2 = (Blog)session.selectOne("queryById",17);
    } finally {
        session.close();
    }
}

結(jié)論:進行了兩次DB查詢

總結(jié):在一級緩存中,同一個SqlSession下,查詢語句相同的SQL會被緩存,如果執(zhí)行增刪改操作之后,該緩存就會被刪除。

創(chuàng)建緩存對象PerpetualCache

我們來回顧一下創(chuàng)建SqlSession的過程

SqlSession session = sessionFactory.openSession();

public class DefaultSqlSessionFactory implements SqlSessionFactory {

    private final Configuration configuration;

    @Override
    public SqlSession openSession() {
        return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
    }
    
    private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
        Transaction tx = null;
        try {
            final Environment environment = configuration.getEnvironment();
            final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
            tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
            //創(chuàng)建SQL執(zhí)行器
            final Executor executor = configuration.newExecutor(tx, execType);
            return new DefaultSqlSession(configuration, executor, autoCommit);
        } catch (Exception e) {
            closeTransaction(tx); // may have fetched a connection so lets call close()
            throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
        } finally {
            ErrorContext.instance().reset();
        }
    }
}

public class Configuration {

    protected final InterceptorChain interceptorChain = new InterceptorChain();

    public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
        executorType = executorType == null ? defaultExecutorType : executorType;
        executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
        Executor executor;
        if (ExecutorType.BATCH == executorType) {
            executor = new BatchExecutor(this, transaction);
        } else if (ExecutorType.REUSE == executorType) {
            executor = new ReuseExecutor(this, transaction);
        } else {
            //默認(rèn)創(chuàng)建SimpleExecutor
            executor = new SimpleExecutor(this, transaction);
        }
        if (cacheEnabled) {
            //開啟二級緩存就會用CachingExecutor裝飾SimpleExecutor
            executor = new CachingExecutor(executor);
        }
        executor = (Executor) interceptorChain.pluginAll(executor);
        return executor;
    }
}

public class SimpleExecutor extends BaseExecutor {

    public SimpleExecutor(Configuration configuration, Transaction transaction) {
        super(configuration, transaction);
    }
}

public abstract class BaseExecutor implements Executor {

    private static final Log log = LogFactory.getLog(BaseExecutor.class);

    protected Transaction transaction;
    protected Executor wrapper;

    protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
    protected PerpetualCache localCache;
    protected PerpetualCache localOutputParameterCache;
    protected Configuration configuration;

    protected int queryStack;
    private boolean closed;

    protected BaseExecutor(Configuration configuration, Transaction transaction) {
        this.transaction = transaction;
        this.deferredLoads = new ConcurrentLinkedQueue<DeferredLoad>();
        //創(chuàng)建一個緩存對象,PerpetualCache并不是線程安全的
        //但SqlSession和Executor對象在通常情況下只能有一個線程訪問,而且訪問完成之后馬上銷毀。也就是session.close();
        this.localCache = new PerpetualCache("LocalCache");
        this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
        this.closed = false;
        this.configuration = configuration;
        this.wrapper = this;
    }   
}   

我只是簡單的貼了代碼,大家可以看我之前的博客,我們可以看到DefaultSqlSession中有SimpleExecutor對象,SimpleExecutor對象中有一個PerpetualCache,一級緩存的數(shù)據(jù)就是存儲在PerpetualCache對象中,SqlSession關(guān)閉的時候會清空PerpetualCache

一級緩存實現(xiàn)

再來看BaseExecutor中的query方法是怎么實現(xiàn)一級緩存的,executor默認(rèn)實現(xiàn)為CachingExecutor

CachingExecutor

public class CachingExecutor implements Executor {

    private final Executor delegate;
    private final TransactionalCacheManager tcm = new TransactionalCacheManager();

    @Override
    public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
        BoundSql boundSql = ms.getBoundSql(parameterObject);
        // 創(chuàng)建 CacheKey
        CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
        return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    }
    
    @Override
    public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
        // 從 MappedStatement 中獲取 Cache,注意這里的 Cache 是從MappedStatement中獲取的
        // 也就是我們上面解析Mapper中<cache/>標(biāo)簽中創(chuàng)建的,它保存在Configration中
        // 我們在上面解析blog.xml時分析過每一個MappedStatement都有一個Cache對象,就是這里
        Cache cache = ms.getCache();
        // 如果配置文件中沒有配置 <cache>,則 cache 為空
        if (cache != null) {
            //如果需要刷新緩存的話就刷新:flushCache="true"
            flushCacheIfRequired(ms);
            if (ms.isUseCache() && resultHandler == null) {
                ensureNoOutParams(ms, boundSql);
                @SuppressWarnings("unchecked")
                // 訪問二級緩存
                List<E> list = (List<E>) tcm.getObject(cache, key);
                // 緩存未命中
                if (list == null) {
                    // 如果沒有值,則執(zhí)行查詢,這個查詢實際也是先走一級緩存查詢,一級緩存也沒有的話,則進行DB查詢
                    list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                    // 緩存查詢結(jié)果
                    tcm.putObject(cache, key, list); // issue #578 and #116
                }
                return list;
            }
        }
        return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    }
}

如果設(shè)置了flushCache="true",則每次查詢都會刷新緩存

<!-- 執(zhí)行此語句清空緩存 -->
<select id="getAll" resultType="entity.TDemo" useCache="true" flushCache="true" >
    select * from t_demo
</select>

如上,注意二級緩存是從 MappedStatement 中獲取的。由于 MappedStatement 存在于全局配置中,可以多個 CachingExecutor 獲取到,這樣就會出現(xiàn)線程安全問題。除此之外,若不加以控制,多個事務(wù)共用一個緩存實例,會導(dǎo)致臟讀問題。至于臟讀問題,需要借助其他類來處理,也就是上面代碼中 tcm 變量對應(yīng)的類型。下面分析一下。

TransactionalCacheManager


/*
 * 事務(wù)緩存管理器 
 */
public class TransactionalCacheManager {

    // Cache 與 TransactionalCache 的映射關(guān)系表
    private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();
    
    public void clear(Cache cache) {
        // 獲取 TransactionalCache 對象,并調(diào)用該對象的 clear 方法,下同
        getTransactionalCache(cache).clear();
    }
    
    public Object getObject(Cache cache, CacheKey key) {
        // 直接從TransactionalCache中獲取緩存
        return getTransactionalCache(cache).getObject(key);
    }
    
    public void putObject(Cache cache, CacheKey key, Object value) {
        // 直接存入TransactionalCache的緩存中
        getTransactionalCache(cache).putObject(key, value);
    }
 
    public void commit() {
        for (TransactionalCache txCache : transactionalCaches.values()) {
            txCache.commit();
        }
    }

    public void rollback() {
        for (TransactionalCache txCache : transactionalCaches.values()) {
            txCache.rollback();
        }
    }
    
    private TransactionalCache getTransactionalCache(Cache cache) {
        // 從映射表中獲取 TransactionalCache
        TransactionalCache txCache = transactionalCaches.get(cache);
        if (txCache == null) {
            // TransactionalCache 也是一種裝飾類,為 Cache 增加事務(wù)功能
            // 創(chuàng)建一個新的TransactionalCache,并將真正的Cache對象存進去     
            txCache = new TransactionalCache(cache);
            transactionalCaches.put(cache, txCache);
        }
        return txCache;
    }

}

TransactionalCacheManager 內(nèi)部維護了 Cache 實例與 TransactionalCache 實例間的映射關(guān)系,該類也僅負責(zé)維護兩者的映射關(guān)系,真正做事的還是 TransactionalCache。TransactionalCache 是一種緩存裝飾器,可以為 Cache 實例增加事務(wù)功能。我在之前提到的臟讀問題正是由該類進行處理的。下面分析一下該類的邏輯。

TransactionalCache

public class TransactionalCache implements Cache {
    
    //真正的緩存對象,和上面的Map<Cache, TransactionalCache>中的Cache是同一個
    private final Cache delegate;
    private boolean clearOnCommit;
    // 在事務(wù)被提交前,所有從數(shù)據(jù)庫中查詢的結(jié)果將緩存在此集合中
    private final Map<Object, Object> entriesToAddOnCommit;
    // 在事務(wù)被提交前,當(dāng)緩存未命中時,CacheKey 將會被存儲在此集合中
    private final Set<Object> entriesMissedInCache;

    public TransactionalCache(Cache delegate) {
        this.delegate = delegate;
        this.clearOnCommit = false;
        this.entriesToAddOnCommit = new HashMap<Object, Object>();
        this.entriesMissedInCache = new HashSet<Object>();
    }
    
    @Override
    public Object getObject(Object key) {
        // 查詢的時候是直接從delegate中去查詢的,也就是從真正的緩存對象中查詢
        Object object = delegate.getObject(key);
        if (object == null) {
            // 緩存未命中,則將 key 存入到 entriesMissedInCache 中
            entriesMissedInCache.add(key);
        }
        if (clearOnCommit) {
            return null;
        } else {
            return object;
        }
    }
    
    @Override
    public void putObject(Object key, Object object) {
        // 將鍵值對存入到 entriesToAddOnCommit 這個Map中中,而非真實的緩存對象 delegate 中
        entriesToAddOnCommit.put(key, object);
    }
    
    @Override
    public Object removeObject(Object key) {
        return null;
    }
    
    @Override
    public void clear() {
        clearOnCommit = true;
        // 清空 entriesToAddOnCommit,但不清空 delegate 緩存
        entriesToAddOnCommit.clear();
    }
    
    public void commit() {
        // 根據(jù) clearOnCommit 的值決定是否清空 delegate
        if (clearOnCommit) {
            delegate.clear();
        }
        // 刷新未緩存的結(jié)果到 delegate 緩存中
        flushPendingEntries();
        // 重置 entriesToAddOnCommit 和 entriesMissedInCache
        reset();
    }

    public void rollback() {
        unlockMissedEntries();
        reset();
    }
    
    private void reset() {
        clearOnCommit = false;
        // 清空集合
        entriesToAddOnCommit.clear();
        entriesMissedInCache.clear();
    }
    
    private void flushPendingEntries() {
        for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
            // 將 entriesToAddOnCommit 中的內(nèi)容轉(zhuǎn)存到 delegate 中
            delegate.putObject(entry.getKey(), entry.getValue());
        }
        for (Object entry : entriesMissedInCache) {
            if (!entriesToAddOnCommit.containsKey(entry)) {
                // 存入空值
                delegate.putObject(entry, null);
            }
        }
    }
    
    private void unlockMissedEntries() {
        for (Object entry : entriesMissedInCache) {
            try {
                // 調(diào)用 removeObject 進行解鎖
                delegate.removeObject(entry);
            } catch (Exception e) {
                log.warn("Unexpected exception while notifiying a rollback to the cache adapter."
                    + "Consider upgrading your cache adapter to the latest version.  Cause: " + e);
            }
        }
    }

}

存儲二級緩存對象的時候是放到了TransactionalCache.entriesToAddOnCommit這個map中,但是每次查詢的時候是直接從TransactionalCache.delegate中去查詢的,所以這個二級緩存查詢數(shù)據(jù)庫后,設(shè)置緩存值是沒有立刻生效的,主要是因為直接存到 delegate 會導(dǎo)致臟數(shù)據(jù)問題。

為何只有SqlSession提交或關(guān)閉之后二級緩存才會生效?

那我們來看下SqlSession.commit()方法做了什么

SqlSession
public class DefaultSqlSession implements SqlSession {

    private final Configuration configuration;
    private final Executor executor;

    @Override
    public void commit(boolean force) {
        try {
            // 主要是這句
            executor.commit(isCommitOrRollbackRequired(force));
            dirty = false;
        } catch (Exception e) {
            throw ExceptionFactory.wrapException("Error committing transaction.  Cause: " + e, e);
        } finally {
            ErrorContext.instance().reset();
        }
    }
}


public class CachingExecutor implements Executor {

    private final Executor delegate;
    private final TransactionalCacheManager tcm = new TransactionalCacheManager();
    
    @Override
    public void commit(boolean required) throws SQLException {
        delegate.commit(required);
        tcm.commit();// 在這里
    }
}

public class TransactionalCacheManager {

    private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();
    
    public void commit() {
        for (TransactionalCache txCache : transactionalCaches.values()) {
            txCache.commit();// 在這里
        }
    }
}

public class TransactionalCache implements Cache {

    private final Cache delegate;

    public void commit() {
        if (clearOnCommit) {
            delegate.clear();
        }
        flushPendingEntries();//這一句
        reset();
    }
    
    private void flushPendingEntries() {
        for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
            // 在這里真正的將entriesToAddOnCommit的對象逐個添加到delegate中,只有這時,二級緩存才真正的生效
            delegate.putObject(entry.getKey(), entry.getValue());
        }
        for (Object entry : entriesMissedInCache) {
            if (!entriesToAddOnCommit.containsKey(entry)) {
                delegate.putObject(entry, null);
            }
        }
    }
}

如果從數(shù)據(jù)庫查詢到的數(shù)據(jù)直接存到 delegate 會導(dǎo)致臟數(shù)據(jù)問題。下面通過一張圖演示一下臟數(shù)據(jù)問題發(fā)生的過程,假設(shè)兩個線程開啟兩個不同的事務(wù),它們的執(zhí)行過程如下:

如上圖,時刻2,事務(wù) A 對記錄 A 進行了更新。時刻3,事務(wù) A 從數(shù)據(jù)庫查詢記錄 A,并將記錄 A 寫入緩存中。時刻4,事務(wù) B 查詢記錄 A,由于緩存中存在記錄 A,事務(wù) B 直接從緩存中取數(shù)據(jù)。這個時候,臟數(shù)據(jù)問題就發(fā)生了。事務(wù) B 在事務(wù) A 未提交情況下,讀取到了事務(wù) A 所修改的記錄。為了解決這個問題,我們可以為每個事務(wù)引入一個獨立的緩存。查詢數(shù)據(jù)時,仍從 delegate 緩存(以下統(tǒng)稱為共享緩存)中查詢。若緩存未命中,則查詢數(shù)據(jù)庫。存儲查詢結(jié)果時,并不直接存儲查詢結(jié)果到共享緩存中,而是先存儲到事務(wù)緩存中,也就是 entriesToAddOnCommit 集合。當(dāng)事務(wù)提交時,再將事務(wù)緩存中的緩存項轉(zhuǎn)存到共享緩存中。這樣,事務(wù) B 只能在事務(wù) A 提交后,才能讀取到事務(wù) A 所做的修改,解決了臟讀問題。

二級緩存的刷新

我們來看看SqlSession的更新操作

public class DefaultSqlSession implements SqlSession {

    private final Configuration configuration;
    private final Executor executor;

    @Override
    public int update(String statement, Object parameter) {
        try {
            dirty = true;
            MappedStatement ms = configuration.getMappedStatement(statement);
            return executor.update(ms, wrapCollection(parameter));
        } catch (Exception e) {
            throw ExceptionFactory.wrapException("Error updating database.  Cause: " + e, e);
        } finally {
            ErrorContext.instance().reset();
        }
    }
}

public class CachingExecutor implements Executor {

    private final Executor delegate;
    private final TransactionalCacheManager tcm = new TransactionalCacheManager();

    @Override
    public int update(MappedStatement ms, Object parameterObject) throws SQLException {
        flushCacheIfRequired(ms);
        return delegate.update(ms, parameterObject);
    }
    
    private void flushCacheIfRequired(MappedStatement ms) {
        //獲取MappedStatement對應(yīng)的Cache,進行清空
        Cache cache = ms.getCache();
        //SQL需設(shè)置flushCache="true" 才會執(zhí)行清空
        if (cache != null && ms.isFlushCacheRequired()) {      
            tcm.clear(cache);
        }
    }
}

MyBatis二級緩存只適用于不常進行增、刪、改的數(shù)據(jù),比如國家行政區(qū)省市區(qū)街道數(shù)據(jù)。一但數(shù)據(jù)變更,MyBatis會清空緩存。因此二級緩存不適用于經(jīng)常進行更新的數(shù)據(jù)。

使用redis存儲二級緩存

通過上面代碼分析,我們知道二級緩存默認(rèn)和一級緩存都是使用的PerpetualCache存儲結(jié)果,一級緩存只要SQLSession關(guān)閉就會清空,其內(nèi)部使用HashMap實現(xiàn),所以二級緩存無法實現(xiàn)分布式,并且服務(wù)器重啟后就沒有緩存了。此時就需要引入第三方緩存中間件,將緩存的值存到外部,如redis和ehcache

修改mapper.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.yibo.saas.common.dal.dao.AreaDefaultMapper">
 
    <!--
    flushInterval(清空緩存的時間間隔): 單位毫秒,可以被設(shè)置為任意的正整數(shù)。
        默認(rèn)情況是不設(shè)置,也就是沒有刷新間隔,緩存僅僅調(diào)用語句時刷新。
    size(引用數(shù)目): 可以被設(shè)置為任意正整數(shù),要記住你緩存的對象數(shù)目和你運行環(huán)境的可用內(nèi)存資源數(shù)目。默認(rèn)值是1024。
    readOnly(只讀):屬性可以被設(shè)置為true或false。只讀的緩存會給所有調(diào)用者返回緩存對象的相同實例。
        因此這些對象不能被修改。這提供了很重要的性能優(yōu)勢。可讀寫的緩存會返回緩存對象的拷貝(通過序列化)。這會慢一些,但是安全,因此默認(rèn)是false。
    eviction(回收策略): 默認(rèn)的是 LRU:
        1.LRU – 最近最少使用的:移除最長時間不被使用的對象。
        2.FIFO – 先進先出:按對象進入緩存的順序來移除它們。
        3.SOFT – 軟引用:移除基于垃圾回收器狀態(tài)和軟引用規(guī)則的對象。
        4.WEAK – 弱引用:更積極地移除基于垃圾收集器狀態(tài)和弱引用規(guī)則的對象。
    blocking(是否使用阻塞緩存): 默認(rèn)為false,當(dāng)指定為true時將采用BlockingCache進行封裝,blocking,阻塞的意思,
        使用BlockingCache會在查詢緩存時鎖住對應(yīng)的Key,如果緩存命中了則會釋放對應(yīng)的鎖,否則會在查詢數(shù)據(jù)庫以后再釋放鎖,
        這樣可以阻止并發(fā)情況下多個線程同時查詢數(shù)據(jù),詳情可參考BlockingCache的源碼。
    type(緩存類):可指定使用的緩存類,mybatis默認(rèn)使用HashMap進行緩存,這里引用第三方中間件進行緩存
    -->
    <cache type="org.mybatis.caches.redis.RedisCache" blocking="false"
           flushInterval="0" readOnly="true" size="1024" eviction="FIFO"/>
 
    <!--
        useCache(是否使用緩存):默認(rèn)true使用緩存
    -->
    <select id="find" parameterType="map" resultType="com.yibo.model.User" useCache="true">
        SELECT * FROM user
    </select>
 
</mapper>

依然很簡單, RedisCache 在保存緩存數(shù)據(jù)和獲取緩存數(shù)據(jù)時,使用了Java的序列化和反序列化,因此需要保證被緩存的對象必須實現(xiàn)Serializable接口。

也可以自己實現(xiàn)cache

public class RedisCache implements Cache {

    private final String id;

    private static ValueOperations<String, Object> valueOs;

    private static RedisTemplate<String, String> template;


    public static void setValueOs(ValueOperations<String, Object> valueOs) {
        RedisCache.valueOs = valueOs;
    }

    public static void setTemplate(RedisTemplate<String, String> template) {
        RedisCache.template = template;
    }

    private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();


    public RedisCache(String id) {
        if (id == null) {
            throw new IllegalArgumentException("Cache instances require an ID");
        }
        this.id = id;
    }

    @Override
    public String getId() {
        return this.id;
    }

    @Override
    public void putObject(Object key, Object value) {
        valueOs.set(key.toString(), value, 10, TimeUnit.MINUTES);
    }

    @Override
    public Object getObject(Object key) {
        return valueOs.get(key.toString());
    }

    @Override
    public Object removeObject(Object key) {
        valueOs.set(key.toString(), "", 0, TimeUnit.MINUTES);
        return key;
    }

    @Override
    public void clear() {
        template.getConnectionFactory().getConnection().flushDb();
    }

    @Override
    public int getSize() {
        return template.getConnectionFactory().getConnection().dbSize().intValue();
    }

    @Override
    public ReadWriteLock getReadWriteLock() {
        return this.readWriteLock;
    }
}

Mapper中配置自己實現(xiàn)的Cache

<cache type="com.chenhao.mybatis.cache.RedisCache"/> 

參考:
https://www.cnblogs.com/java-chen-hao/p/11770064.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容