前言
緩存是一般的ORM框架都會提供的功能,目的就是提升查詢的效率,和Hibernate一樣,MyBatis也有一級緩存和二級緩存,並且我們還可以通過實現MyBatis的緩存接口來採用第三方緩存。
一級緩存
一級緩存也稱爲本地緩存,MyBatis的一級緩存是在會話級別(SqlSession),MyBatis的一級緩存是默認開啓的,我們不需要任何配置。
SqlSession其實在Mybatis中有一個默認的實現——DefaultSqlSession。
public class DefaultSqlSession implements SqlSession {
private final Configuration configuration;
private final Executor executor;
private final boolean autoCommit;
private boolean dirty;
private List<Cursor<?>> cursorList;
}
上述屬性是DefaultSqlSession的一些屬性,我們走到現在可以很明確的一點是,緩存應該是維護在Session級別,那麼針對DefaultSqlSession中的兩個屬性,哪一個和緩存有關呢?Configuration是一個全局配置屬性,緩存固然不會在裏面維護,因此最有可能的是在Executor屬性中進行維護,Executor也是一個接口,其也有一個默認的實現——BaseExecutor(常見的Executor有三種——SimpleExecutor,ReuseExecutor,BatchExecutor)
public abstract class BaseExecutor implements Executor {
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;
}
可以看到這裏有兩個PerpetualCache的屬性,這個就是我們的緩存。
在同一個會話裏,多次執行相同的SQL語句,會直接從內存取到緩存中的結果,不會再發送SQL到數據庫查詢,但是在不同的會話裏頭,即使執行的SQL一樣(也是通過同一個Mapper的同一個方法調用)也不能命中一級緩存,畢竟一級緩存只存在會話級別。
測試代碼:
@Slf4j
public class TestCache {
@Test
public void testFirstLevleCache() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session1 = sqlSessionFactory.openSession();
SqlSession session2 = sqlSessionFactory.openSession();
try{
BlogMapper blogMapper01 = session1.getMapper(BlogMapper.class);
BlogMapper blogMapper02 = session2.getMapper(BlogMapper.class);
Blog blog = blogMapper01.selectBlogById(1);
log.info("第一次查詢,通過BlogMapper01查詢:{}",blog);
log.info("第二次查詢,依舊通過blogMapper01查詢:{}",blogMapper01.selectBlogById(1));
log.info("第三次查詢,通過BlogMapper02查詢:{}",blogMapper02.selectBlogById(1));
}catch (Exception e){
log.info("出現異常,異常信息:{}",e);
}finally {
session1.close();
session2.close();
}
}
}
運行結果
ps:在測試一級緩存的時候,建議關閉二級緩存
需要說一下的是,如果數據被修改或者刪除了,一級緩存會失效
如下代碼
/**
* 測試一級緩存失效的情況
*/
@Test
public void testFirstLevelCacheInvalid() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = sqlSessionFactory.openSession();
try{
BlogMapper blogMapper = session.getMapper(BlogMapper.class);
log.info("第一次查詢,通過BlogMapper查詢:{}",blogMapper.selectBlogById(1));
Blog blog = new Blog();
blog.setBid(1);
blog.setName(new Date().toString());
blogMapper.updateByPrimaryKey(blog);
//update之後,更新操作
session.commit();
log.info("更新之後再次查詢,查詢記錄爲:{}",blogMapper.selectBlogById(1));
}catch (Exception e){
log.info("出現異常,異常信息:{}",e);
}finally {
session.close();
}
}
這段代碼會出現兩條SQL查詢語句。
ps:select標籤中有個flushCache的屬性,默認是false,也就是說查詢的時候是不會清空緩存的,如果有必要可以將這個屬性置爲true,查詢的時候也會更新緩存。
一級緩存的不足之處
不能跨會話共享,不同的會話之間對於相同的數據可能有不一樣的緩存。在有多個會話或者分佈式環境下,會存在髒數據的問題。如果要解決這個問題,就要用到二級緩存。
二級緩存
先來看看下面一段代碼
@Test
public void testFirstCacheDirtyRead() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session1 = sqlSessionFactory.openSession();
SqlSession session2 = sqlSessionFactory.openSession();
try {
BlogMapper mapper1 = session1.getMapper(BlogMapper.class);
log.info("第一次查詢,通過session01,結果爲:{}",mapper1.selectBlogById(1));
// 會話2更新了數據,會話2的一級緩存更新
Blog blog = new Blog();
blog.setBid(1);
blog.setName("會話二修改"+new Date());
BlogMapper mapper2 = session2.getMapper(BlogMapper.class);
mapper2.updateByPrimaryKey(blog);
session2.commit();
log.info("第二次查詢,還是通過session01,結果爲:{}",mapper1.selectBlogById(1));
log.info("通過會話二查詢的真實數據爲:{}",mapper2.selectBlogById(1));
} finally {
session1.close();
session2.close();
}
}
運行結果如下:
這種不就是所謂的髒讀麼?爲了解決這個問題,於是就有了二級緩存。二級緩存的作用域是命名空間級別(Mapper級別)的,同一個Mapper的同一個方法的調用就能共享我們的二級緩存。
我們知道,一級緩存是在DefaultSession的Executor屬性中維護了一個緩存對象,畢竟該緩存是會話級別的,但是針對二級緩存,是Mapper級別的,所以這個緩存結構是不是維護在DefaultSession作用域之外的一個對象中呢?而對應的SqlSession作用域之外的一個對象就是SqlSessionFactory了,但是根據單一職責原則SqlSessionFactory只是單純的產生SqlSession,而不會維護緩存。
要跨越會話共享,SqlSession本身和他裏面的BaseExecutor已經滿足不了要求了,這裏可能就需要擴展一下BaseExecutor的功能了,MyBatis底層通過裝飾器模式,擴展了BaseExecutor對象——CachingExecutor。
CachingExecutor對於查詢請求,會判斷二級緩存是否有緩存結果,如果有就直接返回,如果沒有則會讓被委託的執行器去執行,比如SimpleExecutor來執行查詢,之後再走一次一級緩存的流程。最後會把結果緩存起來返回給用戶。
開啓二級緩存
一級緩存是默認開啓的,開啓二級緩存需要增加幾個配置屬性。
1、全局配置中增加如下屬性,該配置MyBatis默認開啓其實可以不用設置
<setting name="cacheEnabled" value="true"/>
2、每個會話級別的Mapper中開啓緩存配置
在需要開啓二級緩存的Mapper.xml文件中加入表示開始二級緩存
<!-- 聲明這個namespace使用二級緩存 -->
<cache/>
關於cache的各種屬性實例(詳細參數參見MyBatis官方文檔)
<cache type="org.apache.ibatis.cache.impl.PerpetualCache"
size="1024"
eviction="LRU"
flushInterval="120000"
readOnly="false"/>
1、type——指定實現緩存的具體實現類
2、size——指定緩存的大小
3、eviction——指定緩存策略
4、flushInterval——緩存刷新間隔時長
5、readOnly——默認是true(安全),如果改爲false(可讀寫)對象必須實現序列化接口。
Mapper.xml配置了標籤之後,select方法會緩存,update,delete,insert等會刷新緩存。
如果某些查詢方法對數據實時性要求很高,可以單獨針對這個接口關閉二級緩存
<select id="selectBlogById" resultMap="BaseResultMap" useCache="false">
二級緩存測試實例
@Test
public void testSecondLevelCache() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session1 = sqlSessionFactory.openSession();
SqlSession session2 = sqlSessionFactory.openSession();
try {
BlogMapper mapper1 = session1.getMapper(BlogMapper.class);
log.info("會話一開始查詢,查詢結果爲:{}",mapper1.selectBlogById(1));
// 事務不提交的情況下,二級緩存並不會寫入
session1.commit();
BlogMapper mapper2 = session2.getMapper(BlogMapper.class);
log.info("會話二開始查詢,查詢結果爲:{}",mapper2.selectBlogById(1));
} finally {
session1.close();
}
}
上述代碼運行結果:
二級緩存的注意事項
一級緩存是默認開啓的,二級緩存是需要配置纔可以開啓,那麼就需要梳理二級緩存在什麼情況下才有必要去開啓
1、所有的刪除都會刷新二級緩存,導致二級緩存失效,所以查詢爲主的應用中可以使用二級緩存
2、如果多個namespace(可以理解一個mapper文件)中有針對於同一張表的操作,如果在一個namespace中刷新了緩存,另一個namespace中沒有刷新,就會出現髒讀的數據,所以建議一個mapper中只操作一張表。
緩存可以在多個namespace中共享,如果AMapper.xml文件中需要共享BMapper.xml文件中的緩存,可以使用來解決。但是多個mapper,只要有其中任意一個操作的修改等操作,都會刷新緩存,這個時候一定程度上二級緩存已經失去了意義了。
<cache-ref namespace = "com.learn.dao.mapper.BMapper"/>
3、可以採用第三方緩存中間件來實現二級緩存,參看以下實例
使用redis作爲MyBatis二級緩存實例:
先引入MyBatis-redis依賴
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-redis</artifactId>
<version>1.0.0-beta2</version>
</dependency>
引入redis配置文件
host=localhost
port=6379
connectionTimeout=5000
soTimeout=5000
database=0
運行上面的二級緩存實例代碼即可
可以看到指定的redis緩存中如下所示:
總結
本篇博客總結了MyBatis中緩存的問題。