緩存就是記憶體中的資料,常常來自對資料庫查詢結果的儲存。使用緩存,我們可以避免頻繁的與資料庫進行互動,進而提高響應速度MyBatis也提供了對緩存的支援,分為一級緩存和二級緩存,可以通過下圖來了解:
①、一級緩存是SqlSession級别的緩存。在操作資料庫時需要構造sqlSession對象,在對象中有一個資料結構(HashMap)用于存儲緩存資料。不同的sqlSession之間的緩存資料區域(HashMap)是互相不影響的。
②、二級緩存是mapper級别的緩存,多個SqlSession去操作同一個Mapper的sql語句,多個SqlSession可以共用二級緩存,二級緩存是跨SqlSession的
一級緩存
預設是開啟的
①、我們使用同一個sqlSession,對User表根據相同id進行兩次查詢,檢視他們發出sql語句的情況
@Test
public void firstLevelCacheTest() throws IOException {
// 1. 通過類加載器對配置檔案進行加載,加載成了位元組輸入流,存到記憶體中 注意:配置檔案并沒有被解析
InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
// 2. (1)解析了配置檔案,封裝configuration對象 (2)建立了DefaultSqlSessionFactory工廠對象
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
// 3.問題:openSession()執行邏輯是什麼?
// 3. (1)建立事務對象 (2)建立了執行器對象cachingExecutor (3)建立了DefaultSqlSession對象
SqlSession sqlSession = sqlSessionFactory.openSession();
// 4. 委派給Executor來執行,Executor執行時又會調用很多其他元件(參數設定、解析sql的擷取,sql的執行、結果集的封裝)
User user = sqlSession.selectOne("com.itheima.mapper.UserMapper.findByCondition", 1);
User user2 = sqlSession.selectOne("com.itheima.mapper.UserMapper.findByCondition", 1);
System.out.println(user == user2);
sqlSession.close();
}
檢視控制台列印情況:
② 同樣是對user表進行兩次查詢,隻不過兩次查詢之間進行了一次update操作。
@Test
public void test3() throws IOException {
// 1. 通過類加載器對配置檔案進行加載,加載成了位元組輸入流,存到記憶體中 注意:配置檔案并沒有被解析
InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
// 2. (1)解析了配置檔案,封裝configuration對象 (2)建立了DefaultSqlSessionFactory工廠對象
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
// 3.問題:openSession()執行邏輯是什麼?
// 3. (1)建立事務對象 (2)建立了執行器對象cachingExecutor (3)建立了DefaultSqlSession對象
SqlSession sqlSession = sqlSessionFactory.openSession();
// 4. 委派給Executor來執行,Executor執行時又會調用很多其他元件(參數設定、解析sql的擷取,sql的執行、結果集的封裝)
User user = sqlSession.selectOne("com.itheima.mapper.UserMapper.findByCondition", 1);
User user1 = new User();
user1.setId(1);
user1.setUsername("zimu");
sqlSession.update("com.itheima.mapper.UserMapper.updateUser",user1);
sqlSession.commit();
User user2 = sqlSession.selectOne("com.itheima.mapper.UserMapper.findByCondition", 1);
System.out.println(user == user2);
System.out.println(user);
System.out.println(user2);
System.out.println("MyBatis源碼環境搭建成功....");
sqlSession.close();
}
檢視控制台列印情況:
③、總結
1、第一次發起查詢使用者id為1的使用者資訊,先去找緩存中是否有id為1的使用者資訊,如果沒有,從 資料庫查詢使用者資訊。得到使用者資訊,将使用者資訊存儲到一級緩存中。
2、 如果中間sqlSession去執行commit操作(執行插入、更新、删除),則會清空SqlSession中的 一級緩存,這樣做的目的為了讓緩存中存儲的是最新的資訊,避免髒讀。
3、 第二次發起查詢使用者id為1的使用者資訊,先去找緩存中是否有id為1的使用者資訊,緩存中有,直 接從緩存中擷取使用者資訊
一級緩存原理探究與源碼分析
問題1:一級緩存 底層資料結構到底是什麼?
問題2:一級緩存的工作流程是怎樣的?
一級緩存 底層資料結構到底是什麼?
之前說
不同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
一級緩存的執行流程
我們進入到
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()
方法看一看
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()
方法。
我們進入
BaseExecutor
,可以看到一個
query()
方法:
這裡我們很清楚的看到,在執行
query()
方法前,
CacheKey
方法被建立了
我們可以看到,建立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;
}
一級緩存源碼分析結論:
- 一級緩存的資料結構是一個
,它的value就是查詢結果,它的key是HashMap<Object,Object>
,CacheKey
中有一個list屬性,CacheKey
等參數都存入到了這個list中statementId,params,rowbounds,sql
- 先建立
,會首先根據CacheKey
查詢緩存中有沒有,如果有,就處理緩存中的參數,如果沒有,就執行sql,執行sql後執行sql後把結果存入緩存CacheKey
二級緩存
注意:Mybatis的二級緩存不是預設開啟的,是需要經過配置才能使用的
啟用二級緩存
分為三步走:
1)開啟映射器配置檔案中的緩存配置:
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
- 在需要使用二級緩存的Mapper配置檔案中配置<cache>标簽
<!--type:cache使用的類型,預設是PerpetualCache,這在一級緩存中提到過。
eviction: 定義回收的政策,常見的有FIFO,LRU。
flushInterval: 配置一定時間自動重新整理緩存,機關是毫秒。
size: 最多緩存對象的個數。
readOnly: 是否隻讀,若配置可讀寫,則需要對應的實體類能夠序列化。
blocking: 若緩存中找不到對應的key,是否會一直blocking,直到有對應的資料進入緩存。
-->
<cache></cache>
3)在具體CURD标簽上配置 useCache=true
<select id="findById" resultType="com.itheima.pojo.User" useCache="true">
select * from user where id = #{id}
</select>
** 注意:實體類要實作Serializable接口,因為二級緩存會将對象寫進硬碟,就必須序列化,以及相容對象在網絡中的傳輸
具體實作
/**
* 測試一級緩存
*/
@Test
public void secondLevelCacheTest() throws IOException {
InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
// 2. (1)解析了配置檔案,封裝configuration對象 (2)建立了DefaultSqlSessionFactory工廠對象
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
// 3.問題:openSession()執行邏輯是什麼?
// 3. (1)建立事務對象 (2)建立了執行器對象cachingExecutor (3)建立了DefaultSqlSession對象
SqlSession sqlSession1 = sqlSessionFactory.openSession();
// 發起第一次查詢,查詢ID為1的使用者
User user1 = sqlSession1.selectOne("com.itheima.mapper.UserMapper.findByCondition", 1);
// ***必須調用sqlSession1.commit()或者close(),一級緩存中的内容才會重新整理到二級緩存中
sqlSession1.commit();// close();
// 發起第二次查詢,查詢ID為1的使用者
SqlSession sqlSession2 = sqlSessionFactory.openSession();
User user2 = sqlSession2.selectOne("com.itheima.mapper.UserMapper.findByCondition", 1);
System.out.println(user1 == user2);
System.out.println(user1);
System.out.println(user2);
sqlSession1.close();
sqlSession2.close();
}
二級緩存源碼分析
問題:
① cache标簽如何被解析的(二級緩存的底層資料結構是什麼?)?
② 同時開啟一級緩存二級緩存,優先級?
③ 為什麼隻有執行sqlSession.commit或者sqlSession.close二級緩存才會生效
④ 更新方法為什麼不會清空二級緩存?
标簽 < cache/> 的解析
二級緩存和具體的命名空間綁定,一個Mapper中有一個Cache, 相同Mapper中的MappedStatement共用同一個Cache
根據之前的mybatis源碼剖析,xml的解析工作主要交給XMLConfigBuilder.parse()方法來實作
// XMLConfigBuilder.parse()
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
parseConfiguration(parser.eval("/configuration"));// 在這裡
return configuration;
}
// parseConfiguration()
// 既然是在xml中添加的,那麼我們就直接看關于mappers标簽的解析
private void parseConfiguration(XNode root) {
try {
Properties settings = settingsAsPropertiess(root.eval("settings"));
propertiesElement(root.eval("properties"));
loadCustomVfs(settings);
typeAliasesElement(root.eval("typeAliases"));
pluginElement(root.eval("plugins"));
objectFactoryElement(root.eval("objectFactory"));
objectWrapperFactoryElement(root.eval("objectWrapperFactory"));
reflectionFactoryElement(root.eval("reflectionFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.eval("environments"));
databaseIdProviderElement(root.eval("databaseIdProvider"));
typeHandlerElement(root.eval("typeHandlers"));
// 就是這裡
mapperElement(root.eval("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
// mapperElement()
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
// 按照我們本例的配置,則直接走該if判斷
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
// 生成XMLMapperBuilder,并執行其parse方法
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url == null && mapperClass != null) {
Class<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}
我們來看看解析Mapper.xml
// XMLMapperBuilder.parse()
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
// 解析mapper屬性
configurationElement(parser.eval("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingChacheRefs();
parsePendingStatements();
}
// configurationElement()
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.eval("cache-ref"));
// 最終在這裡看到了關于cache屬性的處理
cacheElement(context.eval("cache"));
parameterMapElement(context.eval("/mapper/parameterMap"));
resultMapElements(context.eval("/mapper/resultMap"));
sqlElement(context.eval("/mapper/sql"));
// 這裡會将生成的Cache包裝到對應的MappedStatement
buildStatementFromContext(context.eval("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
}
}
// cacheElement()
private void cacheElement(XNode context) throws Exception {
if (context != null) {
//解析<cache/>标簽的type屬性,這裡我們可以自定義cache的實作類,比如redisCache,如果沒有自定義,這裡使用和一級緩存相同的PERPETUAL
String type = context.getStringAttribute("type", "PERPETUAL");
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
String eviction = context.getStringAttribute("eviction", "LRU");
Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
Long flushInterval = context.getLongAttribute("flushInterval");
Integer size = context.getIntAttribute("size");
boolean readWrite = !context.getBooleanAttribute("readOnly", false);
boolean blocking = context.getBooleanAttribute("blocking", false);
Properties props = context.getChildrenAsProperties();
// 建構Cache對象
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
}
}
先來看看是如何建構Cache對象的
MapperBuilderAssistant.useNewCache()
public Cache useNewCache(Class<? extends Cache> typeClass,
Class<? extends Cache> evictionClass,
Long flushInterval,
Integer size,
boolean readWrite,
boolean blocking,
Properties props) {
// 1.生成Cache對象
Cache cache = new CacheBuilder(currentNamespace)
//這裡如果我們定義了<cache/>中的type,就使用自定義的Cache,否則使用和一級緩存相同的PerpetualCache
.implementation(valueOrDefault(typeClass, PerpetualCache.class))
.addDecorator(valueOrDefault(evictionClass, LruCache.class))
.clearInterval(flushInterval)
.size(size)
.readWrite(readWrite)
.blocking(blocking)
.properties(props)
.build();
// 2.添加到Configuration中
configuration.addCache(cache);
// 3.并将cache指派給MapperBuilderAssistant.currentCache
currentCache = cache;
return cache;
}
我們看到一個Mapper.xml隻會解析一次<cache/>标簽,也就是隻建立一次Cache對象,放進configuration中,并将cache指派給MapperBuilderAssistant.currentCache
buildStatementFromContext(context.eval("select|insert|update|delete"));将Cache包裝到MappedStatement
// buildStatementFromContext()
private void buildStatementFromContext(List<XNode> list) {
if (configuration.getDatabaseId() != null) {
buildStatementFromContext(list, configuration.getDatabaseId());
}
buildStatementFromContext(list, null);
}
//buildStatementFromContext()
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
// 每一條執行語句轉換成一個MappedStatement
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
configuration.addIncompleteStatement(statementParser);
}
}
}
// XMLStatementBuilder.parseStatementNode();
public void parseStatementNode() {
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
...
Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");
String parameterMap = context.getStringAttribute("parameterMap");
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);
String resultMap = context.getStringAttribute("resultMap");
String resultType = context.getStringAttribute("resultType");
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
...
// 建立MappedStatement對象
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
// builderAssistant.addMappedStatement()
public MappedStatement addMappedStatement(
String id,
...) {
if (unresolvedCacheRef) {
throw new IncompleteElementException("Cache-ref not yet resolved");
}
id = applyCurrentNamespace(id, false);
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
//建立MappedStatement對象
MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
...
.flushCacheRequired(valueOrDefault(flushCache, !isSelect))
.useCache(valueOrDefault(useCache, isSelect))
.cache(currentCache);// 在這裡将之前生成的Cache封裝到MappedStatement
ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
if (statementParameterMap != null) {
statementBuilder.parameterMap(statementParameterMap);
}
MappedStatement statement = statementBuilder.build();
configuration.addMappedStatement(statement);
return statement;
}
我們看到将Mapper中建立的Cache對象,加入到了每個MappedStatement對象中,也就是同一個Mapper中所有的MappedStatement中的cache屬性引用的是同一個
有關于<cache/>标簽的解析就到這了。
查詢源碼分析
CachingExecutor
// CachingExecutor
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
// 建立 CacheKey
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
// 從 MappedStatement 中擷取 Cache,注意這裡的 Cache 是從MappedStatement中擷取的
// 也就是我們上面解析Mapper中<cache/>标簽中建立的,它儲存在Configration中
// 我們在上面解析blog.xml時分析過每一個MappedStatement都有一個Cache對象,就是這裡
Cache cache = ms.getCache();
// 如果配置檔案中沒有配置 <cache>,則 cache 為空
if (cache != null) {
//如果需要重新整理緩存的話就重新整理:flushCache="true"
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
// 通路二級緩存
List<E> list = (List<E>) tcm.getObject(cache, key);
// 緩存未命中
if (list == null) {
// 如果沒有值,則執行查詢,這個查詢實際也是先走一級緩存查詢,一級緩存也沒有的話,則進行DB查詢
list = delegate.<E>query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
// 緩存查詢結果
tcm.putObject(cache, key, list);
}
return list;
}
}
return delegate.<E>query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
如果設定了flushCache="true",則每次查詢都會重新整理緩存
<!-- 執行此語句清空緩存 -->
<select id="findbyId" resultType="com.itheima.pojo.user" useCache="true" flushCache="true" >
select * from t_demo
</select>
如上,注意二級緩存是從 MappedStatement 中擷取的。由于 MappedStatement 存在于全局配置中,可以多個 CachingExecutor 擷取到,這樣就會出現線程安全問題。除此之外,若不加以控制,多個事務共用一個緩存執行個體,會導緻髒讀問題。至于髒讀問題,需要借助其他類來處理,也就是上面代碼中 tcm 變量對應的類型。下面分析一下。
TransactionalCacheManager
/** 事務緩存管理器 */
public class TransactionalCacheManager {
// Cache 與 TransactionalCache 的映射關系表
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();
public void clear(Cache cache) {
// 擷取 TransactionalCache 對象,并調用該對象的 clear 方法,下同
getTransactionalCache(cache).clear();
}
public Object getObject(Cache cache, CacheKey key) {
// 直接從TransactionalCache中擷取緩存
return getTransactionalCache(cache).getObject(key);
}
public void putObject(Cache cache, CacheKey key, Object value) {
// 直接存入TransactionalCache的緩存中
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) {
// 從映射表中擷取 TransactionalCache
TransactionalCache txCache = transactionalCaches.get(cache);
if (txCache == null) {
// TransactionalCache 也是一種裝飾類,為 Cache 增加事務功能
// 建立一個新的TransactionalCache,并将真正的Cache對象存進去
txCache = new TransactionalCache(cache);
transactionalCaches.put(cache, txCache);
}
return txCache;
}
}
TransactionalCacheManager 内部維護了 Cache 執行個體與 TransactionalCache 執行個體間的映射關系,該類也僅負責維護兩者的映射關系,真正做事的還是 TransactionalCache。TransactionalCache 是一種緩存裝飾器,可以為 Cache 執行個體增加事務功能。下面分析一下該類的邏輯。
TransactionalCache
public class TransactionalCache implements Cache {
//真正的緩存對象,和上面的Map<Cache, TransactionalCache>中的Cache是同一個
private final Cache delegate;
private boolean clearOnCommit;
// 在事務被送出前,所有從資料庫中查詢的結果将緩存在此集合中
private final Map<Object, Object> entriesToAddOnCommit;
// 在事務被送出前,當緩存未命中時,CacheKey 将會被存儲在此集合中
private final Set<Object> entriesMissedInCache;
@Override
public Object getObject(Object key) {
// 查詢的時候是直接從delegate中去查詢的,也就是從真正的緩存對象中查詢
Object object = delegate.getObject(key);
if (object == null) {
// 緩存未命中,則将 key 存入到 entriesMissedInCache 中
entriesMissedInCache.add(key);
}
if (clearOnCommit) {
return null;
} else {
return object;
}
}
@Override
public void putObject(Object key, Object object) {
// 将鍵值對存入到 entriesToAddOnCommit 這個Map中中,而非真實的緩存對象 delegate 中
entriesToAddOnCommit.put(key, object);
}
@Override
public Object removeObject(Object key) {
return null;
}
@Override
public void clear() {
clearOnCommit = true;
// 清空 entriesToAddOnCommit,但不清空 delegate 緩存
entriesToAddOnCommit.clear();
}
public void commit() {
// 根據 clearOnCommit 的值決定是否清空 delegate
if (clearOnCommit) {
delegate.clear();
}
// 重新整理未緩存的結果到 delegate 緩存中
flushPendingEntries();
// 重置 entriesToAddOnCommit 和 entriesMissedInCache
reset();
}
public void rollback() {
unlockMissedEntries();
reset();
}
private void reset() {
clearOnCommit = false;
// 清空集合
entriesToAddOnCommit.clear();
entriesMissedInCache.clear();
}
private void flushPendingEntries() {
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
// 将 entriesToAddOnCommit 中的内容轉存到 delegate 中
delegate.putObject(entry.getKey(), entry.getValue());
}
for (Object entry : entriesMissedInCache) {
if (!entriesToAddOnCommit.containsKey(entry)) {
// 存入空值
delegate.putObject(entry, null);
}
}
}
private void unlockMissedEntries() {
for (Object entry : entriesMissedInCache) {
try {
// 調用 removeObject 進行解鎖
delegate.removeObject(entry);
} catch (Exception e) {
log.warn("...");
}
}
}
}
存儲二級緩存對象的時候是放到了TransactionalCache.entriesToAddOnCommit這個map中,但是每次查詢的時候是直接從TransactionalCache.delegate中去查詢的,是以這個二級緩存查詢資料庫後,設定緩存值是沒有立刻生效的,主要是因為直接存到 delegate 會導緻髒資料問題
為何隻有SqlSession送出或關閉之後?
那我們來看下SqlSession.commit()方法做了什麼
SqlSession
@Override
public void commit(boolean force) {
try {
// 主要是這句
executor.commit(isCommitOrRollbackRequired(force));
dirty = false;
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error committing transaction. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
// CachingExecutor.commit()
@Override
public void commit(boolean required) throws SQLException {
delegate.commit(required);
tcm.commit();// 在這裡
}
// TransactionalCacheManager.commit()
public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit();// 在這裡
}
}
// TransactionalCache.commit()
public void commit() {
if (clearOnCommit) {
delegate.clear();
}
flushPendingEntries();//這一句
reset();
}
// TransactionalCache.flushPendingEntries()
private void flushPendingEntries() {
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
// 在這裡真正的将entriesToAddOnCommit的對象逐個添加到delegate中,隻有這時,二級緩存才真正的生效
delegate.putObject(entry.getKey(), entry.getValue());
}
for (Object entry : entriesMissedInCache) {
if (!entriesToAddOnCommit.containsKey(entry)) {
delegate.putObject(entry, null);
}
}
}
二級緩存的重新整理
我們來看看SqlSession的更新操作
public int update(String statement, Object parameter) {
int var4;
try {
this.dirty = true;
MappedStatement ms = this.configuration.getMappedStatement(statement);
var4 = this.executor.update(ms, this.wrapCollection(parameter));
} catch (Exception var8) {
throw ExceptionFactory.wrapException("Error updating database. Cause: " + var8, var8);
} finally {
ErrorContext.instance().reset();
}
return var4;
}
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
this.flushCacheIfRequired(ms);
return this.delegate.update(ms, parameterObject);
}
private void flushCacheIfRequired(MappedStatement ms) {
//擷取MappedStatement對應的Cache,進行清空
Cache cache = ms.getCache();
//SQL需設定flushCache="true" 才會執行清空
if (cache != null && ms.isFlushCacheRequired()) {
this.tcm.clear(cache);
}
}
MyBatis二級緩存隻适用于不常進行增、删、改的資料,比如國家行政區省市區街道資料。一但資料變更,MyBatis會清空緩存。是以二級緩存不适用于經常進行更新的資料。
總結:
在二級緩存的設計上,MyBatis大量地運用了裝飾者模式,如CachingExecutor, 以及各種Cache接口的裝飾器。
- 二級緩存實作了Sqlsession之間的緩存資料共享,屬于namespace級别
- 二級緩存具有豐富的緩存政策。
- 二級緩存可由多個裝飾器,與基礎緩存組合而成
- 二級緩存工作由 一個緩存裝飾執行器CachingExecutor和 一個事務型預緩存TransactionalCache 完成