預備知識
對 Mybatis 的執行流程有一定的瞭解。
一級緩存
在讀多寫少的情況下,減少了網絡的開銷,顯著提升性能。
配置
public enum LocalCacheScope {
SESSION,STATEMENT
}
對應到配置文件
<setting name="localCacheScope" value="SESSION"/>
支持 SESSION 和 STATEMENT 兩種。其中
SESSION :指在當前事務中都使用查詢緩存。
STATEMENT:指在當前 Sql 語句中使用查詢緩存
注:如果是 SESSION 的情況下,如果有多個 SqlSession 同時操作操作數據庫的同一張表,就可能出現髒讀。
實現原理
一級緩存的實現在 BaseExecutor:每一個SqlSession中持有了自己的Executor,每一個Executor(都繼承自 BaseExecutor)中有一個Local Cache。
當用戶發起查詢時,Mybatis會根據當前執行的MappedStatement生成一個key,去Local Cache中查詢,
- 如果緩存命中的話
- 如果緩存沒有命中的話,將寫一個佔位符,這樣後續的讀,就可以利用緩存了。之後查詢 DB,將結果寫入Local Cache。
- 如果配置爲 STATEMENT,清除緩存
最後返回結果給用戶。
public abstract class BaseExecutor implements Executor {
//本地緩存,一個 Executor 對應一個 localCache
protected PerpetualCache localCache;
protected PerpetualCache localOutputParameterCache;
//key 爲 Statement Id + Offset + Limmit + Sql + Params
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
//從本地緩存查
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
//本地緩存命中
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
//本地緩存沒有命中
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
// STATEMENT 級別,清除緩存
clearLocalCache();
}
}
return list;
}
@Override
public void clearLocalCache() {
if (!closed) {
localCache.clear();
localOutputParameterCache.clear();
}
}
private void handleLocallyCachedOutputParameters(MappedStatement ms, CacheKey key, Object parameter, BoundSql boundSql) {
if (ms.getStatementType() == StatementType.CALLABLE) {
final Object cachedParameter = localOutputParameterCache.getObject(key);
if (cachedParameter != null && parameter != null) {
final MetaObject metaCachedParameter = configuration.newMetaObject(cachedParameter);
final MetaObject metaParameter = configuration.newMetaObject(parameter);
for (ParameterMapping parameterMapping : boundSql.getParameterMappings()) {
if (parameterMapping.getMode() != ParameterMode.IN) {
final String parameterName = parameterMapping.getProperty();
final Object cachedValue = metaCachedParameter.getValue(parameterName);
metaParameter.setValue(parameterName, cachedValue);
}
}
}
}
}
//這裏緩存更新是有講究的。
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
//寫緩存的佔位符,下次查詢就不會走這裏了。避免了緩存穿透
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
//從數據庫查詢
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
//將數據庫數據寫入緩存。如果在此之前將來了新的查詢,將返回 EXECUTION_PLACEHOLDER
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
//每次更新的時候都會清空緩存。update,delete, insert 都調用 update 來完成。
clearLocalCache();
return doUpdate(ms, parameter);
}
}
一級緩存演示&失效情況
同一次會話期間只要查詢過的數據都會保存在當前SqlSession的一個Map中
- key:hashCode+查詢的SqlId+編寫的sql查詢語句+參數
- 一級緩存失效的四種情況
1、不同的SqlSession對應不同的一級緩存
2、同一個SqlSession但是查詢條件不同
3、同一個SqlSession兩次查詢期間執行了任何一次增刪改操作
4、同一個SqlSession兩次查詢期間手動清空了緩存
驗證
配置爲 SESSION
- 同一個事務 sqlSession 3次查詢同一個表,檢查後續請求是否查詢緩存
- 三個事務,每個事實的 sqlSession 各查詢同一張表一次。檢查後續請求是否查詢緩存
- 兩個 sqlSession,一個事務 sqlSession1 連續兩次查詢表,sqlSession2 在 sqlSession1 的兩次查詢中間修改表
- 兩個 sqlSession,兩個事務 sqlSession1 各一次查詢表,sqlSession2 在兩次查詢中間修改表
配置爲 STATEMENT
- 同一個事務 sqlSession 3次查詢同一個表,檢查後續請求是否查詢緩存
- 三個事務,每個事實的 sqlSession 各查詢同一張表一次。檢查後續請求是否查詢緩存
- 兩個 sqlSession,一個事務 sqlSession1 連續兩次查詢表,sqlSession2 在 sqlSession1 的兩次查詢中間修改表
- 兩個 sqlSession,兩個事務 sqlSession1 各一次查詢表,sqlSession2 在兩次查詢中間修改表
總結
- Mybatis一級緩存的生命週期和SqlSession一致。
- 不管是 SESSION 還是 STATEMENT 在跨事務都不共享緩存。因爲在事務提交的時候會清空緩存。
- Mybatis的緩存是一個粗粒度的緩存,沒有更新緩存和緩存過期的概念,同時只是使用了默認的hashmap,也沒有做容量上的限定。
- Mybatis的一級緩存最大範圍是SqlSession內部,有多個SqlSession或者分佈式的環境下,有操作數據庫寫的話,會引起髒數據,建議是把一級緩存的默認級別設定爲Statement,即不使用一級緩存。
Cache 接口
public interface Cache {
/**
* @return The identifier of this cache
*/
String getId();
/**
* @param key Can be any object but usually it is a {@link CacheKey}
* @param value The result of a select.
*/
void putObject(Object key, Object value);
/**
* @param key The key
* @return The object stored in the cache.
*/
Object getObject(Object key);
/**
* As of 3.3.0 this method is only called during a rollback
* for any previous value that was missing in the cache.
* This lets any blocking cache to release the lock that
* may have previously put on the key.
* A blocking cache puts a lock when a value is null
* and releases it when the value is back again.
* This way other threads will wait for the value to be
* available instead of hitting the database.
*
*
* @param key The key
* @return Not used
*/
Object removeObject(Object key);
/**
* Clears this cache instance
*/
void clear();
/**
* Optional. This method is not called by the core.
*
* @return The number of elements stored in the cache (not its capacity).
*/
int getSize();
}
實現類:
- PerpetualCache:HashMap 保存 key,value
裝飾器類
- blockingCache : 基於 key 的鎖。每個 key 一個鎖。注:只有當前線程才能釋放鎖。也可以通過設置超時時間,避免一直阻塞等待鎖。問題:putObject 爲什麼沒有加鎖?
- FifoCache:FIFO 的淘汰策略。默認隊列大小爲 1024。
- LoggingCache:用日誌記錄查詢的命中率
- LruCache:利用 LinkHashMap 實現 Lru 算法。由於 LinkHashMap 實現了 LRU 算法,這裏非常取巧
- ScheduledCache:每間隔 clearInterval(默認一小時)將所有緩存清空。
- SerializedCache:將 value 用 ObjectInputStream,ByteArrayInputStream 序列化,對 key 沒有序列化。
- SoftCache:第一次將值存儲在軟引用對象中,每次操作(put 和 remove)時都將已經被 GC 的 valuel 刪除。每次獲取(get),對應的軟引用沒有被刪除,就加入Deqeue隊列。Deque 是一個默認長度爲 256 的先進先出隊列。這種實現機制非常類似 Java 的 GC 的簡化版。
- SynchronizedCache:每個操作前面加了 synchronized 關鍵詞
- TransactionalCache:類似數據庫事務機制。將 put 緩存在臨時 Map 中,當調用 commit 的時候,才清除臨時Map,寫入真正 cache 中。
- WeakCache:第一次將值存儲在弱引用對象中,每次操作(put 和 remove)時都將已經被 GC 的 valuel 刪除。每次獲取(get),對應的弱引用沒有被刪除,就加入Deqeue隊列。Deque 是一個默認長度爲 256 的先進先出隊列。這種實現機制非常類似 Java 的 GC 的簡化版。
- TransactionalCacheManager:保存 Cache 和 TransactionalCache 的映射關係
Cache 建造者
CacheBuilder。
二級緩存
- 二級緩存(second level cache),全局作用域緩存;二級緩存默認不開啓,需要手動配置
- MyBatis提供二級緩存的接口以及實現,緩存實現要求 POJO實現Serializable接口
- 二級緩存在 SqlSession 關閉或提交之後纔會生效
配置
<setting name="cacheEnabled" value="true"/>
每個 Mapper 支持 CacheNameSpace 和 CacheNameRef 來配置,先解析配置文件,後解析註解,註解的配置會覆蓋配置文件的配置。配置文件先解析 cache-ref 標籤,後解析 cache 標籤。 註解先解析CacheNameSpace,後解析 CacheNameRef。CacheNameRef 會覆蓋 CacheNameSpace的配置。因此,配置文件和接口註釋是不能夠配合使用的。
CacheNameSpace
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface CacheNamespace {
Class<? extends org.apache.ibatis.cache.Cache> implementation() default PerpetualCache.class;
Class<? extends org.apache.ibatis.cache.Cache> eviction() default LruCache.class;
long flushInterval() default 0;
int size() default 1024;
boolean readWrite() default true;
boolean blocking() default false;
/**
* Property values for a implementation object.
* @since 3.4.2
*/
Property[] properties() default {};
}
private void parseCache() {
CacheNamespace cacheDomain = type.getAnnotation(CacheNamespace.class);
if (cacheDomain != null) {
Integer size = cacheDomain.size() == 0 ? null : cacheDomain.size();
Long flushInterval = cacheDomain.flushInterval() == 0 ? null : cacheDomain.flushInterval();
Properties props = convertToProperties(cacheDomain.properties());
assistant.useNewCache(cacheDomain.implementation(), cacheDomain.eviction(), flushInterval, size, cacheDomain.readWrite(), cacheDomain.blocking(), props);
}
}
private Properties convertToProperties(Property[] properties) {
if (properties.length == 0) {
return null;
}
Properties props = new Properties();
for (Property property : properties) {
props.setProperty(property.name(),
PropertyParser.parse(property.value(), configuration.getVariables()));
}
return props;
}
public Cache useNewCache(Class<? extends Cache> typeClass,
Class<? extends Cache> evictionClass,
Long flushInterval,
Integer size,
boolean readWrite,
boolean blocking,
Properties props) {
Cache cache = new CacheBuilder(currentNamespace)
.implementation(valueOrDefault(typeClass, PerpetualCache.class))
.addDecorator(valueOrDefault(evictionClass, LruCache.class))
.clearInterval(flushInterval)
.size(size)
.readWrite(readWrite)
.blocking(blocking)
.properties(props)
.build();
configuration.addCache(cache);
currentCache = cache;
return cache;
}
支持配置文件和註解兩種,配置文件先解析,之後註解,註解會覆蓋配置文件。最終還是調用 CacheBuilder 來創建 Cache。
方式一:配置文件
解析 cache 標籤
<?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.example.TestMapper">
<cache type="com.example.MybatisRedisCache" eviction="FIFO" flushInterval="60000" readOnly="false" size="1024" blocking="false">
<property name="key1" value="value1" />
<property name="key2" value="value2" />
</cache>
</mapper>
-
type:默認只實現了 PerPetualCache,默認也只能是PerPetualCache,如果你實現了自己的 Cache,可以配置。比如,你可以實現用 Redis 或 ECache,實現 Cache 接口,即可。
-
eviction:實現了 LruCache、FifoCache、SoftCache、WeakCache
-
flushInterval:多長時間將所有緩存全部清空。實際爲 ScheculedCache
-
size:緩存數量
-
readOnly:默認 false,如果爲 false,對 value 進行序列化。否則不進行序列化
-
blocking:默認 false。是否是阻塞式的,如果拿不到鎖,一直阻塞。
方式二:註解方式
@CacheNamespace(implementation = MybatisRedisCache.class, size=256, flushInterval=10000, readWrite=true, blocking=false, eviction=FiFoCache.class, properties={"eviction":"LRU", "flushInterval":"100",})
public interface TestMapper(
@Select("select t_user.* from t_user where t_user.t_user_id = #{tUserId}")
@Options(useCache = true)
List<PeoplePo> getUser(PeopleVo p);
}
CacheNamespaceRef
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface CacheNamespaceRef {
/**
* A namespace type to reference a cache (the namespace name become a FQCN of specified type)
*/
Class<?> value() default void.class;
/**
* A namespace name to reference a cache
* @since 3.4.2
*/
String name() default "";
}
private void parseCacheRef() {
CacheNamespaceRef cacheDomainRef = type.getAnnotation(CacheNamespaceRef.class);
if (cacheDomainRef != null) {
Class<?> refType = cacheDomainRef.value();
String refName = cacheDomainRef.name();
//不能都不配置
if (refType == void.class && refName.isEmpty()) {
throw new BuilderException("Should be specified either value() or name() attribute in the @CacheNamespaceRef");
}
//不能都配置
if (refType != void.class && !refName.isEmpty()) {
throw new BuilderException("Cannot use both value() and name() attribute in the @CacheNamespaceRef");
}
//value 的優先級高於 name 的優先級
String namespace = (refType != void.class) ? refType.getName() : refName;
try {
assistant.useCacheRef(namespace);
} catch (IncompleteElementException e) {
configuration.addIncompleteCacheRef(new CacheRefResolver(assistant, namespace));
}
}
}
public Cache useCacheRef(String namespace) {
if (namespace == null) {
throw new BuilderException("cache-ref element requires a namespace attribute.");
}
try {
unresolvedCacheRef = true;
//從已有的 Cache 中查找
Cache cache = configuration.getCache(namespace);
if (cache == null) {
throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.");
}
currentCache = cache;
unresolvedCacheRef = false;
return cache;
} catch (IllegalArgumentException e) {
throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e);
}
}
用來引用已配置的 Cache。支持配置文件和註解兩種,配置文件先解析,之後註解,註解會覆蓋配置文件。
方式一:配置文件
CacheNamespaceRef 爲 cache-ref 標籤下的 namespace
<?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.example.TestMapper">
<cache-ref namespace="MybatisRedisCache"</cache-ref>
</mapper>
方式二:註解方式
支持 value 和 name 兩種方式,但只能配置一種,value 的優先級高於 name 的優先級。
@CacheNamespaceRef(implementation = MybatisRedisCache.class)
public interface TestMapper(
@Select("select t_user.* from t_user where t_user.t_user_id = #{tUserId}")
@Options(useCache = true)
List<PeoplePo> getUser(PeopleVo p);
}
配置緩存的方式
1、全局setting的cacheEnable:
- 配置二級緩存的開關。一級緩存一直是打開的。
2、select標籤的useCache屬性:
- 配置這個select是否使用二級緩存。一級緩存一直是使用的
3、sql標籤的flushCache屬性:
- 增刪改默認flushCache=true。sql執行以後,會同時清空一級和二級緩存。
- 查詢默認flushCache=false。
4、sqlSession.clearCache():
- 只是用來清除一級緩存。
5、二級緩存需要手動開啓和配置,他是基於namespace級別的緩存。當在某一個作用域 (一級緩存Session/二級緩存Namespaces) 進行了 C/U/D 操作後,默認該作用域下所有 select 中的緩存將被clear。
緩存初始化
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 {
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
有了前面的基礎,這裏理解起來就非常容易了。
public class CachingExecutor implements Executor {
private final Executor delegate;
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
public CachingExecutor(Executor delegate) {
this.delegate = delegate;
delegate.setExecutorWrapper(this);
}
@Override
public Transaction getTransaction() {
return delegate.getTransaction();
}
@Override
public void close(boolean forceRollback) {
try {
//issues #499, #524 and #573
if (forceRollback) {
tcm.rollback();
} else {
tcm.commit();
}
} finally {
delegate.close(forceRollback);
}
}
@Override
public boolean isClosed() {
return delegate.isClosed();
}
@Override
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
flushCacheIfRequired(ms);
return delegate.update(ms, parameterObject);
}
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
@Override
public <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException {
flushCacheIfRequired(ms);
return delegate.queryCursor(ms, parameter, rowBounds);
}
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
//查詢二級緩存
Cache cache = ms.getCache();
if (cache != null) {
flushCacheIfRequired(ms);
//配置使用緩存
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
//緩存爲空,從一級緩存中查詢
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
//加入二級緩存。問題:如果一級緩存返回 PLACEHOLDER,這裏是否始終PLACEHOLDER,導致返不期望的結果?
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
//緩存沒有命中,從代理中查。繼續查詢一級緩存。
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
@Override
public List<BatchResult> flushStatements() throws SQLException {
return delegate.flushStatements();
}
@Override
public void commit(boolean required) throws SQLException {
delegate.commit(required);
tcm.commit();
}
@Override
public void rollback(boolean required) throws SQLException {
try {
delegate.rollback(required);
} finally {
if (required) {
tcm.rollback();
}
}
}
private void ensureNoOutParams(MappedStatement ms, BoundSql boundSql) {
if (ms.getStatementType() == StatementType.CALLABLE) {
for (ParameterMapping parameterMapping : boundSql.getParameterMappings()) {
if (parameterMapping.getMode() != ParameterMode.IN) {
throw new ExecutorException("Caching stored procedures with OUT params is not supported. Please configure useCache=false in " + ms.getId() + " statement.");
}
}
}
}
@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
return delegate.createCacheKey(ms, parameterObject, rowBounds, boundSql);
}
@Override
public boolean isCached(MappedStatement ms, CacheKey key) {
return delegate.isCached(ms, key);
}
@Override
public void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType) {
delegate.deferLoad(ms, resultObject, property, key, targetType);
}
@Override
public void clearLocalCache() {
delegate.clearLocalCache();
}
private void flushCacheIfRequired(MappedStatement ms) {
Cache cache = ms.getCache();
if (cache != null && ms.isFlushCacheRequired()) {
tcm.clear(cache);
}
}
@Override
public void setExecutorWrapper(Executor executor) {
throw new UnsupportedOperationException("This method should not be called");
}
}
驗證
-
不提交事務,sqlSession1查詢完數據後,sqlSession2相同的查詢是否會從緩存中獲取數據。
-
當提交事務時,sqlSession1查詢完數據後,sqlSession2相同的查詢是否會從緩存中獲取數據。
-
同一個事務 sqlSession 3次查詢同一個表,檢查後續請求是否查詢緩存
-
兩個 sqlSession,一個事務 sqlSession1,sqlSession2 各連續兩次查詢表,sqlSession3 在 sqlSession1 的兩次查詢中間修改表,
SqlSession1 SqlSession2 SqlSession3 查詢 查詢 查詢 提交 查詢 更新 查詢 -
兩個 sqlSession,兩個事務 sqlSession1 各一次查詢表,sqlSession2 在兩次查詢中間修改表
總結
- Mybatis的二級緩存相對於一級緩存來說,實現了SqlSession之間緩存數據的共享,同時粒度更加的細,能夠到Mapper級別,通過Cache接口實現類不同的組合,對Cache的可控性也更強。
- Mybatis在多表查詢時,極大可能會出現髒數據,有設計上的缺陷,安全使用的條件比較苛刻。
- 在分佈式環境下,由於默認的Mybatis Cache實現都是基於本地的,分佈式環境下必然會出現讀取到髒數據,需要使用集中式緩存將Mybatis的Cache接口實現,有一定的開發成本,不如直接用Redis,Memcache實現業務上的緩存就好了
總結
mybatis 的 cache 設計還是很讚的。尤其是初始化部分 CacheBuilder。
但是 mybatis 的緩存總體上來說不如直接上 Redis 好。原因在於一不小心就會出現髒讀。