MyBatis框架原理3:緩存
上一篇[MyBatis框架原理2:SqlSession運行過程][1]介紹了MyBatis的工作流程,其中涉及到了MyBatis緩存的使用,首先回顧一下工作流程圖:
如果開啟了二級緩存,數據查詢執行過程就是首先從二級緩存中查詢,如果未命中則從一級緩存中查詢,如果也未命中則從數據庫中查詢。MyBatis的一級和二級緩存都是基於Cache接口的實現,下面先來看看Cache接口和其各種實現類。
Cache接口及常用裝飾器
public interface Cache { String getId(); //緩存中添加數據,key為生成的CacheKey,value為查詢結果 void putObject(Object key, Object value); //查詢 Object getObject(Object key); //刪除 Object removeObject(Object key); //清空緩存 void clear(); //獲取緩存數量 int getSize(); //獲取讀寫鎖 ReadWriteLock getReadWriteLock(); }
Cache接口位於MyBatis的cache包下,定義了緩存的基本方法,其實現類采用了裝飾器模式,通過實現類的組裝,可以實現操控緩存的功能。cache包結構如下:
- PerpetualCache是Cache接口的實現類,通過內部的HashMap來對緩存進行基本的操作,通常配合裝飾器類一起使用。
- BlockingCache裝飾器:保證只有一個線程到數據庫中查詢指定key的數據,如果該線程在BlockingCache中未查找到數據,就獲取key對應的鎖,阻塞其他查詢這個key的線程,通過其內部ConcurrentHashMap來實現,源碼如下:
public class BlockingCache implements Cache { //阻塞時長 private long timeout; private final Cache delegate; //key和ReentrantLock對象一一對應 private final ConcurrentHashMap<Object, ReentrantLock> locks; @Override public Object getObject(Object key) { //獲取key的鎖 acquireLock(key); //根據key查詢 Object value = delegate.getObject(key); //如果命中緩存,釋放鎖,未命中則繼續持有鎖 if (value != null) { releaseLock(key); } return value; } @Override //從數據庫獲取結果後,將結果放入BlockingCache,然後釋放鎖 public void putObject(Object key, Object value) { try { delegate.putObject(key, value); } finally { releaseLock(key); } } ...
- FifoCache裝飾器: 先入先出規則刪除最早的緩存,通過其內部的Deque實現。
- LruCache裝飾器: 刪除最近使用最少的緩存, 通過內部的LinkedHashMap實現。
- SynchronizedCache裝飾器:同步Cache。
- LoggingCache裝飾器: 提供日誌功能,記錄和輸出緩存命中率。
- SerializedCache裝飾器:序列化功能。
CacheKey
CacheKey對象是用來確認緩存項的唯一標識,由其內部ArrayList添加的所有對象來確認兩個CacheKey是否相同,通常ArrayList內將添加MappedStatement的id,SQL語句,用戶傳遞給SQL語句的參數以及查詢結果集範圍RowBounds等,CacheKey源碼如下:
public class CacheKey implements Cloneable, Serializable {
...
private final int multiplier;
private int hashcode;
private long checksum;
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>();
}
//向updateLis中添加對象
public void update(Object object) {
int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
count++;
checksum += baseHashCode;
baseHashCode *= count;
hashcode = multiplier * hashcode + baseHashCode;
updateList.add(object);
}
@Override
//重寫equals方法判斷CacheKey是否相同
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (!(object instanceof CacheKey)) {
return false;
}
final CacheKey cacheKey = (CacheKey) object;
if (hashcode != cacheKey.hashcode) {
return false;
}
if (checksum != cacheKey.checksum) {
return false;
}
if (count != cacheKey.count) {
return false;
}
//比較updateList中每一項
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;
}
}
一級緩存
一級緩存是session級別緩存,只存在當前會話中,在沒有任何配置下,MyBatis默認開啟一級緩存,當一個SqlSession第一次執行SQL語句和參數查詢時,將生成的CacheKey和查詢結果放入緩存中,下一次通過相同的SQL語句和參數查詢時,就會從緩存中獲取,當進行更新或者插入操作時,一級緩存會進行清空。在上一篇中說到,MayBatis進行一級緩存查詢和寫入是由BaseExecutor執行的,源碼如下:
- 初始化緩存:
一級緩存是Cache接口的PerpetualCache實現類對象
public abstract class BaseExecutor implements Executor {
...
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>();
//一級緩存初始化
this.localCache = new PerpetualCache("LocalCache");
this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
this.closed = false;
this.configuration = configuration;
this.wrapper = this;
}
...
- 生成CacheKey
key在CachingExecutor中生成,CacheKey的updateList中放入了MappedStatement,傳入SQL的參數,結果集範圍rowBounds和boundSql:
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);
}
- 將查詢結果和CacheKey放入緩存:
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
//緩存中放入CacheKey和占位符
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
//在數據庫中查詢操作
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
//緩存中放入CacheKey和結果集
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
//返回結果
return list;
}
- 再次執行相同查詢條件時從緩存獲取結果:
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
clearLocalCache();
}
}
return list;
}
- 更新操作時清空緩存:
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.");
}
//清空緩存
clearLocalCache();
return doUpdate(ms, parameter);
}
通過以下代碼驗證下,分別開兩個session進行相同的查詢,第一個session查詢兩次:
public void testSelect() {
SqlSession sqlSession = sqlSessionFactory.openSession();
User user = sqlSession.selectOne("findUserById", 1);
System.out.println(user);
User user2 = sqlSession.selectOne("findUserById", 1);
System.out.println(user2);
sqlSession.close();
System.out.println("sqlSession closed!===================================");
//新建會話
SqlSession sqlSession2 = sqlSessionFactory.openSession();
User user3 = sqlSession2.selectOne("findUserById", 1);
System.out.println(user3);
sqlSession2.close();
}
把日誌設置為DEBUG級別得到運行日誌:
DEBUG [main] - ==> Preparing: SELECT * FROM user WHERE id = ?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
User [id=1, username=小明, birthday=null, sex=1, address=四川成都]
User [id=1, username=小明, birthday=null, sex=1, address=四川成都]
DEBUG [main] - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@16022d9d]
DEBUG [main] - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@16022d9d]
DEBUG [main] - Returned connection 369241501 to pool.
sqlSession closed!===================================
DEBUG [main] - Opening JDBC Connection
DEBUG [main] - Checked out connection 369241501 from pool.
DEBUG [main] - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@16022d9d]
DEBUG [main] - ==> Preparing: SELECT * FROM user WHERE id = ?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
User [id=1, username=小明, birthday=null, sex=1, address=四川成都]
DEBUG [main] - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@16022d9d]
DEBUG [main] - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@16022d9d]
DEBUG [main] - Returned connection 369241501 to pool.
第一次會話中,雖然查詢了兩次id為1的用戶,但是只執行了一次SQL,關閉會話後開啟一次新的會話,再次查詢id為1的用戶,SQL再次執行,說明了一級緩存只存在SqlSession中,不同SqlSession不能共享。
二級緩存
二級緩存是Mapper級別緩存,也就是同一Mapper下不同的session共享二級緩存區域。
只需要在XML映射文件中增加cache標簽或cache-ref標簽標簽就可以開啟二級緩存,cache-ref標簽配置的是共享其指定Mapper的二級緩存區域。具體配置信息如下:
- blocking : 是否使用阻塞緩存
- readOnly : 是否只讀
- eviction: 緩存策略,可指定Cache接口下裝飾器類FifoCache、LruCache、SoftCache和WeakCache
- flushInterval : 自動刷新緩存時間
- size : 設置緩存個數
- type : 設置緩存類型,用於自定義緩存類,默認為PerpetualCache
二級緩存是在MyBatis的解析配置文件時初始化,在XMLMapperBuilder中將緩存配置解析:
private void cacheElement(XNode context) throws Exception {
if (context != null) {
//指定默認類型為PerpetualCache
String type = context.getStringAttribute("type", "PERPETUAL");
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
//默認緩存策略為LruCache
String eviction = context.getStringAttribute("eviction", "LRU");
Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
Long flushInterval = context.getLongAttribute("flushInterval");
Integer size = context.getIntAttribute("size");
boolean readWrite = !context.getBooleanAttribute("readOnly", false);
boolean blocking = context.getBooleanAttribute("blocking", false);
Properties props = context.getChildrenAsProperties();
//委托builderAssistant構建二級緩存
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, 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)
//設置緩存類型,默認為PerpetualCache
.implementation(valueOrDefault(typeClass, PerpetualCache.class))
//設置緩存策略,默認使用LruCache裝飾器
.addDecorator(valueOrDefault(evictionClass, LruCache.class))
//設置刷新時間
.clearInterval(flushInterval)
//設置大小
.size(size)
//設置是否只讀
.readWrite(readWrite)
.blocking(blocking)
.properties(props)
.build();
configuration.addCache(cache);
currentCache = cache;
return cache;
}
最終得到默認的二級緩存對象結構為:
CachingExecutor將初始化的Cache對象用TransactionalCache包裝後放入TransactionalCacheManager的Map中,下面代碼中的tcm就是TransactionalCacheManager對象,CachingExecutor執行二級緩存操作過程:
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
//從Configuration的MappedStatement中獲取二級緩存
Cache cache = ms.getCache();
if (cache != null) {
//判斷是否需要刷新緩存,SELECT不刷新,INSERT|UPDATE|DELETE刷新緩存
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
//從二級緩存中獲取數據
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
//委托BaseExecutor查詢
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
//查詢結果放入二級緩存
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
通過之前一級緩存的例子驗證二級緩存,只需要在UserMapper映射文件中加入cache標簽,並且讓相關POJO類實現java.io.Serializable接口,運行得到日誌:
DEBUG [main] - ==> Preparing: SELECT * FROM user WHERE id = ?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
User [id=1, username=小明, birthday=null, sex=1, address=四川成都]
DEBUG [main] - Cache Hit Ratio [com.kkb.mybatis.mapper.UserMapper]: 0.0
User [id=1, username=小明, birthday=null, sex=1, address=四川成都]
DEBUG [main] - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@5c072e3f]
DEBUG [main] - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@5c072e3f]
DEBUG [main] - Returned connection 1543974463 to pool.
sqlSession closed!===================================
DEBUG [main] - Cache Hit Ratio [com.kkb.mybatis.mapper.UserMapper]: 0.3333333333333333
User [id=1, username=小明, birthday=null, sex=1, address=四川成都]
不同session查詢同一條記錄時,總共只執行了一次SQL語句,並且日誌打印出了緩存的命中率,這時候不同session已經共享了二級緩存區域。
[1]: https://www.cnblogs.com/abcboy/p/9656302.html
MyBatis框架原理3:緩存