Mybatis的緩存——一級緩存和源碼分析
什麼是緩存?
緩存就是存在內存中的數據,而內存讀取都是非常快的 ,通常我們會把更新變動不太頻繁且查詢頻繁的數據,在第一次從數據庫查詢出後,存放在緩存中,這樣就可以避免之後多次的與數據庫進行交互,從而提升響應速度。
mybatis 也提供了對緩存的支持,分為:
- 一級緩存
- 二級緩存
- 一級緩存:
每個sqlSeesion對象都有一個一級緩存,我們在操作數據庫時需要構造sqlSeesion對象,在對象中有一個HashMap用於存儲緩存數據。不同的sqlSession之間的緩存數據區域(HashMap)是互不影響的。 - 二級緩存:
二級緩存是mapper級別(或稱為namespace級別)的緩存,多個sqlSession去操作同一個Mapper的sql語句,多個SqlSession可以共用二級緩存,二級緩存是跨sqlSession的。
一級緩存
首先我們來開一級緩存,一級緩存是默認開啟的,所以我們可以很方便來體驗一下一級緩存。
測試一、
準備一張表,有兩個字段id和username
在測試類中:
public class TestCache {
private SqlSession sqlSession;
private UserMapper mapper;
@Before
public void before() throws IOException {
InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
SqlSessionFactory build = new SqlSessionFactoryBuilder().build(resourceAsStream);
sqlSession = build.openSession();
mapper = sqlSession.getMapper(UserMapper.class);
}
@Test
public void testFirst(){
//第一次查詢————首先去一級緩存中查詢
User user1 = mapper.findById(1);
System.out.println("======"+user1);
//第二次查詢
User user2 = mapper.findById(1);
System.out.println("======"+user2);
System.out.println(user1==user2);
}
}
我們用同一個sqlSession
分別根據id
來查詢用戶,id
都為1,之後再比較它們的地址值。來看一下結果:
23:16:25,818 DEBUG findById:159 - ==> Preparing: select * from user where id=?
23:16:25,862 DEBUG findById:159 - ==> Parameters: 1(Integer)
23:16:25,894 DEBUG findById:159 - <== Total: 1
======User{id=1, username='lucy'}
======User{id=1, username='lucy'}
true
我們發現只打印了一條SQL,同時它們的地址值一致。
說明第一次查詢,緩存中沒有,然後從數據庫中查詢——執行SQL,然後存入緩存,第二次查詢時發現緩存中有了,所以直接從緩存中取出,不再執行SQL了。
我們剛才提到,一級緩存的數據結構是一個hashmap,也就是說有key有value。
value就是我們查詢出的結果,key是由多個值組成的:
- statementid :namespace.id組成
- params:查詢時傳入的參數
- boundsql:mybatis底層的對象,它封裝着我們要執行的sql
- rowbounds:分頁對象
- …還有一些會在源碼分析中道明
測試二、
我們現在修改一下,我們在查詢第一次結果後,修改一下數據庫的值,然後再進行第二次查詢,我們來看一下查詢結果。id=1 的username為lucy
@Test
public void testFirst(){
//第一次查詢
User user1 = mapper.findById(1);
System.out.println("======"+user1);
//修改id為1的username
User updateUser = new User();
updateUser.setId(1);
updateUser.setUsername("李思");
mapper.updateUser(updateUser);
//手動提交事務
sqlSession.commit();
//第二次查詢
User user2 = mapper.findById(1);
System.out.println("======"+user2);
System.out.println(user1==user2);
}
在提交事務的地方打一個斷點,可以看到執行了兩條sql,一個是查詢id為1,一個是修改id為1的username
最終結果:
23:50:15,933 DEBUG findById:159 - ==> Preparing: select * from user where id=?
23:50:15,976 DEBUG findById:159 - ==> Parameters: 1(Integer)
23:50:16,002 DEBUG findById:159 - <== Total: 1
======User{id=1, username='lucy', roleList=null, orderList=null}
23:50:16,003 DEBUG updateUser:159 - ==> Preparing: update user set username=? where id =?
23:50:16,005 DEBUG updateUser:159 - ==> Parameters: 李思(String), 1(Integer)
23:50:16,016 DEBUG updateUser:159 - <== Updates: 1
23:53:18,316 DEBUG JdbcTransaction:70 - Committing JDBC Connection [com.mysql.jdbc.JDBC4Connection@421e361]
23:53:22,306 DEBUG findById:159 - ==> Preparing: select * from user where id=?
23:53:22,306 DEBUG findById:159 - ==> Parameters: 1(Integer)
23:53:22,307 DEBUG findById:159 - <== Total: 1
======User{id=1, username='李思', roleList=null, orderList=null}
我們看到,最終打印了3條sql,再進行修改後的第二次查詢也打印了。
說明在第二次查詢時在緩存中找不到所對應的key了。在進行修改操作時,會刷新緩存
我們也可以通過sqlSession.clearCache();
手動刷新一級緩存
總結:
- 一級緩存的數據結構時HashMap
- 不同的SqlSession的一級緩存互不影響
- 一級緩存的key是由多個值組成的,value就是其查詢結果
- 增刪改操作會刷新一級緩存
- 通過
sqlSession.clearCache()
手動刷新一級緩存
一級緩存源碼分析:
我們在分析一級緩存之前帶着一些疑問來讀代碼
-
一級緩存是什麼? 真的是上面說的HashMap嗎?
-
一級緩存什麼時候被創建?
-
一級緩存的工作流程是怎麼樣的?
1. 一級緩存到底是什麼?
之前說不同的SqlSession的一級緩存互不影響
,所以我從SqlSession這個類入手
可以看到,org.apache.ibatis.session.SqlSession
中有一個和緩存有關的方法——clearCache()
刷新緩存的方法,點進去,找到它的實現類DefaultSqlSession
@Override
public void clearCache() {
executor.clearLocalCache();
}
再次點進去executor.clearLocalCache()
,再次點進去並找到其實現類BaseExecutor
,
@Override
public void clearLocalCache() {
if (!closed) {
localCache.clear();
localOutputParameterCache.clear();
}
}
進入localCache.clear()
方法。進入到了org.apache.ibatis.cache.impl.PerpetualCache
類中
package org.apache.ibatis.cache.impl;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.CacheException;
/**
* @author Clinton Begin
*/
public class PerpetualCache implements Cache {
private final String id;
private Map<Object, Object> cache = new HashMap<Object, Object>();
public PerpetualCache(String id) {
this.id = id;
}
//省略部分...
@Override
public void clear() {
cache.clear();
}
//省略部分...
}
我們看到了PerpetualCache
類中有一個屬性 private Map<Object, Object> cache = new HashMap<Object, Object>()
,很明顯它是一個HashMap,我們所調用的.clear()
方法,實際上就是調用的Map的clear方法
得出結論:
一級緩存的數據結構確實是HashMap
2. 一級緩存什麼時候被創建?
我們進入到org.apache.ibatis.executor.Executor
中
看到一個方法CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql)
,見名思意是一個創建CacheKey的方法
找到它的實現類和方法org.apache.ibatis.executor.BaseExecuto.createCacheKey
我們分析一下創建CacheKey的這塊代碼:
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
//初始化CacheKey
CacheKey cacheKey = new CacheKey();
//存入statementId
cacheKey.update(ms.getId());
//分別存入分頁需要的Offset和Limit
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
//把從BoundSql中封裝的sql取出並存入到cacheKey對象中
cacheKey.update(boundSql.getSql());
//下面這一塊就是封裝參數
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
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);
}
}
//從configuration對象中(也就是載入配置文件後存放的對象)把EnvironmentId存入
/**
* <environments default="development">
* <environment id="development"> //就是這個id
* <!--當前事務交由JDBC進行管理-->
* <transactionManager type="JDBC"></transactionManager>
* <!--當前使用mybatis提供的連接池-->
* <dataSource type="POOLED">
* <property name="driver" value="${jdbc.driver}"/>
* <property name="url" value="${jdbc.url}"/>
* <property name="username" value="${jdbc.username}"/>
* <property name="password" value="${jdbc.password}"/>
* </dataSource>
* </environment>
* </environments>
*/
if (configuration.getEnvironment() != null) {
// issue #176
cacheKey.update(configuration.getEnvironment().getId());
}
//返回
return cacheKey;
}
我們再點進去cacheKey.update()
方法看一看
/**
* @author Clinton Begin
*/
public class CacheKey implements Cloneable, Serializable {
private static final long serialVersionUID = 1146682552656046210L;
public static final CacheKey NULL_CACHE_KEY = new NullCacheKey();
private static final int DEFAULT_MULTIPLYER = 37;
private static final int DEFAULT_HASHCODE = 17;
private final int multiplier;
private int hashcode;
private long checksum;
private int count;
//值存入的地方
private transient List<Object> updateList;
//省略部分方法......
//省略部分方法......
public void update(Object object) {
int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
count++;
checksum += baseHashCode;
baseHashCode *= count;
hashcode = multiplier * hashcode + baseHashCode;
//看到把值傳入到了一個list中
updateList.add(object);
}
//省略部分方法......
}
我們知道了那些數據是在CacheKey對象中如何存儲的了。下面我們返回createCacheKey()
方法。
Ctrl+鼠標左鍵 點擊方法名,查詢有哪些地方調用了此方法
我們進入BaseExecutor
,可以看到一個query()
方法:
這裡我們很清楚的看到,在執行query()
方法前,CacheKey
方法被創建了
3. 一級緩存的執行流程
我們可以看到,創建CacheKey後調用了query()方法,我們再次點進去:
在執行SQL前如何在一級緩存中找不到Key,那麼將會執行sql,我們來看一下執行sql前後會做些什麼,進入list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
分析一下:
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
//1. 把key存入緩存,value放一個佔位符
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
//2. 與數據庫交互
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
//3. 如果第2步出了什麼異常,把第1步存入的key刪除
localCache.removeObject(key);
}
//4. 把結果存入緩存
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
至此,我們思路就非常的清晰了。
結論:
在執行sql前,會首先根據CacheKey
查詢緩存中有沒有,如果有,就處理緩存中的參數,如果沒有,就執行sql,執行sql後把結果存入緩存。
一級緩存源碼分析結論:
- 一級緩存的數據結構是一個
HashMap<Object,Object>
,它的value就是查詢結果,它的key是CacheKey
,CacheKey
中有一個list屬性,statementId,params,rowbounds,sql
等參數都存入到了這個list中 - 一級緩存在調用
query()
方法前被創建。並傳入到query()
方法中 - 會首先根據
CacheKey
查詢緩存中有沒有,如果有,就處理緩存中的參數,如果沒有,就執行sql,執行sql後把結果存入緩存。