Mybatis一级缓存和结合Spring Framework后失效的源码探究
1.在下面的案例中,执行两次查询控制台只会输出一次 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();//开启会话
PersonMapper mapper = sqlSession.getMapper(PersonMapper.class);
mapper.list();
mapper.list();
之所以会出现这种情况,是因为 Mybatis 存在一级缓存导致的,下面 debug 探究下内部流程:
(1)mapper.list() 会进入 MapperProxy#invoke():参数 proxy是一个代理对象(每个 Mapper 接口都会被转换成一个代理对象),里面包含会话 sqlSession、接口信息、方法信息;method 是目标方法(当前执行的方法),它里面包含了所属的哪个类(接口)、方法名、返回类型(List、Map、void 或其他)、参数类型等;args 是参数;
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中获取到方法的信息:比如方法名、类型(select、update等)、返回类型
//如果获取中没有MapperMethod,则创建一个并放入methodCache中
final MapperMethod mapperMethod = cachedMapperMethod(method);
//执行查询SQL并返回结果
return mapperMethod.execute(sqlSession, args);
}
cacheMapperMethod:MapperMethod 包含方法名、类型(select、update等)、返回类型等信息
private MapperMethod cachedMapperMethod(Method method) {
//缓存中获取
MapperMethod mapperMethod = methodCache.get(method);
//没有则创建一个对象并放入缓存中供下次方便取用
if (mapperMethod == null) {
mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
methodCache.put(method, mapperMethod);
}
return mapperMethod;
}
(2)MapperMethod#execute() 根据 SQL 类型进入不同的查询方法
public Object execute(SqlSession sqlSession, Object[] args) {
//返回结果
Object result;
//判断语句类型
switch (command.getType()) {
case INSERT: {//插入语句
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {//更新语句
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {//删除语句
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT://查询语句
//返回空的查询
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);
//返回游标的查询
} 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;
}
(3)上面的案例是 select 语句,返回结果是List集合,所以进入 MapperMethod#executeForMany():
private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
List<E> result;
//获取参数
Object param = method.convertArgsToSqlCommandParam(args);
//是否有分页查询
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中的泛型跟结果类型不一致,进行转换
if (!method.getReturnType().isAssignableFrom(result.getClass())) {
if (method.getReturnType().isArray()) {
return convertToArray(result);
} else {
return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
}
}
return result;
}
(4)selectList 执行了 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执行的信息:resource(xxMapper.xml)、id、sql、返回类型等
MappedStatement ms = configuration.getMappedStatement(statement);
//执行查询
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();
}
}
(5)接下来调用缓存执行器的方法:CachingExecutor#query()
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
//获取到执行SQL
BoundSql boundSql = ms.getBoundSql(parameterObject);
//将SQL包装成一个缓存对对象,该对象和结果集组成键值对存储到缓存中,方便下次直接从缓存中拿而不需要再次查询
//createCacheKey:调用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;
}
}
//没有缓存连接查询
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
(6)接下来执行 BaseExecutor#query():从下面可以看到将结果缓存到 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.");
}
//如果不是嵌套查询(默认为0),且 <select> 的 flushCache=true 时清空缓存
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
//嵌套查询层数+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 {
//连接查询获取到数据结果
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
//删除占位符
localCache.removeObject(key);
}
//将结果缓存起来
localCache.putObject(key, list);
//处理存储过程
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
2.但当 Spring Framework + Mybatis 时,情况就不一样了,每次查询都会连接数据库查询,控制台都会打印 SQL 出来,如下案例:
@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进入的步骤跟上面的(1)、(2)、(3)是一致的,但第四步却是进入 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);
}
接下来的 selectList() 会被方法拦截:method.invoke() 会执行到 DefaultSqlSession#selectList(),重新回到上文的第四步并且继续下去,也就是在上文的(1)~(6)中插入了前后文,在其中做了关闭会话的操作;
private class SqlSessionInterceptor implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//得到会话
SqlSession sqlSession = getSqlSession(
SqlSessionTemplate.this.sqlSessionFactory,
SqlSessionTemplate.this.executorType,
SqlSessionTemplate.this.exceptionTranslator);
try {
//执行方法查询
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);//在关闭会话前提交和回滚
}
return result; } catch (Throwable t) {//有异常抛出异常并结束会话
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 {
//关闭会话
if (sqlSession != null) {
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
}
}
}
}
总结:
Mybatis 的一级缓存是会话级别的缓存(单线程的,特别鸡肋),Mybatis 每创建一个 SqlSession 会话对象,就表示打开一次数据库会话,在一次会话中,应用程序很可能在短时间内反复执行相同的查询语句,如果不对数据进行缓存,则每查询一次就要执行一次数据库查询,这就造成数据库资源的浪费。又因为通过 SqlSession 执行的操作,实际上由 Executor 来完成数据库操作的,所以在 Executor 中会建立一个简单的缓存,即一级缓存;将每次的查询结果缓存起来,再次执行查询的时候,会先查询一级缓存(默认开启的),如果命中,则直接返回,否则再去查询数据库并放入缓存中。
一级缓存的生命周期与 SqlSession 的生命周期相同,因此当 Mybatis 和 Spring Framework 的集成包中扩展了一个 SqlSessionTemplate 类(它是一个代理类,增强了查询方法),所有的查询经过 SqlSessionTemplate 代理拦截后再进入到 DefaultSqlSession#selectList() 中,结束查询后把会话SqlSession 关了,所以导致了缓存失效。
那为什么要这么操作呢?
原始的 Mybatis 有暴露 SqlSession 接口,因此有 close 方法暴露出来供你选择使用,你可以选择关与不关,但在 Mybatis 和 Spring Framework 的集成包中,SqlSession 是交给了 Spring Framework 管理的,没有暴露出来,为了稳妥决定,直接给你关了。
Mybatis一级缓存和结合Spring Framework后失效的源码探究的更多相关文章
- Mybatis的初始化和结合Spring Framework后初始化的源码探究
带着下面的问题进行学习: (1)Mybatis 框架或 Spring Framework 框架对数据层 Mapper 接口做了代理,那是做了 JDK 动态代理还是 CGLIB 代理? (2)Mappe ...
- Spring Framework自动装配setAutowireMode和Mybatis案例的源码探究
由前文可得知, Spring Framework的自动装配有两种方式:xml配置和注解配置: 自动装配的类型有: (1)xml配置中的byType根据类型查找(@Autowired注解是默认根据类型查 ...
- Mybatis 一级缓存和二级缓存原理区别 (图文详解)
Java面试经常问到Mybatis一级缓存和二级缓存,今天就给大家重点详解Mybatis一级缓存和二级缓存原理与区别@mikechen Mybatis缓存 缓存就是内存中的数据,常常来自对数据库查询结 ...
- MyBatis 一级缓存与二级缓存
MyBatis一级缓存 MyBatis一级缓存默认开启,一级缓存为Session级别的缓存,在执行以下操作时一级缓存会清空 1.执行session.clearCache(); 2.执行CUD操作 3. ...
- MyBatis一级缓存引起的无穷递归
MyBatis一级缓存引起的无穷递归 引言: 最近在项目中参与了一个领取优惠劵的活动,当多个用户领取同一张优惠劵的时候,使用了数据库锁控制并发,起初的设想是:如果多个人同时领一张劵,第一个到达的人领取 ...
- mybatis一级缓存详解
mybatis缓存分为一级缓存,二级缓存和自定义缓存.本文重点讲解一级缓存 一:前言 在介绍缓存之前,先了解下mybatis的几个核心概念: * SqlSession:代表和数据库的一次会话,向用户提 ...
- MyBatis一级缓存(转载)
<深入理解mybatis原理> MyBatis的一级缓存实现详解 及使用注意事项 http://demo.netfoucs.com/luanlouis/article/details/41 ...
- MyBatis 一级缓存、二级缓存全详解(一)
目录 MyBatis 一级缓存.二级缓存全详解(一) 什么是缓存 什么是MyBatis中的缓存 MyBatis 中的一级缓存 初探一级缓存 探究一级缓存是如何失效的 一级缓存原理探究 还有其他要补充的 ...
- Mybatis一级缓存和二级缓存总结
1:mybatis一级缓存:级别是session级别的,如果是同一个线程,同一个session,同一个查询条件,则只会查询数据库一次 2:mybatis二级缓存:级别是sessionfactory级别 ...
随机推荐
- 频繁的或者大范围的来实现数据的共享要使用Vuex
一. Vuex 概述 1.1 组件之间共享数据的方式 由于使用频繁,通常将v-bind:属性名=" "的格式简写成:属性名=" ".兄弟组件之间的共享即不相干组 ...
- lombok插件@Slf4j注解不生效问题解决办法
最近在尝试使用日志工具Sfl4j,当时使用log时报错,找了好久才解决这个问题. 1.首先需要下载Lombok插件 File->settings->Plugins 搜索Lombok,点击安 ...
- 使用Reactor完成类似的Flink的操作
一.背景 Flink在处理流式任务的时候有很大的优势,其中windows等操作符可以很方便的完成聚合任务,但是Flink是一套独立的服务,业务流程中如果想使用需要将数据发到kafka,用Flink处理 ...
- PAT-1102(Invert a Binary Tree)+二叉树的镜像+层次遍历+中序遍历+已知树的结构构树
Invert a Binary Tree pat-1102 import java.util.Arrays; import java.util.Queue; import java.util.Scan ...
- Java I/O流 03
I/O流·字符流 字符流FileReader * A:字符流是什么 * 字符流是可以直接读写字符的 IO流 * 字符流读取字符,就要先读取到字节数据,然后转换为字符:如果要写出字符,需要把字符转换为字 ...
- 番外----python入门----关于pycharm
江湖上有句话叫 "武林至尊,宝刀屠龙,号令天下,莫敢不从,倚天不出,谁与争锋". 今天,我们就来介绍一下,python编程界的"屠龙刀",pycharm. 一. ...
- MySQL基本指令3 和 索引 、分页
1视图: -创建 create view 视图名称 as SQL ps:虚拟 -修改 alter view 视图名称 as SQL -删除 drop view 视图名称 2触发器 3自定义函 ...
- Linux less命令查看文件常用查询方法
g 跳到文件开头 G 跳到文件结尾 / 往下搜索字符 ? 网上搜索字符 n 执行上一个搜索(/或者?的搜索),例如上一个搜索是使用/搜索的,则继续使用/搜索,即往下搜索结果 N 反向执行上一个搜索(/ ...
- java知识汇总
文章目录 Java基础知识 基本类型 类别及其对应包装类 1. byte---Byte 2. char---Character 3. short---Short 4. int---Integer 5. ...
- solr简明教程
文章目录 安装 启动 创建core 配置core索引MySQL数据 3.2.1 3.2.2 3.2.3 测试定时更新 五.配置中文分词 SolrJ 操作索引的增.删.查 七.通过SolrJ对MySQL ...