Mybatis 源碼分析(三)之 Mybatis 的一級緩存和二級緩存
Mybatis系列:
Mybatis 基礎介紹與逆向工程的構建 :http://www.lxweimin.com/p/1c18db4d7a38
Mybatis 源碼分析(一)之 Mybatis 的Executor的初始化:http://www.lxweimin.com/p/c7425c841337
Mybatis 源碼分析(二)之 Mybatis 操作數據庫的流程 :http://www.lxweimin.com/p/11d354ec3612
Mybatis 源碼分析(三)之 Mybatis 的一級緩存和二級緩存 :http://www.lxweimin.com/p/5515640d14fe
Mybatis緩存的作用
每當我們使用 MyBatis 開啟一次和數據庫的會話,MyBatis 會創建出一個 SqlSession 對象表示一次數據庫會話。
在對數據庫的一次會話中,我們有可能會反復地執行完全相同的查詢語句,如果不采取一些措施的話,每一次查詢都會查詢一次數據庫,而我們在極短的時間內做了完全相同的查詢,那么它們的結果極有可能完全相同,由于查詢一次數據庫的代價很大,這有可能造成很大的資源浪費。
為了解決這一問題,減少資源的浪費,MyBatis會在表示會話的SqlSession對象中建立一個簡單的緩存,將每次查詢到的結果結果緩存起來,當下次查詢的時候,如果判斷先前有個完全一樣的查詢,會直接從緩存中直接將結果取出,返回給用戶,不需要再進行一次數據庫查詢了。
mybatis的緩存有一級緩存和二級緩存。
一級緩存
一級緩存是默認開啟的,作用域是session級別的,緩存的key格式如下:
cache key: id + sql + limit + offset
在commit之前,第一次查詢結果換以key value的形式存起來,如果有相同的key進來,直接返回value,這樣有助于減輕數據的壓力。
相關源碼:
org.apache.ibatis.executor.BaseExecutor#createCacheKey
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (this.closed) {
throw new ExecutorException("Executor was closed.");
} else {
CacheKey cacheKey = new CacheKey();
cacheKey.update(ms.getId());
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
cacheKey.update(boundSql.getSql());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
for(int i = 0; i < parameterMappings.size(); ++i) {
ParameterMapping parameterMapping = (ParameterMapping)parameterMappings.get(i);
if (parameterMapping.getMode() != ParameterMode.OUT) {
String propertyName = parameterMapping.getProperty();
Object value;
if (boundSql.hasAdditionalParameter(propertyName)) {
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = this.configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
cacheKey.update(value);
}
}
if (this.configuration.getEnvironment() != null) {
cacheKey.update(this.configuration.getEnvironment().getId());
}
return cacheKey;
}
}
查詢數據庫并存入一級緩存的語句
org.apache.ibatis.executor.BaseExecutor#queryFromDatabase
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
this.localCache.putObject(key, ExecutionPlaceholder.EXECUTION_PLACEHOLDER);
List list;
try {
list = this.doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
this.localCache.removeObject(key);
}
//將查詢出來的結果存入一級緩存
this.localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
//如果是存儲過程把參數存入localOutputParameterCache
this.localOutputParameterCache.putObject(key, parameter);
}
return list;
}
并且當commit或者rollback的時候會清除緩存,并且當執行insert、update、delete的時候也會清除緩存。
相關源碼:
org.apache.ibatis.executor.BaseExecutor#update
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (this.closed) {
throw new ExecutorException("Executor was closed.");
} else {
//刪除一級緩存
this.clearLocalCache();
return this.doUpdate(ms, parameter);
}
}
org.apache.ibatis.executor.BaseExecutor#commit
public void commit(boolean required) throws SQLException {
if (this.closed) {
throw new ExecutorException("Cannot commit, transaction is already closed");
} else {
//刪除一級緩存
this.clearLocalCache();
this.flushStatements();
if (required) {
this.transaction.commit();
}
}
}
org.apache.ibatis.executor.BaseExecutor#rollback
public void rollback(boolean required) throws SQLException {
if (!this.closed) {
try {
//刪除一級緩存
this.clearLocalCache();
this.flushStatements(true);
} finally {
if (required) {
this.transaction.rollback();
}
}
}
}
二級緩存
二級緩存是手動開啟的,作用域為sessionfactory(也可以說MapperStatement級緩存,也就是一個namespace就會有一個緩存),因為二級緩存的數據不一定都是存儲到內存中,它的存儲介質多種多樣,實現二級緩存的時候,MyBatis要求返回的POJO必須是可序列化的,也就是要求實現Serializable接口,如果存儲在內存中的話,實測不序列化也可以的。
如果開啟了二級緩存的話,你的Executor將會被裝飾成CachingExecutor,緩存是通過CachingExecutor來操作的,查詢出來的結果會存在statement中的cache中,若有更新,刪除類的操作默認就會清空該MapperStatement的cache(也可以通過修改xml中的屬性,讓它不執行),不會影響其他的MapperStatement。
相關源碼:
org.apache.ibatis.session.Configuration#newExecutor(org.apache.ibatis.transaction.Transaction, org.apache.ibatis.session.ExecutorType)
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? this.defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Object 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);
}
//是否開啟緩存,傳入的參數為SimpleExecutor
if (this.cacheEnabled) {
executor = new CachingExecutor((Executor)executor);
}
//責任鏈模式攔截器
Executor executor = (Executor)this.interceptorChain.pluginAll(executor);
return executor;
}
query
org.apache.ibatis.executor.CachingExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler)
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
CacheKey key = this.createCacheKey(ms, parameterObject, rowBounds, boundSql);
return this.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
org.apache.ibatis.executor.CachingExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler, org.apache.ibatis.cache.CacheKey, org.apache.ibatis.mapping.BoundSql)
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
//獲得該MappedStatement的cache
Cache cache = ms.getCache();
//如果緩存不為空
if (cache != null) {
//看是否需要清除cache(在xml中可以配置flushCache屬性決定何時清空cache)
this.flushCacheIfRequired(ms);
//若開啟了cache且resultHandler 為空
if (ms.isUseCache() && resultHandler == null) {
this.ensureNoOutParams(ms, parameterObject, boundSql);
//從TransactionalCacheManager中取cache
List<E> list = (List)this.tcm.getObject(cache, key);
//若取出來list是空的
if (list == null) {
//查詢數據庫
list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
//將結果存入cache中
this.tcm.putObject(cache, key, list);
}
return list;
}
}
//如果緩存為空,去查詢數據庫
return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
對于
this.tcm.getObject(cache, key);
因同一個namespace下的MappedStatement的cache是同一個,而TransactionalCacheManager中統一管理cache是里面的屬性transactionalCaches,該屬性以MappedStatement中的Cache為key,TransactionalCache對象為Value。即一個namespace對應一個TransactionalCache。
相關源碼:
TransactionalCacheManager
org.apache.ibatis.cache.TransactionalCacheManager
public class TransactionalCacheManager {
private Map<Cache, TransactionalCache> transactionalCaches = new HashMap();
...
}
TransactionalCache
org.apache.ibatis.cache.decorators.TransactionalCache
public class TransactionalCache implements Cache {
private static final Log log = LogFactory.getLog(TransactionalCache.class);
//namespace中的cache
private Cache delegate;
//提交的時候清除cache的標志位
private boolean clearOnCommit;
//待提交的集合
private Map<Object, Object> entriesToAddOnCommit;
//未查到的key存放的集合
private Set<Object> entriesMissedInCache;
...
}
update
//更新
org.apache.ibatis.executor.CachingExecutor#update
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
//看是否需要清除cache(在xml中可以配置flushCache屬性決定何時清空cache)
this.flushCacheIfRequired(ms);
return this.delegate.update(ms, parameterObject);
}
org.apache.ibatis.executor.CachingExecutor#flushCacheIfRequired
private void flushCacheIfRequired(MappedStatement ms) {
//獲得cache
Cache cache = ms.getCache();
//若isFlushCacheRequired為true,則清除cache
if (cache != null && ms.isFlushCacheRequired()) {
this.tcm.clear(cache);
}
}
一級、二級緩存測試
因為一級緩存是默認生效的,下面是二級緩存開啟步驟。
mybatis-config.xml
<settings>
<!--這個配置使全局的映射器(二級緩存)啟用或禁用緩存-->
<setting name="cacheEnabled" value="true" />
</settings>
在mapper.xml可以進行如下的配置
<mapper>
<!--開啟本mapper的namespace下的二級緩存-->
<!--
eviction:代表的是緩存回收策略,目前MyBatis提供以下策略。
(1) LRU,最近最少使用的,一處最長時間不用的對象
(2) FIFO,先進先出,按對象進入緩存的順序來移除他們
(3) SOFT,軟引用,移除基于垃圾回收器狀態和軟引用規則的對象
(4) WEAK,弱引用,更積極的移除基于垃圾收集器狀態和弱引用規則的對象。這里采用的是LRU,
移除最長時間不用的對形象
flushInterval:刷新間隔時間,單位為毫秒,這里配置的是100秒刷新,如果你不配置它,那么當
SQL被執行的時候才會去刷新緩存。
size:引用數目,一個正整數,代表緩存最多可以存儲多少個對象,不宜設置過大。設置過大會導致內存溢出。
這里配置的是1024個對象
readOnly:只讀,意味著緩存數據只能讀取而不能修改,這樣設置的好處是我們可以快速讀取緩存,缺點是我們沒有
辦法修改緩存,他的默認值是false,不允許我們修改
-->
<cache eviction="LRU" flushInterval="100000" readOnly="true" size="1024"/>
<!--刷新二級緩存-->
<update id="updateByPrimaryKey" parameterType="com.demo.mybatis.pojo.User" flushCache="true">
update user
set name = #{name,jdbcType=VARCHAR},
age = #{age,jdbcType=INTEGER}
where id = #{id,jdbcType=INTEGER}
</update>
<!--可以通過設置useCache來規定這個sql是否開啟緩存,ture是開啟,false是關閉-->
<select id="selectByPrimaryKey" parameterType="java.lang.Integer" resultMap="BaseResultMap" useCache="true" >
select
<include refid="Base_Column_List" />
from user
where id = #{id,jdbcType=INTEGER}
</select>
</mapper>
其中僅僅添加下面這個也可以
<cache/>
如果我們配置了二級緩存就意味著:
映射語句文件中的所有select語句將會被緩存。
映射語句文件中的所欲insert、update和delete語句會刷新緩存。
緩存會使用默認的Least Recently Used(LRU,最近最少使用的)算法來收回。
根據時間表,比如No Flush Interval,(CNFI沒有刷新間隔),緩存不會以任何時間順序來刷新。
緩存會存儲列表集合或對象(無論查詢方法返回什么)的1024個引用。
緩存會被視為是read/write(可讀/可寫)的緩存,意味著對象檢索不是共享的,而且可以安全的被調用者修改,不干擾其他調用者或線程所做的潛在修改。
User.java
public class User implements Serializable {
private Integer id;
private String name;
private Integer age;
private static final long serialVersionUID = 1L;
...
set/get
...
}
UserMapper.java
User selectByPrimaryKey(Integer id);
測試方法
@Test
public void test03() throws IOException {
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//第一次
User user = userMapper.selectByPrimaryKey(1);
System.out.println("user1 => " + user.toString());
//第二次
User user2 = userMapper.selectByPrimaryKey(1);
System.out.println("user2 => " + user2.toString());
//session提交
sqlSession.commit();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
//第三次
User user3 = userMapper2.selectByPrimaryKey(1);
System.out.println("user3 => " + user3.toString());
//第四次
User user4 = userMapper2.selectByPrimaryKey(1);
System.out.println("user4 => " + user4.toString());
sqlSession2.commit();
}
來看下結果
DEBUG 2019-01-30 00:01:29791 Opening JDBC Connection
DEBUG 2019-01-30 00:01:34688 Created connection 1121453612.
DEBUG 2019-01-30 00:01:34689 Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@42d8062c]
DEBUG 2019-01-30 00:01:34691 ==> Preparing: select id, name, age from user where id = ?
DEBUG 2019-01-30 00:01:34737 ==> Parameters: 1(Integer)
DEBUG 2019-01-30 00:01:34757 <== Total: 1
user1 => User{id=1, name='ayang', age=18}
DEBUG 2019-01-30 00:01:34757 Cache Hit Ratio [com.demo.mybatis.mapper.UserMapper]: 0.0
user2 => User{id=1, name='ayang', age=18}
DEBUG 2019-01-30 00:01:34818 Cache Hit Ratio [com.demo.mybatis.mapper.UserMapper]: 0.3333333333333333
user3 => User{id=1, name='ayang', age=18}
DEBUG 2019-01-30 00:01:34819 Cache Hit Ratio [com.demo.mybatis.mapper.UserMapper]: 0.5
user4 => User{id=1, name='ayang', age=18}
可以看到第一次和第二次走的是一級緩存,第三次和第四次走的是二級緩存。
總結:
一級緩存是自動開啟的,sqlSession級別的緩存,查詢結果存放在BaseExecutor中的localCache中。
如果第一次做完查詢,接著做一次update | insert | delete | commit | rollback操作,則會清除緩存,第二次查詢則繼續走數據庫。
對于一級緩存不同的sqlSession之間的緩存是互相不影響的。
二級緩存是手動開啟的,作用域為sessionfactory,也可以說MapperStatement級緩存,也就是一個namespace(mapper.xml)就會有一個緩存,不同的sqlSession之間的緩存是共享的。
因為二級緩存的數據不一定都是存儲到內存中,它的存儲介質多種多樣,實現二級緩存的時候,MyBatis要求返回的POJO必須是可序列化的,也就是要求實現Serializable接口,如果存儲在內存中的話,實測不序列化也可以的。
一般為了避免出現臟數據,所以我們可以在每一次的insert | update | delete操作后都進行緩存刷新,也就是在Statement配置中配置flushCache屬性,如下:
<!--刷新二級緩存 flushCache="true"-->
<update id="updateByPrimaryKey" parameterType="com.demo.mybatis.pojo.User" flushCache="true">
update user
set name = #{name,jdbcType=VARCHAR},
age = #{age,jdbcType=INTEGER}
where id = #{id,jdbcType=INTEGER}
</update>