赞
踩
答: 当我们建立SqlSession
时,就可以通过Mybatis
进行sql
查询,假如本次session
查询时我们需要进行两次相同的sql
查询,就需要进行进行两次的磁盘IO
,为了避免这种没必要的等待,Mybatis
为每一个SqlSession
设置一级缓存,在同一个SqlSession
中,一级缓存会将第一次查询结果缓存起来,第二次相同的查询就可以直接使用了。
如下图,第一次查询时,该SqlSession
就会将executor
的查询结果存到缓存中。
第二次查询时,由于查询内容和第一次一样,所以直接从缓存中返回结果即可。
答: Mybatis默认是开启一级缓存的,如下所示,可以发现只要第二次使用的sql和参数一样,就会从一级缓存中获取数据。
User1 user1 = user1Mapper.select("1");
logger.info("一级缓存第一次查询:[{}]", user1);
User1 user11 = user1Mapper.select("1");
logger.info("一级缓存第二次查询:[{}]", user11);
User1 user12 = user1Mapper.select("2");
logger.info("一级缓存第三次查询,id不同:[{}]", user12);
输出结果
/**
* 输出结果
* 2022-11-27 15:51:28,313 [main] DEBUG [com.zsy.mapper.User1Mapper.select] - ==> Preparing: select * from user1 where id = ?
* 2022-11-27 15:51:28,338 [main] DEBUG [com.zsy.mapper.User1Mapper.select] - ==> Parameters: 1(String)
* 2022-11-27 15:51:28,539 [main] DEBUG [com.zsy.mapper.User1Mapper.select] - <== Total: 1
* 2022-11-27 15:51:28,541 [main] DEBUG [com.zsy.mapper.User1Mapper.select] - ==> Preparing: select * from user1 where id = ?
* 2022-11-27 15:51:28,541 [main] DEBUG [com.zsy.mapper.User1Mapper.select] - ==> Parameters: 2(String)
* [main] INFO com.zsy.mapper.MyBatisTest - 一级缓存第一次查询:[User1{id='1', name='小明', user2=null}]
* [main] INFO com.zsy.mapper.MyBatisTest - 一级缓存第二次查询:[User1{id='1', name='小明', user2=null}]
* 2022-11-27 15:51:28,667 [main] DEBUG [com.zsy.mapper.User1Mapper.select] - <== Total: 1
* [main] INFO com.zsy.mapper.MyBatisTest - 一级缓存第三次查询,id不同:[User1{id='2', name='小王', user2=null}]
*/
答: 这个问题我们不妨以终为始来了解,通过IDEA
将上文缓存的put方法中打一个断点,来了解一下执行流程。当然我们也得有一个查询代码,查询代码如下所示
User1 user1 = user1Mapper.select("1");
logger.info("一级缓存第一次查询:[{}]", user1);
这个查询首先会触发Mybatis
创建的代理对象MapperProxy
的调用
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
return this.mapperMethod.execute(sqlSession, args);
}
然后走到execute
,可以看到真正调用selectOne
执行
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
Object param;
switch(this.command.getType()) {
........
//更具返回类型决定select逻辑
case SELECT:
if (this.method.returnsVoid() && this.method.hasResultHandler()) {
this.executeWithResultHandler(sqlSession, args);
result = null;
} else if (this.method.returnsMany()) {
result = this.executeForMany(sqlSession, args);
} else if (this.method.returnsMap()) {
result = this.executeForMap(sqlSession, args);
} else if (this.method.returnsCursor()) {
result = this.executeForCursor(sqlSession, args);
} else {
//本次查询是查询一个对象值的所以走到selectOne
param = this.method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(this.command.getName(), param);
if (this.method.returnsOptional() && (result == null || !this.method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
.......
}
再步入可以发现selectOne
逻辑说白了就是调用selectList
再去第一条,若有多条数据则直接报错
public <T> T selectOne(String statement, Object parameter) {
List<T> list = this.selectList(statement, parameter);
if (list.size() == 1) {
return list.get(0);
} else if (list.size() > 1) {
throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
} else {
return null;
}
}
不断步进可以看到selectList
通过executor.query
获取到执行结果var6
并返回
private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
List var6;
try {
MappedStatement ms = this.configuration.getMappedStatement(statement);
//进行真正的查询逻辑,然后直接返回
var6 = this.executor.query(ms, this.wrapCollection(parameter), rowBounds, handler);
} ..........
return var6;
}
我们看看query
的逻辑吧,这个query
调用者是CachingExecutor
,说明这个查询会涉及缓存操作
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
CacheKey key = this.createCacheKey(ms, parameterObject, rowBounds, boundSql);
return this.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
从笔者debug
的图片中也能看出,boundSql
封装了查询SQL
、参数等信息,然后调用query
不断步进我们会发现调用到BaseExecutor
的queryFromDatabase
,然后将查询结果存到缓存中
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
this.localCache.putObject(key, ExecutionPlaceholder.EXECUTION_PLACEHOLDER);
List list;
try {
//查询逻辑
list = this.doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
this.localCache.removeObject(key);
}
//将结果缓存起来
this.localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
this.localOutputParameterCache.putObject(key, parameter);
}
return list;
}
最终结果就被缓存,这个key
就是CacheKey key = this.createCacheKey(ms, parameterObject, rowBounds, boundSql);
的结果
这样第二次查询时,源码就会走到BaseExecutor
的query
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
........
List list;
try {
++this.queryStack;
//查询一级缓存中是否有值,若有则直接处理返回
list = resultHandler == null ? (List)this.localCache.getObject(key) : null;
if (list != null) {
this.handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
--this.queryStack;
}
.......
return list;
}
}
总结一下流程就如下图所示
答: 可以从三种情况进行阐述吧:
SqlSession
调用了close
之后,会直接释放PerpetualCache
对象。缓存自然不能使用了。update
、delete
、insert
等操作,缓存就会被清空,但是缓存对象还能用。clearCache
同理,缓存被清空,但是对象还能用。答: 日常开发过程中,我们都是整合Spring
的,所以每一次查询都会创建一个新的sqlSession
,所以如果希望使用一级缓存则要开启一个事务确保本次所有操作都在同一个sqlSession
中。
答: 不至于,一级缓存是session
级别,并且随便进行一个修改操作缓存就会被清空,退一万步来说,就算对象过大,我们不也可以手动清除缓存不是吗?
为了讨论二级缓存,我们不妨展示一个简单的二级缓存配置示例
首先Mybatis
配置开启二级缓存(注意mybatis默认是开启的,这里显示声明一下而已)
<settings>
<!--开启二级缓存-->
<setting name="cacheEnabled" value="true"/>
</settings>
对应的Mapper.xml
添加下面这段配置
<cache/>
测试代码
User1 user1 = user1Mapper.select("1");
logger.info("二级缓存第一次查询:[{}]", user1);
if (sqlSession != null) {
sqlSession.close();
}
SqlSession sqlSession2 = sqlSessionFactory.openSession();
User1Mapper user1Mapper1 = sqlSession2.getMapper(User1Mapper.class);
User1 user13 = user1Mapper1.select("1");
logger.info("二级缓存第二次查询:[{}]", user13);
if (sqlSession2 != null) {
sqlSession2.close();
}
可以看到使用同样的会话,第二次查询不会查询SQL
而是直接从二级缓存获取数据。
2022-11-28 12:59:19,074 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@23986957]
2022-11-28 12:59:19,192 [main] DEBUG [com.zsy.mapper.User1Mapper.select] - ==> Preparing: select * from user1 where id = ?
2022-11-28 12:59:19,216 [main] DEBUG [com.zsy.mapper.User1Mapper.select] - ==> Parameters: 1(String)
2022-11-28 12:59:19,339 [main] DEBUG [com.zsy.mapper.User1Mapper.select] - <== Total: 1
[main] INFO com.zsy.mapper.MyBatisTest - 二级缓存第一次查询:[User1{id='1', name='小明', user2=null}]
2022-11-28 12:59:19,348 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@23986957]
[main] INFO com.zsy.mapper.MyBatisTest - 二级缓存第二次查询:[User1{id='1', name='小明', user2=null}]
**答:**如下图所示,在开启二级缓存配置后,框架会首先去CachingExecutor
看看是否有缓存数据,若没有则会从一级缓存查询,实在找不到就通过BaseExecutor
查询并处理完缓存起来。
注意这里CachingExecutor
用到了装饰者模式,将Executor 组合进来,所以CachingExecutor
会先调用(List)this.tcm.getObject(cache, key);
看看缓存中是否有数据,若没有在进行进一步查询并缓存的操作。
//将基础执行器作为被装饰的成员属性组合进来
private final Executor delegate;
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) {
this.flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
this.ensureNoOutParams(ms, boundSql);
//先去缓存查询
List<E> list = (List)this.tcm.getObject(cache, key);
if (list == null) {
// 若为空则用非缓存执行器进行数据获取
list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
//将数据存到缓存中
this.tcm.putObject(cache, key, list);
}
return list;
}
}
return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
答: 有两种,一种是自定义划分,我们在每个Mapper.xml
中添加 <cache/>
使得每一个mapper
都有一个全局的独立缓存空间
假如我们希望多个mapper
共享一个空间的话,需要被分享的mapper
使用<cache/>
,而其他mapper
则用<cache-ref namespace="">
指向这个空间即可。
答: 总的来说是三个条件:
<setting name="cacheEnabled" value="true"/>
mapper.xml
标签配置了 <cache/>
或者 <cache-ref/>
select
语句配置useCache=true
答: 前面说了,先去二级缓存查,若没有再去一级缓存,最后使用执行器查SQL。
答: 有三种吧:
LRU
、FIFO
等。org.apache.ibatis.cache.Cache
自行实现一个缓存。答: 如下图,可以看到框架自身基于装饰者模式实现了很多缓存工具,并且每个缓存容量都有限制,不同的缓存工具内存回收策略是不同的:例如LruCache
即最近最少使用算法,内存容量满了就回收到现在为止最不常用的。而FifoCache
同理,内存满了之后回收最先被缓存的数据,ScheduledCache
则是定时清理缓存了。
答: 二级缓存关联刷新问题说白了就是一张表有涉及多表联查然后对数据进行缓存,然后被关联表修改后,未能及时更新导致的问题。
如下图所示,UserMapper查询数据需要关联机构表,第一次查询后将数据缓存起来,在第二次查询前,机构的mapper将数据更新,这就导致UserMapper第二次查询拿到的机构信息是老的,进而导致数据一致性问题。
解决方案也很简单,我们只要确保缓存更新被关联表时,及时刷新响应缓存即可,具体可以参考这篇文章
答: 主要参数有这么四个:
缓存回收策略(eviction)
:这个参数有这么4个LRU
最近最少回收算法这种是默认的算法、FIFO
先进先出算法、SOFT
算法(基于垃圾回收器算法和软引用回收的对象)、WEAK
算法即基于垃圾回收器算法和弱引用规则回收对象。刷新间隔(flushInterval)
:单位毫秒。容量(size)
:引用数目,正整数。是否只读(readOnly)
:如果只读则直接返回缓存实例,性能上会相对有些优势。若不为只读则会通过序列化获取对象的拷贝,性能就相对差一些。配置范例如下所示:
<cache eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>
**答:**有两种情况一种是第一次查询的sqlsession
没有提交或者关闭
User1 user1 = user1Mapper.select("1");
logger.info("二级缓存第一次查询:[{}]", user1);
SqlSession sqlSession2 = sqlSessionFactory.openSession();
User1Mapper user1Mapper1 = sqlSession2.getMapper(User1Mapper.class);
User1 user13 = user1Mapper1.select("1");
logger.info("二级缓存第二次查询:[{}]", user13);
输出结果
2022-11-29 01:05:43,339 [main] DEBUG [com.zsy.mapper.User1Mapper.select] - ==> Preparing: select * from user1 where id = ?
2022-11-29 01:05:43,363 [main] DEBUG [com.zsy.mapper.User1Mapper.select] - ==> Parameters: 1(String)
2022-11-29 01:05:43,502 [main] DEBUG [com.zsy.mapper.User1Mapper.select] - <== Total: 1
2022-11-29 01:05:43,506 [main] DEBUG [com.zsy.mapper.User1Mapper] - Cache Hit Ratio [com.zsy.mapper.User1Mapper]: 0.0
2022-11-29 01:05:43,506 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Opening JDBC Connection
[main] INFO com.zsy.mapper.MyBatisTest - 二级缓存第一次查询:[User1{id='1', name='小明', user2=null}]
2022-11-29 01:05:44,234 [main] DEBUG [org.apache.ibatis.datasource.pooled.PooledDataSource] - Created connection 550668305.
2022-11-29 01:05:44,234 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@20d28811]
2022-11-29 01:05:44,351 [main] DEBUG [com.zsy.mapper.User1Mapper.select] - ==> Preparing: select * from user1 where id = ?
2022-11-29 01:05:44,351 [main] DEBUG [com.zsy.mapper.User1Mapper.select] - ==> Parameters: 1(String)
2022-11-29 01:05:44,465 [main] DEBUG [com.zsy.mapper.User1Mapper.select] - <== Total: 1
[main] INFO com.zsy.mapper.MyBatisTest - 二级缓存第二次查询:[User1{id='1', name='小明', user2=null}]
第二种则是更新操作
2022-11-29 01:07:22,302 [main] DEBUG [com.zsy.mapper.User1Mapper.select] - ==> Preparing: select * from user1 where id = ?
2022-11-29 01:07:22,326 [main] DEBUG [com.zsy.mapper.User1Mapper.select] - ==> Parameters: 1(String)
2022-11-29 01:07:22,456 [main] DEBUG [com.zsy.mapper.User1Mapper.select] - <== Total: 1
[main] INFO com.zsy.mapper.MyBatisTest - 二级缓存第一次查询:[User1{id='1', name='小明', user2=null}]
2022-11-29 01:07:22,479 [main] DEBUG [com.zsy.mapper.User1Mapper.updatebySet] - ==> Preparing: update user1 SET id=?, name=? where id=?
2022-11-29 01:07:22,479 [main] DEBUG [com.zsy.mapper.User1Mapper.updatebySet] - ==> Parameters: 1(String), aa(String), 1(String)
2022-11-29 01:07:22,713 [main] DEBUG [com.zsy.mapper.User1Mapper.updatebySet] - <== Updates: 1
2022-11-29 01:07:22,714 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Rolling back JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@f8c1ddd]
2022-11-29 01:07:22,833 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@f8c1ddd]
2022-11-29 01:07:22,949 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@f8c1ddd]
2022-11-29 01:07:22,949 [main] DEBUG [org.apache.ibatis.datasource.pooled.PooledDataSource] - Returned connection 260840925 to pool.
2022-11-29 01:07:22,949 [main] DEBUG [com.zsy.mapper.User1Mapper] - Cache Hit Ratio [com.zsy.mapper.User1Mapper]: 0.0
2022-11-29 01:07:22,949 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Opening JDBC Connection
2022-11-29 01:07:22,949 [main] DEBUG [org.apache.ibatis.datasource.pooled.PooledDataSource] - Checked out connection 260840925 from pool.
2022-11-29 01:07:22,949 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@f8c1ddd]
2022-11-29 01:07:23,065 [main] DEBUG [com.zsy.mapper.User1Mapper.select] - ==> Preparing: select * from user1 where id = ?
2022-11-29 01:07:23,065 [main] DEBUG [com.zsy.mapper.User1Mapper.select] - ==> Parameters: 1(String)
[main] INFO com.zsy.mapper.MyBatisTest - 二级缓存第二次查询:[User1{id='1', name='小明', user2=null}]
2022-11-29 01:07:23,184 [main] DEBUG [com.zsy.mapper.User1Mapper.select] - <== Total: 1
要想真正用上二级缓存,需要像这样及时提交或者关闭其他session
User1 user1 = user1Mapper.select("1");
logger.info("二级缓存第一次查询:[{}]", user1);
if (sqlSession != null) {
sqlSession.close();
}
SqlSession sqlSession2 = sqlSessionFactory.openSession();
User1Mapper user1Mapper1 = sqlSession2.getMapper(User1Mapper.class);
User1 user13 = user1Mapper1.select("1");
logger.info("二级缓存第二次查询:[{}]", user13);
if (sqlSession2 != null) {
sqlSession2.close();
}
答:
一级缓存默认开启,作用域session
,当session
调用close
或者flush
时就会被清空,缓存也是PerpetualCache
一种基于HashMap
实现的缓存。
而二级缓存作用于mapper(namespace)
,也是基于缓存也是PerpetualCache
,默认不开启,需要缓存的属性类必须实现序列化接口,而且二级缓存可以自定义缓存存储源。
【Java教程】看懂这篇文章-你就懂了Mybatis的二级缓存
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。