前言
像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"/>