MyBatis原理系列(一)-手把手帶你閱讀MyBatis源碼
MyBatis原理系列(二)-手把手帶你了解MyBatis的啟動流程
MyBatis原理系列(三)-手把手帶你了解SqlSession,SqlSessionFactory,SqlSessionFactoryBuilder的關(guān)系
MyBatis原理系列(四)-手把手帶你了解MyBatis的Executor執(zhí)行器
MyBatis原理系列(五)-手把手帶你了解Statement、StatementHandler、MappedStatement間的關(guān)系
MyBatis原理系列(六)-手把手帶你了解BoundSql的創(chuàng)建過程
MyBatis原理系列(七)-手把手帶你了解如何自定義插件
MyBatis原理系列(八)-手把手帶你了解一級緩存和二級緩存
MyBatis原理系列(九)-手把手帶你了解MyBatis事務(wù)管理機制
緩存在硬件和軟件應(yīng)用廣泛,我們在大學(xué)學(xué)過計算機與操作系統(tǒng)中接觸過高速緩存,閃存等。在工作中,我們也接觸過一些緩存中間件,比如Redis,MemCache。MyBatis作為一款優(yōu)秀的ORM框架,也提供了緩存的功能,減少訪問數(shù)據(jù)庫的次數(shù),從而提高性能。本文將和大家介紹MyBatis的實現(xiàn)和原理。
1. 初識緩存
MyBatis提供的緩存功能包含一級緩存和二級緩存,都是默認開啟的,它們的作用范圍也是不同的。MyBatis的緩存是基于cache接口的。cache接口的繼承關(guān)系如下

cache作為頂層接口,定義了緩存的基本操作,比如設(shè)置緩存,獲取緩存的方法。
public interface Cache {
/**
* 唯一標示緩存
* @return
*/
String getId();
/**
* 以key value形式設(shè)置緩存
* @param key
* @param value
*/
void putObject(Object key, Object value);
/**
* 獲取緩存
* @param key
* @return
*/
Object getObject(Object key);
/**
* 刪除緩存
*/
Object removeObject(Object key);
/**
* 清空緩存實例
*/
void clear();
/**
* 緩存中元素的數(shù)量
* @return
*/
int getSize();
/**
* 讀寫鎖
* @return
*/
default ReadWriteLock getReadWriteLock() {
return null;
}
}
PerpetualCache 是cache的默認實現(xiàn),也是最簡單的實現(xiàn),它以HashMap作為緩存容器,存儲緩存。其它類型的緩存是對PerpetualCache的包裝。
public class PerpetualCache implements Cache {
private final String id;
// 以map存儲緩存
private final Map<Object, Object> cache = new HashMap<>();
public PerpetualCache(String id) {
this.id = id;
}
@Override
public String getId() {
return id;
}
@Override
public int getSize() {
return cache.size();
}
@Override
public void putObject(Object key, Object value) {
cache.put(key, value);
}
@Override
public Object getObject(Object key) {
return cache.get(key);
}
@Override
public Object removeObject(Object key) {
return cache.remove(key);
}
@Override
public void clear() {
cache.clear();
}
@Override
public boolean equals(Object o) {
if (getId() == null) {
throw new CacheException("Cache instances require an ID.");
}
if (this == o) {
return true;
}
if (!(o instanceof Cache)) {
return false;
}
Cache otherCache = (Cache) o;
return getId().equals(otherCache.getId());
}
@Override
public int hashCode() {
if (getId() == null) {
throw new CacheException("Cache instances require an ID.");
}
return getId().hashCode();
}
}
2. 一級緩存
2.1 一級緩存開啟
MyBatis一級緩存是默認開啟的,并且它的作用范圍是SqlSession級別的。我么知道SqlSession是頂層的接口,最終的數(shù)據(jù)庫操作都是交由給執(zhí)行器進行操作的。了解前面的Executor的同學(xué)可知,緩存就是在執(zhí)行Executor中進行維護的,其中l(wèi)ocalCache成員變量就是一級緩存對象,其類型就是PerpetualCache。
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<>();
this.localCache = new PerpetualCache("LocalCache");
this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
this.closed = false;
this.configuration = configuration;
this.wrapper = this;
}
}
一級緩存是默認開啟的,Configuration的成員變量localCacheScope的默認就是Sesssion級別的。
// Configuration類
protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;
如果要關(guān)閉,我們可以在mybatis-config.xml中的settings標簽中將這個配置設(shè)置成Statement類型的
<setting name="localCacheScope" value="STATEMENT"/>
如果某個select標簽查詢不需要緩存,在select標簽加上flushCache="true"也可以設(shè)置單個查詢關(guān)閉緩存
<select id="selectByPrimaryKey" parameterType="java.lang.Long"
resultMap="BaseResultMap" flushCache="true">
select
<include refid="Base_Column_List" />
from t_test_user
where id = #{id,jdbcType=BIGINT}
</select>
2.1 一級緩存存取
緩存在查詢中才會用到,例如我們用同一個sql語句反復(fù)去查詢數(shù)據(jù)庫,并且在此期間沒有進行過數(shù)據(jù)修改操作,預(yù)期是返回相同的結(jié)果。如果沒有緩存,我們將每次都要訪問數(shù)據(jù)庫返回結(jié)果,這個過程無疑是浪費資源和消耗性能的。因此我們可以將第一次查詢的結(jié)果緩存在內(nèi)存中,第二次用相同的sql語句查詢的時候,先去緩存中查詢,如果命中則直接返回,否則去數(shù)據(jù)庫查詢并放到緩存中返回。我們接下來看看BaseExecutor的query方法是怎么做的吧。
@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());
// Executor是否關(guān)閉
if (closed) {
throw new ExecutorException("Executor was closed.");
}
// select標簽是否配置了flushCache=true
if (queryStack == 0 && ms.isFlushCacheRequired()) {
// 清除一級緩存
clearLocalCache();
}
List<E> list;
try {
queryStack++;
// 查詢一級緩存
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
// 處理緩存的結(jié)果
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 緩存中沒有則查詢數(shù)據(jù)庫
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
// 如果關(guān)閉了一級緩存,查詢完后清除一級緩存
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
第一次查詢肯定從緩存中查詢不到東西,于是走向了queryFromDatabase分支,這個方法就直接從數(shù)據(jù)庫中去查詢
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
// 添加占位符,標示正在執(zhí)行
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
// 調(diào)用子類的查詢方法獲取結(jié)果
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
// 將查詢結(jié)果放到緩存中
localCache.putObject(key, list);
// 如果是存儲過程則需要處理輸出參數(shù)
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
注意這個緩存真的是查詢sql完全一樣,這個一樣還包括參數(shù)的一致,才會從緩存中獲取到結(jié)果,那么如何判斷兩個查詢sql是否一樣呢。createCacheKey就幫忙解答了這個疑惑,它會給每個sql都生成一個key,如果兩個生成的key一致,那就表明不管是sql還是參數(shù)都是一致的。
@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
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();
// mimic DefaultParameterHandler logic
for (ParameterMapping parameterMapping : parameterMappings) {
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
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 = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
cacheKey.update(value);
}
}
if (configuration.getEnvironment() != null) {
// issue #176
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}
2.3 一級緩存清除
在執(zhí)行update,commit,或者rollback操作的時候都會進行清除緩存操作,所有的緩存都將失效。
@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.");
}
// 清除一級緩存
clearLocalCache();
return doUpdate(ms, parameter);
}
3. 二級緩存
一級緩存的作用范圍是SqlSession級別的,但是SqlSession是單線程的,不同線程間的操作會有一些臟數(shù)據(jù)的問題。二級緩存的范圍更大,是Mapper級別的緩存,因此不同sqlSession間可以共享緩存。
3.1 二級緩存開啟
- 開啟二級緩存需要配置
cacheEnabled為true,這個屬性默認為true。
<setting name="cacheEnabled" value="true"/>
- 在需要進行開啟二級緩存的mapper中新增cache配置,cache配置有很多屬性。
type : 緩存實現(xiàn)類,默認是PerpetualCache,也可以是第三方緩存的實現(xiàn)
size:最多緩存對象的個數(shù)
eviction:緩存回收策略,默認是LRU
LRU:最近最少使用策略,回收最長時間不被使用的緩存
FIFO:先進先出策略,回收最新進入的緩存
SOFT - 軟引用:移除基于垃圾回收器狀態(tài)和軟引用規(guī)則的對象
WEAK - 弱引用:更積極地移除基于垃圾收集器狀態(tài)和弱引用規(guī)則的對象flushInterval:緩存刷新的間隔時間,默認是不刷新的
readOnly : 是否只讀,true 只會進行讀取操作,修改操作交由用戶處理
false 可以進行讀取操作,也可以進行修改操作
<cache type="org.apache.ibatis.cache.impl.PerpetualCache"
size="1024"
eviction="LRU"
flushInterval="120000"
readOnly="false"/>
- 也可以對單個Statement標簽進行關(guān)閉和開啟操作,通過配置
useCache="true"來開啟緩存
<select id="selectByPrimaryKey" parameterType="java.lang.Long"
resultMap="BaseResultMap" useCache="true">
select
<include refid="Base_Column_List" />
from t_test_user
where id = #{id,jdbcType=BIGINT}
</select>
3.2 二級緩存存取
二級緩存是Mapper級別的緩存,因此SqlSession是不可以管理的,我們再把目光轉(zhuǎn)向Executor,Executor在介紹的時候涉及到了CachingExecutor,在Configuration創(chuàng)建Executor的時候,如果開啟了二級緩存,就使用到了CachingExecutor進行了包裝。
// Configuration
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);
}
// 創(chuàng)建插件對象
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
CachingExecutor 中只有兩個成員變量,其中一個就是TransactionalCacheManager用來管理緩存。
// 1. 委托執(zhí)行器,也就是被包裝的三種執(zhí)行器的中的一種
private final Executor delegate;
// 2. 緩存管理類,用來管理TransactionalCache
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
TransactionalCacheManager 結(jié)構(gòu)也比較簡單,內(nèi)部也維護著一個HashMap緩存,其中TransactionalCache實現(xiàn)了Cache接口。
public class TransactionalCacheManager {
// 緩存,TransactionalCache實現(xiàn)了Cache接口
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
public void clear(Cache cache) {
getTransactionalCache(cache).clear();
}
public Object getObject(Cache cache, CacheKey key) {
return getTransactionalCache(cache).getObject(key);
}
public void putObject(Cache cache, CacheKey key, Object value) {
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) {
return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
}
}
二級緩存的的存取過程是怎么樣的呢,我們可以看看CachingExecutor的query方法。如果Statement標簽配置了開啟緩存,則從緩存中去取,否則執(zhí)行執(zhí)行一級緩存的查詢邏輯。如果開啟了緩存,則先從二級緩存中查找,如果命中直接返回,否則執(zhí)行一級緩存的邏輯。因此當二級緩存開啟時,優(yōu)先從二級緩存中查找,再去從一級緩存中查找,最后從數(shù)據(jù)庫查找。
// CachingExecutor
@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) {
// select標簽是否配置了flushCache屬性
flushCacheIfRequired(ms);
// 如果select標簽配置了useCache屬性
if (ms.isUseCache() && resultHandler == null) {
// 二級緩存不能緩存輸出類型的參數(shù)
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
// 獲取二級緩存
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
// 如果二級緩存為空,則再去查詢一級緩存,如果一級緩存也沒命中,則查詢數(shù)據(jù)庫放到緩存中
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
// 二級緩存存儲時先保存在臨時屬性中,等事務(wù)提交再保存到真實的二級緩存
// 緩存在一個中間變量
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
// 沒開啟緩存
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
3.3 二級緩存清除
清空緩存也是在執(zhí)行更新操作的時候進行刪除緩存
@Override
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
// 清空緩存
flushCacheIfRequired(ms);
// 調(diào)用實際執(zhí)行器的update方法
return delegate.update(ms, parameterObject);
}
4. 例子
接下來我們將以兩個例子來更加清晰的介紹下一級緩存和二級緩存
4.1 一級緩存
一級緩存是SqlSession級別的緩存,如果用同一個sql執(zhí)行兩次相同的sql,第一次會執(zhí)行查詢打印sql,第二次則是直接從緩存中去獲取,不會打印sql,從日志可以看出來只打印了一次sql,說明第二次是從緩存中獲取的。
先將二級緩存關(guān)閉
<setting name="cacheEnabled" value="false"/>
然后執(zhí)行兩次相同的語句
public static void main(String[] args) {
try {
// 1. 讀取配置
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
// 2. 創(chuàng)建SqlSessionFactory工廠
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 3. 獲取sqlSession
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.SIMPLE);
// 4. 獲取Mapper
TTestUserMapper userMapper = sqlSession.getMapper(TTestUserMapper.class);
// 5. 執(zhí)行接口方法
TTestUser user = userMapper.selectByPrimaryKey(1000L);
TTestUser user1 = userMapper.selectByPrimaryKey(1000L);
// 6. 提交事物
sqlSession.commit();
// 7. 關(guān)閉資源
sqlSession.close();
inputStream.close();
} catch (Exception e){
log.error(e.getMessage(), e);
}
}
最后打印了一次sql,說明第二次是從緩存中獲取的
16:37:33.088 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Opening JDBC Connection
16:37:35.027 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Created connection 1995250556.
16:37:35.028 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@76ed1b7c]
16:37:35.050 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPrimaryKey - ==> Preparing: select id, member_id, real_name, nickname, date_create, date_update, deleted from t_test_user where id = ?
16:37:35.108 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPrimaryKey - ==> Parameters: 1000(Long)
16:37:35.171 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPrimaryKey - <== Total: 1
16:37:35.174 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@76ed1b7c]
16:37:35.191 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@76ed1b7c]
16:37:35.191 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Returned connection 1995250556 to pool.
因為是SqlSession級別的,如果不同的SqlSession級別的執(zhí)行相同的sql,應(yīng)該互不影響,應(yīng)該會打印兩次sql,我們將上面的代碼稍微修改下
public static void main(String[] args) {
try {
// 1. 讀取配置
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
// 2. 創(chuàng)建SqlSessionFactory工廠
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 3. 獲取sqlSession
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.SIMPLE);
// 4. 獲取Mapper
TTestUserMapper userMapper = sqlSession.getMapper(TTestUserMapper.class);
// 5. 執(zhí)行接口方法
TTestUser user = userMapper.selectByPrimaryKey(1000L);
// 開啟新的sqlSession
SqlSession sqlSession2 = sqlSessionFactory.openSession();
TTestUserMapper userMapper2 = sqlSession2.getMapper(TTestUserMapper.class);
TTestUser user2 = userMapper2.selectByPrimaryKey(1000L);
// 6. 提交事物
sqlSession.commit();
// 7. 關(guān)閉資源
sqlSession.close();
inputStream.close();
} catch (Exception e){
log.error(e.getMessage(), e);
}
}
打印了兩次sql,證明了一級緩存是SqlSession的級別的,不同的SqlSession間不能共享緩存。
16:44:06.871 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Opening JDBC Connection
16:44:08.297 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Created connection 34073107.
16:44:08.297 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@207ea13]
16:44:08.316 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPrimaryKey - ==> Preparing: select id, member_id, real_name, nickname, date_create, date_update, deleted from t_test_user where id = ?
16:44:08.365 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPrimaryKey - ==> Parameters: 1000(Long)
16:44:08.447 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPrimaryKey - <== Total: 1
16:44:08.448 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Opening JDBC Connection
16:44:08.717 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Created connection 1527254842.
16:44:08.718 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@5b080f3a]
16:44:08.740 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPrimaryKey - ==> Preparing: select id, member_id, real_name, nickname, date_create, date_update, deleted from t_test_user where id = ?
16:44:08.741 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPrimaryKey - ==> Parameters: 1000(Long)
16:44:08.764 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPrimaryKey - <== Total: 1
16:44:08.764 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@207ea13]
16:44:08.788 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@207ea13]
16:44:08.789 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Returned connection 34073107 to pool.
4.1 二級緩存
先開啟二級緩存
<setting name="cacheEnabled" value="true"/>
然后對應(yīng)的mapper中開啟緩存
<select id="selectByPrimaryKey" parameterType="java.lang.Long"
resultMap="BaseResultMap" useCache="true">
select
<include refid="Base_Column_List" />
from t_test_user
where id = #{id,jdbcType=BIGINT}
</select>
<cache type="org.apache.ibatis.cache.impl.PerpetualCache"
size="1024"
eviction="LRU"
flushInterval="120000"
readOnly="false"/>
復(fù)用上面的代碼,我們看看不同SqlSession間是否能夠共享緩存。
發(fā)現(xiàn)還是打印了2次sql,說明緩存沒生效,配置都配置正確了,會有其它原因嗎
16:56:34.043 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Opening JDBC Connection
16:56:35.278 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Created connection 316335490.
16:56:35.279 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@12dae582]
16:56:35.292 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPrimaryKey - ==> Preparing: select id, member_id, real_name, nickname, date_create, date_update, deleted from t_test_user where id = ?
16:56:35.341 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPrimaryKey - ==> Parameters: 1000(Long)
16:56:35.386 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPrimaryKey - <== Total: 1
16:56:35.387 [main] DEBUG com.example.demo.dao.TTestUserMapper - Cache Hit Ratio [com.example.demo.dao.TTestUserMapper]: 0.0
16:56:35.387 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Opening JDBC Connection
16:56:35.544 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Created connection 375074687.
16:56:35.544 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@165b2f7f]
16:56:35.560 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPrimaryKey - ==> Preparing: select id, member_id, real_name, nickname, date_create, date_update, deleted from t_test_user where id = ?
16:56:35.560 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPrimaryKey - ==> Parameters: 1000(Long)
16:56:35.571 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPrimaryKey - <== Total: 1
16:56:35.583 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@12dae582]
16:56:35.602 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@12dae582]
16:56:35.602 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Returned connection 316335490 to pool.
再看看CachingExecutor中的query方法,有這一行代碼
// CachingExecutor
// 二級緩存存儲時先保存在臨時屬性中,等事務(wù)提交再保存到真實的二級緩存
tcm.putObject(cache, key, list); // issue #578 and #116
再看看CachingExecutor的commit方法,在commit的時候才會將緩存放到真正的緩存中,這樣做的目的就是為了防止不通SqlSession間的臟讀,一個SqlSession讀取了另一個SqlSession還未提交的數(shù)據(jù)。
@Override
public void commit(boolean required) throws SQLException {
delegate.commit(required);
tcm.commit();
}
接下來修改上述代碼為如下
public static void main(String[] args) {
try {
// 1. 讀取配置
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
// 2. 創(chuàng)建SqlSessionFactory工廠
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 3. 獲取sqlSession
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.SIMPLE);
// 4. 獲取Mapper
TTestUserMapper userMapper = sqlSession.getMapper(TTestUserMapper.class);
// 5. 執(zhí)行接口方法
TTestUser user = userMapper.selectByPrimaryKey(1000L);
sqlSession.commit();
// 開啟新的sqlSession
SqlSession sqlSession2 = sqlSessionFactory.openSession();
TTestUserMapper userMapper2 = sqlSession2.getMapper(TTestUserMapper.class);
TTestUser user2 = userMapper2.selectByPrimaryKey(1000L);
sqlSession2.commit();
// 7. 關(guān)閉資源
sqlSession.close();
sqlSession2.close();
inputStream.close();
} catch (Exception e){
log.error(e.getMessage(), e);
}
}
第一次查詢提交了事務(wù)后,第二次直接命中了緩存,從而印證了事務(wù)提交才會將查詢結(jié)果放到緩存中。
17:08:20.993 [main] DEBUG com.example.demo.dao.TTestUserMapper - Cache Hit Ratio [com.example.demo.dao.TTestUserMapper]: 0.0
17:08:21.011 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Opening JDBC Connection
17:08:22.568 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Created connection 316335490.
17:08:22.568 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@12dae582]
17:08:22.589 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPrimaryKey - ==> Preparing: select id, member_id, real_name, nickname, date_create, date_update, deleted from t_test_user where id = ?
17:08:22.643 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPrimaryKey - ==> Parameters: 1000(Long)
17:08:22.692 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPrimaryKey - <== Total: 1
17:08:22.706 [main] DEBUG com.example.demo.dao.TTestUserMapper - Cache Hit Ratio [com.example.demo.dao.TTestUserMapper]: 0.5
17:08:22.707 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@12dae582]
17:08:22.733 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@12dae582]
17:08:22.733 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Returned connection 316335490 to pool.
5. 總結(jié)
- MyBatis 中包含一級緩存和二級緩存,一級緩存的作用范圍是SqlSession級別的,二級緩存是Mapper級別的。
- MyBatis 中的一級緩存和二級緩存都是默認開啟的,不過二級緩存還要額外在mapper和statement中配置緩存屬性
- 一級緩存和二級緩存適用于讀多寫少的場景,如果頻繁的更新數(shù)據(jù),將降低查詢性能。