文章目录
- 一、前言
- 二、一级缓存
- 1. 简单使用
- 2. 问题:当MyBatis单独使用时,一级缓存默认生效,但是当和Spring整合就即失效?
- 3. MyBatis 事务管理模式:
- 三、二级缓存
一、前言
在实际项目开发中,通常对数据库查询的性能要求很高,而Mybatis提供了查询缓存数据,从而达到提高查询性能的要求。MyBatis的查询缓存分为一级缓存和二级缓存。一级缓存是SqlSessin级别的缓存,二级缓存是mapper 级别的缓存,二级缓存是多个SqlSession共享的。需要注意,MyBatis的缓存机制是基于id 进行缓存的,也就是说MyBatis使用HashMap缓存数据时,实际用对象的id作为key,而对象作为value保存的。
SqlSessionFactory : SqlSessionFactory 是Mybatis的关键对象,他是单个数据库映射关系经过编译后端内存镜像。SqlSessionFactory对象的实例可以通过SqlSessionFactoryBuilder对象从XML配置文件或一个预先定制的Configuration的实例来构建。SqlSessionFactory是线程安全的。
SqlSession : SqlSession 是Mybatis中持久化操作的对象,类似于JDBC中的Connection。他是应用程序和持久存储层之间执行交互操作的一个单线程对象。SqlSeesion是线程不安全的,所以不能被共享,每个线程都应有他自己的是SqlSession实例(Spring中在ThreadLocal中便保存着SqlSession副本)。
二、一级缓存
1. 简单使用
一级缓存的作用域是SqlSession范围的。当在同一个SqlSession中执行两次相同的sql语句时,第一次执行完毕会将数据库中查询的数据写到缓存(内存)中,第二次查询时会从缓存中获取数据,不再去底层数据库查询。如果SqlSession执行的DML操作,即插入、更新、删除操作,并提交到数据库。MyBatis会清空SqlSession中的一级缓存,以保证缓存中的信息时最新的。
MyBatis默认开启一级缓存。
可在Mybatis的配置文件中通过 localCacheScope 进行缓存作用域的配置
<?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>
<settings>
<!--使用jdbc的getGeneratekeys获取自增主键值-->
<setting name="useGeneratedKeys" value="true"/>
<!--使用列别名替换别名 默认true-->
<setting name="useColumnLabel" value="true"/>
<!--开启驼峰命名转换Table:create_time到 Entity(createTime)-->
<setting name="mapUnderscoreToCamelCase" value="true"/>
<!-- 打印日志 -->
<setting name="logImpl" value="STDOUT_LOGGING" />
<!--
localCacheScope 取值有SESSION 和 STATEMENT。默认SESSION。
SESSION情况下,会缓存一个会话中执行的所有查询。
STATEMENT情况下,本地会话仅用在语句执行上,对相同的SqlSession的不同调用将不会共享数据
-->
<setting name="localCacheScope" value="SESSION"></setting>
<!--<setting name="lazyLoadingEnabled" value="true"></setting>-->
<!--<setting name="aggressiveLazyLoading" value="false"></setting>-->
</settings>
</configuration>
在Spring整合后的MyBatis,如果不开启事务,在每次操作数据库的时候都会创建一个SqlSession,在操作结束后关闭。
下面是一个简单的方法,里面进行了两次数据库查询
未启用事务下,一级缓存失效:打印两次Sql语句代表查询两次数据库
当启用事务后,一级缓存生效 : 只查询了一次数据库,第二次查询从缓存中拿取
2. 问题:当MyBatis单独使用时,一级缓存默认生效,但是当和Spring整合就即失效?
调用接口后可以发现。未开启事务时,SqlSession创建了两次。同时之前也说了一级缓存的作用域是SqlSession范围的。即SqlSession关闭后,一级缓存就失效了。所以这里会查询两次,因为SqlSession的关闭导致一级缓存失效。
原因总结:
原因在于当mybatis和spring结合使用的时候,将原本的DefaultSqlSession替换成了SqlSessionTemplate,并且在SqlSessionTemplate将sqlSession替换成了代理对象,当我们对数据库进行操作时方法的时候会调用到SqlSessionInterceptor的invoke方法, 而在invoke方法的fianlly中调用了SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)将我们的session关闭了。原生的mybatis之所以没有关闭session是因为它把session暴露给我们了,而和spring结合使用的时候并没有提供暴露session的方法,所以只能在这里关,而一旦session关闭了,那一级缓存自然也就失效了。
源码分析:
通过断点发现 SqlSessionDaoSupport 中将创建
容器将自动为给定的 SqlSessionFactory 创建SqlSessionTemplate。进入 createSqlSessionTemplate
一路前行到
SqlSessionTemplate 下面的方法构造方法
在这里可以看到,Spring 对原先的 sqlSession进行了代理,返回指定接口的代理类的实例sqlSessionProxy,并且将方法调用分派给指定的内部类 SqlSessionInterceptor
来处理。
接下来看内部类 SqlSessionInterceptor
/**
* Proxy needed to route MyBatis method calls to the proper SqlSession got
* from Spring's Transaction Manager
* It also unwraps exceptions thrown by {@code Method#invoke(Object, Object...)} to
* pass a {@code PersistenceException} to the {@code PersistenceExceptionTranslator}.
*/
private class SqlSessionInterceptor implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 在方法执行前会先创建一个SqlSession。
SqlSession sqlSession = getSqlSession(
SqlSessionTemplate.this.sqlSessionFactory,
SqlSessionTemplate.this.executorType,
SqlSessionTemplate.this.exceptionTranslator);
try {
// 代理方法执行
Object result = method.invoke(sqlSession, args);
// 判断MyBatis的事务管理模式,是否是托管给Spring容器管理(后面详解)
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 {
// 在这里可以看到如果在执行结束后会关闭SqlSession,从而导致MyBatis一级缓存失效。
if (sqlSession != null) {
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
}
}
}
}
在 getSqlSession()
方法中会调用 registerSessionHolder
方法。该方法会同步是否活动的,如果 存在活动的事务,则注册会话持有者。而Spring的事务即是活动的。所以如果使用事务会注册会话持有者。注册会话持有者时会调用 ResourceHolderSupport.requested()
方法将引用计数器 referenceCount
加1。
在closeSqlSession方法中。如果未启动事务,则会直接调用session.close()方法关闭session。如果启用事务,则在 getSqlSession
时注册了会话持有者,所以会调用ResourceHolderSupport.released()
方法将引用计数减少1,因为holder已被释放,当注册的会话持有者。并不会关闭session。
上述代码可以看出在没有启动事务的情况下,由于SqlSession关闭导致一级缓存失效的原因,启用事务时,由于SqlSession并未关闭,所以一级缓存未失效。下面继续,事务开启后何时提交和关闭的。
断点走到方法执行结束后
下一步的断点走到了AppUtils中的 invokeJoinpointUsingReflection 方法。其实在调用方法时断点就走到了
前一句。明显可知这里使用代理为了完成AOP。
而下一步断点后面又到了 TransactionAspectSupport
类的 invokeWithinTransaction
方法。这里看类名就很明确了,这是事务支持的切面。
即在completeTransactionAfterThrowing
处理事务回滚。在 commitTransactionAfterReturning
中处理事务提交。
随后根据 txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
方法可知,进入AbstractPlatformTransactionManager中 commit -> processCommit()方法中 triggerBeforeCommit
、triggerBeforeCompletion
等方法则会触发一些回调。
举例triggerBeforeCommit
方法会调用 TransactionSynchronizationUtils.triggerBeforeCommit(status.isReadOnly());
可以看到轮询触发回调。
则会触发到 SqlSessionUtils
类的内部类 SqlSessionSynchronization
中,执行对应的回调方法。
至此,整个问题流程大体分析完毕。
3. MyBatis 事务管理模式:
上面有说道 MyBatis的事务管理模式:
MyBatis的事务管理器有两种类型,分别是JDBC 和 MANAGED
- JDBC : 这个配置就至二级使用了JDBC的提交和回滚设置,它依赖于从数据源得到的连接来管理事务范围。
- MANAGED:这个配置基本没做什么, 他从不提交或者回滚一个连接。而是让容器来管理实务的整个生命周期,默认情况下他会关闭连接,可以通过closeConnection 设置为false来解决问题。
<?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="dev">
<environment id="dev">
<transactionManager type="MANAGED">
<property name="closeConnection" value="false"></property>
</transactionManager>
<dataSource type="...">
....
</dataSource>
</environment>
</environments>
</configuration>
这也是为什么上面源码分析的时候需要判断一下是否是 MANAGED模式,否则直接将事务提交的原因。
三、二级缓存
二级缓存是Mapper级别的缓存,使用二级缓存时,多个SqlSession使用同一个Mapper的sql语句去操作数据库,得到的数据会存在二级缓存区。二级缓存区同样也是使用HashMap进行存储数据。相较于一级缓存,二级缓存范围更大,且是跨SqlSession的,即多个SqlSession共享二级缓存。
MyBatis默认没有开启二级缓存,需要在配置文件中进行配置 cacheEnabled 为true
<?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>
<settings>
<!--使用jdbc的getGeneratekeys获取自增主键值-->
<setting name="useGeneratedKeys" value="true"/>
<!--使用列别名替换别名 默认true-->
<setting name="useColumnLabel" value="true"/>
<!--开启驼峰命名转换Table:create_time到 Entity(createTime)-->
<setting name="mapUnderscoreToCamelCase" value="true"/>
<!-- 打印日志 -->
<setting name="logImpl" value="STDOUT_LOGGING" />
<!--<!–-->
<!--localCacheScope 取值有SESSION 和 STATEMENT。默认SESSION。-->
<!--SESSION情况下,会缓存一个会话中执行的所有查询。-->
<!--STATEMENT情况下,本地会话仅用在语句执行上,对相同的SqlSession的不同调用将不会共享数据-->
<!--–>-->
<!--<setting name="localCacheScope" value="SESSION"></setting>-->
<!-- 开启二级缓存。默认是false不开启 -->
<setting name="cacheEnabled" value="true"></setting>
<!--<setting name="lazyLoadingEnabled" value="true"></setting>-->
<!--<setting name="aggressiveLazyLoading" value="false"></setting>-->
</settings>
</configuration>
随后在Mapper类上加上注解 @CacheNamespace
@CacheNamespace
public interface UserMapper {
List<User> selectUser(int id);
}
看到注解开启,多次查询并再次从数据库中查询。
CacheNamespace 注解的属性 :
配置名 | 释义 |
eviction | 回收策略,默认LRU。 共有四种 -----------> LRU:最近最少使用策略,移除最长时间不被使用的对象; FIFO先进先出策略;SOFT : 软引用策略,移除基于垃圾回收期状态和软引用规则的对象; WEAK : 弱引用策略, 更积极的移除基于垃圾回收器状态和弱引用规则的对象 |
flushInterval | 缓存刷新间隔,单位毫秒,可为任意正整数。默认不设置,即不刷新,仅调用语句时刷新 |
size | 缓存数据,可以为任意正整数。默认1024 |
readWrite | 是否可读写,默认true。只读的缓存会返回给所有调用者相同的实例,因此这些对象不能被修改。 可读的缓存会返回缓存对象的拷贝(通过序列化) |
implementation | 缓存实现类型,默认org.apache.ibatis.cache.impl.PerpetualCache |
注:
- 使用二级缓存时,与查询结果映射的java对象必须实现
Serializable
接口。如果存在父类,其成员都需要实现序列化接口。因为二级缓存的介质不一定是内存,可能是硬盘或远程服务器,所以需要对缓存数据实现序列化和反序列化接口。 - 可在Mapper.xml文件中进行 类似@CacheNamespace 注解的配置,如下:
<?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.kingfish.dao.UserMapper">
<!-- 创建一个LRU缓存,每隔60s刷新,最大存储512个对象,返回对象被认为是只读的,配置二级缓存实现类型-->
<cache eviction="LRU" flushInterval="60000" size="512" readOnly="true" type="org.apache.ibatis.cache.impl.PerpetualCache"/>
<!-- useCache : 是否使用二级缓存,默认是true -->
<!-- flushCache : 是否清空二级缓存,默认是true -->
<select id="selectUser" resultType="com.kingfish.pojo.User" useCache="true">
select *
from `user`;
</select>
</mapper>
- useCache : 是否使用二级缓存,默认是true
flushCache : 是否清空二级缓存,默认是true
如:select不想被缓存时,可以添加select的属性useCache=“false”;insert、update和delete不想让他刷新缓存时,添加属性flushCache=”false ”。