MyBatis原理系列(一)-手把手帶你閱讀MyBatis源碼
MyBatis原理系列(二)-手把手帶你了解MyBatis的啟動(dòng)流程
MyBatis原理系列(三)-手把手帶你了解SqlSession,SqlSessionFactory,SqlSessionFactoryBuilder的關(guān)系
MyBatis原理系列(四)-手把手帶你了解MyBatis的Executor執(zhí)行器
MyBatis原理系列(五)-手把手帶你了解Statement浑侥、StatementHandler、MappedStatement間的關(guān)系
MyBatis原理系列(六)-手把手帶你了解BoundSql的創(chuàng)建過(guò)程
MyBatis原理系列(七)-手把手帶你了解如何自定義插件
MyBatis原理系列(八)-手把手帶你了解一級(jí)緩存和二級(jí)緩存
MyBatis原理系列(九)-手把手帶你了解MyBatis事務(wù)管理機(jī)制
緩存在硬件和軟件應(yīng)用廣泛,我們?cè)诖髮W(xué)學(xué)過(guò)計(jì)算機(jī)與操作系統(tǒng)中接觸過(guò)高速緩存,閃存等会宪。在工作中,我們也接觸過(guò)一些緩存中間件巍沙,比如Redis,MemCache削咆。MyBatis作為一款優(yōu)秀的ORM框架,也提供了緩存的功能蠢笋,減少訪問(wèn)數(shù)據(jù)庫(kù)的次數(shù)拨齐,從而提高性能。本文將和大家介紹MyBatis的實(shí)現(xiàn)和原理昨寞。
1. 初識(shí)緩存
MyBatis提供的緩存功能包含一級(jí)緩存和二級(jí)緩存瞻惋,都是默認(rèn)開(kāi)啟的,它們的作用范圍也是不同的援岩。MyBatis的緩存是基于cache接口的蹂匹。cache接口的繼承關(guān)系如下
cache作為頂層接口,定義了緩存的基本操作庶近,比如設(shè)置緩存,獲取緩存的方法。
public interface Cache {
/**
* 唯一標(biāo)示緩存
* @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);
/**
* 清空緩存實(shí)例
*/
void clear();
/**
* 緩存中元素的數(shù)量
* @return
*/
int getSize();
/**
* 讀寫(xiě)鎖
* @return
*/
default ReadWriteLock getReadWriteLock() {
return null;
}
}
PerpetualCache 是cache的默認(rèn)實(shí)現(xiàn)钞螟,也是最簡(jiǎn)單的實(shí)現(xiàn)熔任,它以HashMap作為緩存容器,存儲(chǔ)緩存。其它類(lèi)型的緩存是對(duì)PerpetualCache的包裝交洗。
public class PerpetualCache implements Cache {
private final String id;
// 以map存儲(chǔ)緩存
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. 一級(jí)緩存
2.1 一級(jí)緩存開(kāi)啟
MyBatis一級(jí)緩存是默認(rèn)開(kāi)啟的蜜笤,并且它的作用范圍是SqlSession級(jí)別的暖混。我么知道SqlSession是頂層的接口泪勒,最終的數(shù)據(jù)庫(kù)操作都是交由給執(zhí)行器進(jìn)行操作的税产。了解前面的Executor的同學(xué)可知,緩存就是在執(zhí)行Executor中進(jìn)行維護(hù)的为居,其中l(wèi)ocalCache成員變量就是一級(jí)緩存對(duì)象,其類(lèi)型就是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;
}
}
一級(jí)緩存是默認(rèn)開(kāi)啟的削罩,Configuration的成員變量localCacheScope的默認(rèn)就是Sesssion級(jí)別的。
// Configuration類(lèi)
protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;
如果要關(guān)閉伴鳖,我們可以在mybatis-config.xml中的settings標(biāo)簽中將這個(gè)配置設(shè)置成Statement類(lèi)型的
<setting name="localCacheScope" value="STATEMENT"/>
如果某個(gè)select標(biāo)簽查詢不需要緩存宛徊,在select標(biāo)簽加上flushCache="true"
也可以設(shè)置單個(gè)查詢關(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 一級(jí)緩存存取
緩存在查詢中才會(huì)用到定硝,例如我們用同一個(gè)sql語(yǔ)句反復(fù)去查詢數(shù)據(jù)庫(kù)坝疼,并且在此期間沒(méi)有進(jìn)行過(guò)數(shù)據(jù)修改操作膀钠,預(yù)期是返回相同的結(jié)果誉结。如果沒(méi)有緩存,我們將每次都要訪問(wèn)數(shù)據(jù)庫(kù)返回結(jié)果雌澄,這個(gè)過(guò)程無(wú)疑是浪費(fèi)資源和消耗性能的屎媳。因此我們可以將第一次查詢的結(jié)果緩存在內(nèi)存中湃崩,第二次用相同的sql語(yǔ)句查詢的時(shí)候薄扁,先去緩存中查詢面哥,如果命中則直接返回县匠,否則去數(shù)據(jù)庫(kù)查詢并放到緩存中返回顶瞳。我們接下來(lái)看看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標(biāo)簽是否配置了flushCache=true
if (queryStack == 0 && ms.isFlushCacheRequired()) {
// 清除一級(jí)緩存
clearLocalCache();
}
List<E> list;
try {
queryStack++;
// 查詢一級(jí)緩存
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
// 處理緩存的結(jié)果
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 緩存中沒(méi)有則查詢數(shù)據(jù)庫(kù)
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)閉了一級(jí)緩存,查詢完后清除一級(jí)緩存
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
第一次查詢肯定從緩存中查詢不到東西慨菱,于是走向了queryFromDatabase分支焰络,這個(gè)方法就直接從數(shù)據(jù)庫(kù)中去查詢
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
// 添加占位符,標(biāo)示正在執(zhí)行
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
// 調(diào)用子類(lèi)的查詢方法獲取結(jié)果
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
// 將查詢結(jié)果放到緩存中
localCache.putObject(key, list);
// 如果是存儲(chǔ)過(guò)程則需要處理輸出參數(shù)
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
注意這個(gè)緩存真的是查詢sql完全一樣符喝,這個(gè)一樣還包括參數(shù)的一致闪彼,才會(huì)從緩存中獲取到結(jié)果,那么如何判斷兩個(gè)查詢sql是否一樣呢。createCacheKey就幫忙解答了這個(gè)疑惑畏腕,它會(huì)給每個(gè)sql都生成一個(gè)key缴川,如果兩個(gè)生成的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 一級(jí)緩存清除
在執(zhí)行update,commit沟娱,或者rollback操作的時(shí)候都會(huì)進(jìn)行清除緩存操作,所有的緩存都將失效恋日。
@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.");
}
// 清除一級(jí)緩存
clearLocalCache();
return doUpdate(ms, parameter);
}
3. 二級(jí)緩存
一級(jí)緩存的作用范圍是SqlSession級(jí)別的,但是SqlSession是單線程的嘹狞,不同線程間的操作會(huì)有一些臟數(shù)據(jù)的問(wèn)題谚鄙。二級(jí)緩存的范圍更大,是Mapper級(jí)別的緩存刁绒,因此不同sqlSession間可以共享緩存。
3.1 二級(jí)緩存開(kāi)啟
- 開(kāi)啟二級(jí)緩存需要配置
cacheEnabled
為true烤黍,這個(gè)屬性默認(rèn)為true知市。
<setting name="cacheEnabled" value="true"/>
- 在需要進(jìn)行開(kāi)啟二級(jí)緩存的mapper中新增cache配置,cache配置有很多屬性速蕊。
type : 緩存實(shí)現(xiàn)類(lèi)嫂丙,默認(rèn)是PerpetualCache,也可以是第三方緩存的實(shí)現(xiàn)
size:最多緩存對(duì)象的個(gè)數(shù)
eviction:緩存回收策略规哲,默認(rèn)是LRU
LRU:最近最少使用策略跟啤,回收最長(zhǎng)時(shí)間不被使用的緩存
FIFO:先進(jìn)先出策略唉锌,回收最新進(jìn)入的緩存
SOFT - 軟引用:移除基于垃圾回收器狀態(tài)和軟引用規(guī)則的對(duì)象
WEAK - 弱引用:更積極地移除基于垃圾收集器狀態(tài)和弱引用規(guī)則的對(duì)象flushInterval:緩存刷新的間隔時(shí)間隅肥,默認(rèn)是不刷新的
readOnly : 是否只讀,true 只會(huì)進(jìn)行讀取操作袄简,修改操作交由用戶處理
false 可以進(jìn)行讀取操作腥放,也可以進(jìn)行修改操作
<cache type="org.apache.ibatis.cache.impl.PerpetualCache"
size="1024"
eviction="LRU"
flushInterval="120000"
readOnly="false"/>
- 也可以對(duì)單個(gè)Statement標(biāo)簽進(jìn)行關(guān)閉和開(kāi)啟操作,通過(guò)配置
useCache="true"
來(lái)開(kāi)啟緩存
<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 二級(jí)緩存存取
二級(jí)緩存是Mapper級(jí)別的緩存绿语,因此SqlSession是不可以管理的秃症,我們?cè)侔涯抗廪D(zhuǎn)向Executor,Executor在介紹的時(shí)候涉及到了CachingExecutor吕粹,在Configuration創(chuàng)建Executor的時(shí)候种柑,如果開(kāi)啟了二級(jí)緩存,就使用到了CachingExecutor進(jìn)行了包裝匹耕。
// 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);
}
// 是否開(kāi)啟了二級(jí)緩存
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
// 創(chuàng)建插件對(duì)象
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
CachingExecutor 中只有兩個(gè)成員變量聚请,其中一個(gè)就是TransactionalCacheManager用來(lái)管理緩存。
// 1. 委托執(zhí)行器泌神,也就是被包裝的三種執(zhí)行器的中的一種
private final Executor delegate;
// 2. 緩存管理類(lèi)良漱,用來(lái)管理TransactionalCache
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
TransactionalCacheManager 結(jié)構(gòu)也比較簡(jiǎn)單舞虱,內(nèi)部也維護(hù)著一個(gè)HashMap緩存,其中TransactionalCache實(shí)現(xiàn)了Cache接口母市。
public class TransactionalCacheManager {
// 緩存矾兜,TransactionalCache實(shí)現(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);
}
}
二級(jí)緩存的的存取過(guò)程是怎么樣的呢,我們可以看看CachingExecutor的query方法患久。如果Statement標(biāo)簽配置了開(kāi)啟緩存椅寺,則從緩存中去取,否則執(zhí)行執(zhí)行一級(jí)緩存的查詢邏輯蒋失。如果開(kāi)啟了緩存返帕,則先從二級(jí)緩存中查找,如果命中直接返回篙挽,否則執(zhí)行一級(jí)緩存的邏輯荆萤。因此當(dāng)二級(jí)緩存開(kāi)啟時(shí),優(yōu)先從二級(jí)緩存中查找铣卡,再去從一級(jí)緩存中查找链韭,最后從數(shù)據(jù)庫(kù)查找。
// CachingExecutor
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
// 獲取二級(jí)緩存配置標(biāo)簽
Cache cache = ms.getCache();
if (cache != null) {
// select標(biāo)簽是否配置了flushCache屬性
flushCacheIfRequired(ms);
// 如果select標(biāo)簽配置了useCache屬性
if (ms.isUseCache() && resultHandler == null) {
// 二級(jí)緩存不能緩存輸出類(lèi)型的參數(shù)
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
// 獲取二級(jí)緩存
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
// 如果二級(jí)緩存為空煮落,則再去查詢一級(jí)緩存敞峭,如果一級(jí)緩存也沒(méi)命中,則查詢數(shù)據(jù)庫(kù)放到緩存中
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
// 二級(jí)緩存存儲(chǔ)時(shí)先保存在臨時(shí)屬性中蝉仇,等事務(wù)提交再保存到真實(shí)的二級(jí)緩存
// 緩存在一個(gè)中間變量
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
// 沒(méi)開(kāi)啟緩存
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
3.3 二級(jí)緩存清除
清空緩存也是在執(zhí)行更新操作的時(shí)候進(jìn)行刪除緩存
@Override
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
// 清空緩存
flushCacheIfRequired(ms);
// 調(diào)用實(shí)際執(zhí)行器的update方法
return delegate.update(ms, parameterObject);
}
4. 例子
接下來(lái)我們將以兩個(gè)例子來(lái)更加清晰的介紹下一級(jí)緩存和二級(jí)緩存
4.1 一級(jí)緩存
一級(jí)緩存是SqlSession級(jí)別的緩存旋讹,如果用同一個(gè)sql執(zhí)行兩次相同的sql,第一次會(huì)執(zhí)行查詢打印sql轿衔,第二次則是直接從緩存中去獲取沉迹,不會(huì)打印sql,從日志可以看出來(lái)只打印了一次sql害驹,說(shuō)明第二次是從緩存中獲取的胚股。
先將二級(jí)緩存關(guān)閉
<setting name="cacheEnabled" value="false"/>
然后執(zhí)行兩次相同的語(yǔ)句
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,說(shuō)明第二次是從緩存中獲取的
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.
因?yàn)槭荢qlSession級(jí)別的裙秋,如果不同的SqlSession級(jí)別的執(zhí)行相同的sql琅拌,應(yīng)該互不影響,應(yīng)該會(huì)打印兩次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);
// 開(kāi)啟新的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进宝,證明了一級(jí)緩存是SqlSession的級(jí)別的,不同的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 二級(jí)緩存
先開(kāi)啟二級(jí)緩存
<setting name="cacheEnabled" value="true"/>
然后對(duì)應(yīng)的mapper中開(kāi)啟緩存
<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,說(shuō)明緩存沒(méi)生效未玻,配置都配置正確了灾而,會(huì)有其它原因嗎
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
// 二級(jí)緩存存儲(chǔ)時(shí)先保存在臨時(shí)屬性中扳剿,等事務(wù)提交再保存到真實(shí)的二級(jí)緩存
tcm.putObject(cache, key, list); // issue #578 and #116
再看看CachingExecutor的commit方法旁趟,在commit的時(shí)候才會(huì)將緩存放到真正的緩存中,這樣做的目的就是為了防止不通SqlSession間的臟讀庇绽,一個(gè)SqlSession讀取了另一個(gè)SqlSession還未提交的數(shù)據(jù)锡搜。
@Override
public void commit(boolean required) throws SQLException {
delegate.commit(required);
tcm.commit();
}
接下來(lái)修改上述代碼為如下
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();
// 開(kāi)啟新的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ù)提交才會(huì)將查詢結(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 中包含一級(jí)緩存和二級(jí)緩存,一級(jí)緩存的作用范圍是SqlSession級(jí)別的辟狈,二級(jí)緩存是Mapper級(jí)別的肠缔。
- MyBatis 中的一級(jí)緩存和二級(jí)緩存都是默認(rèn)開(kāi)啟的,不過(guò)二級(jí)緩存還要額外在mapper和statement中配置緩存屬性
- 一級(jí)緩存和二級(jí)緩存適用于讀多寫(xiě)少的場(chǎng)景哼转,如果頻繁的更新數(shù)據(jù)桩砰,將降低查詢性能。