Mybatis一級(jí)緩存和結(jié)合Spring Framework后失效的源碼探究
1.在下面的案例中,執(zhí)行兩次查詢控制臺(tái)只會(huì)輸出一次 SQL 查詢:
mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/xxx?useUnicode=true&characterEncoding=utf-8&autoReconnect=true"/>
<property name="username" value="xxx"/>
<property name="password" value="xxx"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="com/hrh/mapper/PersonMapper.xml"/>
</mappers>
</configuration>
PersonMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.hrh.mapper.PersonMapper">
<resultMap id="BaseResultMap" type="com.hrh.bean.Person">
<id column="id" property="id" jdbcType="BIGINT"/>
<result column="name" property="name" jdbcType="VARCHAR"/>
<result column="age" property="age" jdbcType="BIGINT"/>
</resultMap>
<sql id="Base_Column_List">
id, name, age
</sql>
<select id="list" resultType="com.hrh.bean.Person">
select
<include refid="Base_Column_List"/>
from tab_person
</select>
</mapper>
public interface PersonMapper {
List<Person> list();
}
String resource = "mybatis-config2.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory =
new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();//開(kāi)啟會(huì)話
PersonMapper mapper = sqlSession.getMapper(PersonMapper.class);
mapper.list();
mapper.list();

之所以會(huì)出現(xiàn)這種情況,是因?yàn)?Mybatis 存在一級(jí)緩存導(dǎo)致的,下面 debug 探究下內(nèi)部流程:

?。?)mapper.list() 會(huì)進(jìn)入 MapperProxy#invoke():參數(shù)proxy是一個(gè)代理對(duì)象(每個(gè) Mapper 接口都會(huì)被轉(zhuǎn)換成一個(gè)代理對(duì)象),里面包含會(huì)話 sqlSession、接口信息、方法信息;method是目標(biāo)方法(當(dāng)前執(zhí)行的方法),它里面包含了所屬的哪個(gè)類(lèi)(接口)、方法名、返回類(lèi)型(List、Map、void 或其他)、參數(shù)類(lèi)型等;args是參數(shù);
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (isDefaultMethod(method)) {
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
//從方法緩存methodCache中獲取到方法的信息:比如方法名、類(lèi)型(select、update等)、返回類(lèi)型
//如果獲取中沒(méi)有MapperMethod,則創(chuàng)建一個(gè)并放入methodCache中
final MapperMethod mapperMethod = cachedMapperMethod(method);
//執(zhí)行查詢SQL并返回結(jié)果
return mapperMethod.execute(sqlSession, args);
}

cacheMapperMethod:MapperMethod 包含方法名、類(lèi)型(select、update等)、返回類(lèi)型等信息
private MapperMethod cachedMapperMethod(Method method) {
//緩存中獲取
MapperMethod mapperMethod = methodCache.get(method);
//沒(méi)有則創(chuàng)建一個(gè)對(duì)象并放入緩存中供下次方便取用
if (mapperMethod == null) {
mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
methodCache.put(method, mapperMethod);
}
return mapperMethod;
}
?。?)MapperMethod#execute()根據(jù) SQL 類(lèi)型進(jìn)入不同的查詢方法
public Object execute(SqlSession sqlSession, Object[] args) {
//返回結(jié)果
Object result;
//判斷語(yǔ)句類(lèi)型
switch (command.getType()) {
case INSERT: {//插入語(yǔ)句
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {//更新語(yǔ)句
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {//刪除語(yǔ)句
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT://查詢語(yǔ)句
//返回空的查詢
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
//返回List的查詢
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
//返回Map的查詢
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
//返回游標(biāo)的查詢
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}
?。?)上面的案例是 select 語(yǔ)句,返回結(jié)果是List集合,所以進(jìn)入 MapperMethod#executeForMany():
private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
List<E> result;
//獲取參數(shù)
Object param = method.convertArgsToSqlCommandParam(args);
//是否有分頁(yè)查詢
if (method.hasRowBounds()) {
RowBounds rowBounds = method.extractRowBounds(args);
result = sqlSession.<E>selectList(command.getName(), param, rowBounds);
} else {
result = sqlSession.<E>selectList(command.getName(), param);
}
// issue #510 Collections & arrays support
//如果list中的泛型跟結(jié)果類(lèi)型不一致,進(jìn)行轉(zhuǎn)換
if (!method.getReturnType().isAssignableFrom(result.getClass())) {
if (method.getReturnType().isArray()) {
return convertToArray(result);
} else {
return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
}
}
return result;
}
?。?)selectList執(zhí)行了DefaultSqlSession#selectList():
public <E> List<E> selectList(String statement, Object parameter) {
return this.selectList(statement, parameter, RowBounds.DEFAULT);
}
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
//SQL執(zhí)行的信息:resource(xxMapper.xml)、id、sql、返回類(lèi)型等
MappedStatement ms = configuration.getMappedStatement(statement);
//執(zhí)行查詢
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}

?。?)接下來(lái)調(diào)用緩存執(zhí)行器的方法:CachingExecutor#query()
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
//獲取到執(zhí)行SQL
BoundSql boundSql = ms.getBoundSql(parameterObject);
//將SQL包裝成一個(gè)緩存對(duì)對(duì)象,該對(duì)象和結(jié)果集組成鍵值對(duì)存儲(chǔ)到緩存中,方便下次直接從緩存中拿而不需要再次查詢
//createCacheKey:調(diào)用BaseExecutor#createCacheKey
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 {
//獲取緩存
Cache cache = ms.getCache();
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
//沒(méi)有緩存連接查詢
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
?。?)接下來(lái)執(zhí)行 BaseExecutor#query():從下面可以看到將結(jié)果緩存到localCache 中了
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());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
//如果不是嵌套查詢(默認(rèn)為0),且 <select> 的 flushCache=true 時(shí)清空緩存
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
//嵌套查詢層數(shù)+1
queryStack++;
//從localCache緩存中獲取
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
//連接查詢
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
//下面是延遲加載邏輯
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
//緩存中添加占位符
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
//連接查詢獲取到數(shù)據(jù)結(jié)果
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
//刪除占位符
localCache.removeObject(key);
}
//將結(jié)果緩存起來(lái)
localCache.putObject(key, list);
//處理存儲(chǔ)過(guò)程
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
2.但當(dāng) Spring Framework + Mybatis 時(shí),情況就不一樣了,每次查詢都會(huì)連接數(shù)據(jù)庫(kù)查詢,控制臺(tái)都會(huì)打印 SQL 出來(lái),如下案例:
@Service
public class PersonService {
@Autowired
PersonMapper personMapper;
public List<Person> getList() {
personMapper.list();
personMapper.list();
return personMapper.list();
}
}
@Configuration
@ComponentScan("com.hrh")
@MapperScan("com.hrh.mapper")
public class MyBatisConfig {
@Bean
public SqlSessionFactoryBean sqlSessionFactory() throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource());
factoryBean.setMapperLocations(resolveMapperLocations());
return factoryBean;
}
public Resource[] resolveMapperLocations() {
ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
List<String> mapperLocations = new ArrayList<>();
mapperLocations.add("classpath*:com/hrh/mapper/*Mapper*.xml");
List<Resource> resources = new ArrayList();
if (mapperLocations != null) {
for (String mapperLocation : mapperLocations) {
try {
Resource[] mappers = resourceResolver.getResources(mapperLocation);
resources.addAll(Arrays.asList(mappers));
} catch (IOException e) {
// ignore
}
}
}
return resources.toArray(new Resource[resources.size()]);
}
@Bean
public DataSource dataSource() {
DriverManagerDataSource driverManagerDataSource = new DriverManagerDataSource();
driverManagerDataSource.setDriverClassName("com.mysql.jdbc.Driver");
driverManagerDataSource.setUsername("xxx");
driverManagerDataSource.setPassword("xxx");
driverManagerDataSource.setUrl("jdbc:mysql://localhost:3306/xxx?useUnicode=true&characterEncoding=utf-8&autoReconnect=true");
return driverManagerDataSource;
}
}
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MyBatisConfig.class);
PersonService bean = context.getBean(PersonService.class);
bean.getList();

下面debug進(jìn)入的步驟跟上面的(1)、(2)、(3)是一致的,但第四步卻是進(jìn)入SqlSessionTemplate#selectList()中【SqlSessionTemplate是mybatis-spring-xx.jar的,上文的DefaultSqlSession是屬于mybatis-xx.jar的】:
public <E> List<E> selectList(String statement, Object parameter) {
return this.selectList(statement, parameter, RowBounds.DEFAULT);
}
接下來(lái)的selectList() 會(huì)被方法攔截:method.invoke() 會(huì)執(zhí)行到 DefaultSqlSession#selectList(),重新回到上文的第四步并且繼續(xù)下去,也就是在上文的(1)~(6)中插入了前后文,在其中做了關(guān)閉會(huì)話的操作;
private class SqlSessionInterceptor implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//得到會(huì)話
SqlSession sqlSession = getSqlSession(
SqlSessionTemplate.this.sqlSessionFactory,
SqlSessionTemplate.this.executorType,
SqlSessionTemplate.this.exceptionTranslator);
try {
//執(zhí)行方法查詢
Object result = method.invoke(sqlSession, args);
if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
// force commit even on non-dirty sessions because some databases require
// a commit/rollback before calling close()
sqlSession.commit(true);//在關(guān)閉會(huì)話前提交和回滾
}
return result;
} catch (Throwable t) {//有異常拋出異常并結(jié)束會(huì)話
Throwable unwrapped = unwrapThrowable(t);
if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
// release the connection to avoid a deadlock if the translator is no loaded. See issue #22
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
sqlSession = null;
Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped);
if (translated != null) {
unwrapped = translated;
}
}
throw unwrapped;
} finally {
//關(guān)閉會(huì)話
if (sqlSession != null) {
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
}
}
}
}
總結(jié):
Mybatis 的一級(jí)緩存是會(huì)話級(jí)別的緩存(單線程的,特別雞肋),Mybatis 每創(chuàng)建一個(gè) SqlSession 會(huì)話對(duì)象,就表示打開(kāi)一次數(shù)據(jù)庫(kù)會(huì)話,在一次會(huì)話中,應(yīng)用程序很可能在短時(shí)間內(nèi)反復(fù)執(zhí)行相同的查詢語(yǔ)句,如果不對(duì)數(shù)據(jù)進(jìn)行緩存,則每查詢一次就要執(zhí)行一次數(shù)據(jù)庫(kù)查詢,這就造成數(shù)據(jù)庫(kù)資源的浪費(fèi)。又因?yàn)橥ㄟ^(guò) SqlSession 執(zhí)行的操作,實(shí)際上由 Executor 來(lái)完成數(shù)據(jù)庫(kù)操作的,所以在 Executor 中會(huì)建立一個(gè)簡(jiǎn)單的緩存,即一級(jí)緩存;將每次的查詢結(jié)果緩存起來(lái),再次執(zhí)行查詢的時(shí)候,會(huì)先查詢一級(jí)緩存(默認(rèn)開(kāi)啟的),如果命中,則直接返回,否則再去查詢數(shù)據(jù)庫(kù)并放入緩存中。
一級(jí)緩存的生命周期與 SqlSession 的生命周期相同,因此當(dāng) Mybatis 和Spring Framework 的集成包中擴(kuò)展了一個(gè) SqlSessionTemplate 類(lèi)(它是一個(gè)代理類(lèi),增強(qiáng)了查詢方法),所有的查詢經(jīng)過(guò) SqlSessionTemplate 代理攔截后再進(jìn)入到 DefaultSqlSession#selectList() 中,結(jié)束查詢后把會(huì)話SqlSession 關(guān)了,所以導(dǎo)致了緩存失效。
那為什么要這么操作呢?
原始的 Mybatis 有暴露 SqlSession 接口,因此有 close 方法暴露出來(lái)供你選擇使用,你可以選擇關(guān)與不關(guān),但在Mybatis 和Spring Framework 的集成包中,SqlSession 是交給了Spring Framework 管理的,沒(méi)有暴露出來(lái),為了穩(wěn)妥決定,直接給你關(guān)了。
到此這篇關(guān)于Mybatis一級(jí)緩存和結(jié)合Spring Framework后失效的源碼探究的文章就介紹到這了,更多相關(guān)Mybatis一級(jí)緩存Spring Framework失效內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java多線程編程實(shí)戰(zhàn)之模擬大量數(shù)據(jù)同步
這篇文章主要介紹了Java多線程編程實(shí)戰(zhàn)之模擬大量數(shù)據(jù)同步,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-02-02
如何使用jmeter錄制瀏覽器Https請(qǐng)求過(guò)程圖解
這篇文章主要介紹了基于jmeter錄制瀏覽器Https請(qǐng)求過(guò)程圖解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-04-04
全面解析Spring Security 過(guò)濾器鏈的機(jī)制和特性
這篇文章主要介紹了Spring Security 過(guò)濾器鏈的機(jī)制和特性,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-07-07
Java基于接口實(shí)現(xiàn)模擬動(dòng)物聲音代碼實(shí)例
這篇文章主要介紹了Java基于接口實(shí)現(xiàn)模擬動(dòng)物聲音代碼實(shí)例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-06-06
Spring循環(huán)依賴(lài)正確性及Bean注入的順序關(guān)系詳解
這篇文章主要給大家介紹了關(guān)于Spring循環(huán)依賴(lài)的正確性,以及Bean注入的順序關(guān)系的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。2018-01-01
Java中使用異或語(yǔ)句實(shí)現(xiàn)兩個(gè)變量的互換
這篇文章主要介紹了Java中使用異或語(yǔ)句實(shí)現(xiàn)兩個(gè)變量的互換,本文直接給出代碼實(shí)例以及運(yùn)行結(jié)果,需要的朋友可以參考下2015-06-06
Java靜態(tài)和非靜態(tài)成員變量初始化過(guò)程解析
這篇文章主要介紹了Java靜態(tài)和非靜態(tài)成員變量初始化過(guò)程解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-01-01
java如何實(shí)現(xiàn)多線程的順序執(zhí)行
多線程是java的一種重要技術(shù),但是多線程的運(yùn)行是沒(méi)有絕對(duì)的順序的,那么java如何實(shí)現(xiàn)多線程的順序執(zhí)行,下面就一起來(lái)了解一下2021-05-05
MyBatis中#號(hào)與美元符號(hào)的區(qū)別
#{變量名}可以進(jìn)行預(yù)編譯、類(lèi)型匹配等操作,#{變量名}會(huì)轉(zhuǎn)化為jdbc的類(lèi)型。很多朋友不清楚在mybatis中#號(hào)與美元符號(hào)的不同,接下來(lái)通過(guò)本文給大家介紹兩者的區(qū)別,感興趣的朋友參考下吧2017-01-01
MyBatis-Plus模糊查詢特殊字符串轉(zhuǎn)義的實(shí)現(xiàn)
使用MyBatis中的模糊查詢時(shí),當(dāng)查詢關(guān)鍵字中包括有_、\、%時(shí),查詢關(guān)鍵字失效,本文主要介紹了MyBatis-Plus模糊查詢特殊字符串轉(zhuǎn)義的實(shí)現(xiàn),感興趣的可以了解一下2024-06-06

