1 缘起
回顾SpringBoot如何进行事务管理相关知识的时,
发现使用Spring的注解@Transational即可实现事务管理,完成回滚操作,
然而SpringBoot中使用MyBatis这个ORM框架操作数据库,实现CURD,
这两者有什么关系呢?
研究一番之后,发现,@Transactional注解于MyBatis是有关联的,
并且不单单对事务起作用,同样对MyBatis一级缓存起作用,
于是,开始研究MyBatis的缓存,分享如在,
帮助读者轻松应对知识考核与交流。
2 MyBatis缓存
MyBatis提供两种缓存:一级缓存和二级缓存,
其中,生效条件:
序号 | 缓存级别 | 生效条件 |
---|---|---|
1 | 一级缓存 | 基于同一个SqlSession |
2 | 二级缓存 | 基于同一个namespace |
2.1 一级缓存:L1 Cache
MyBatis一级缓存结构如下图所示。由图可知,对于同一个SqlSession,
一级缓存生效。
MyBatis在SqlSession层次的数据查询顺序:L1级缓存->数据库
第一次创建或者开启SqlSession时,一级缓存没有数据,
缓存命中失败,查询请求会直接向数据发起,
查到数据后,数据会保存到一级缓存,
同一个SqlSession在相同查询条件下,会直接使用L1级缓存的数据,
提高了查询性能,但是,会出现脏数据。分布式条件下或者直接操作数据库,其他SqlSession变更了数据,此时,查到的数据仍是L1级缓存的数据。
2.1.1 L1级缓存生效
操作添加@Transactional事务注解,保证当前事务共用一套SqlSession,
从而达到L1级缓存生效的条件。
@Override
@Transactional(rollbackFor = Exception.class)
public BaseUserVO queryUserById(long id) {
BaseUserVO baseUserVO = userDAO.queryUserById(id);
BaseUserVO baseUserVO1 = userDAO.queryUserById(id);
BaseUserVO baseUserVO2 = userDAO.queryUserById(id);
return Optional.ofNullable(baseUserVO).orElse(new BaseUserVO());
}
同一个事务中的SqlSession是公用的,所以,保证了SqlSession一致性,
相同查询条件下,只要L1级缓存存在符合条件的数据,会直接使用。
测试结果日志信息如下图所示,由图可知,第一次查询,L1级缓存命中失败,
直接向数据库发起查询,查询语句如日志所示。由日志信息可知,SqlSession是同一个,地址为:37444756。
当前事务中查询语句执行结束后,SqlSession执行commit并取消注册,最后关闭SqlSession。
执行过程日志如下图所示。
2.1.2 源码分析:为什么添加@Transactional注解使L1级缓存生效
首先进入SqlSessionTemplate即SqlSession模板配置,用于构建SqlSession,交给Spring容器管理。源码如下图所示,
需要关注的是SqlSessionInterceptor,SqlSession拦截器。
位置:org.mybatis.spring.SqlSessionTemplate#SqlSessionTemplate(org.apache.ibatis.session.SqlSessionFactory, org.apache.ibatis.session.ExecutorType, org.springframework.dao.support.PersistenceExceptionTranslator)
SqlSession拦截器源码如下图所示,为MyBatis执行的CURD方法分配合适的SqlSession。在这个代理方法中,有两个部分需要关注:
(1)getSqlSession:获取SqlSession,这就是从事务中获取SqlSession,同一个事务的SqlSession是一致的,保证L1级缓存生效;
(2)sqlSession.commit:这一点是解释为什么不添加事务无法启用L1级缓存,因为,不启用事务,SqlSession就不是同一个,会触发SqlSession强制commit,细心的读者会发现,源码中(如下图)在sqlSession.commit上面有两行注释,解释了不使用事务无法使L1级缓存生效。
位置:org.mybatis.spring.SqlSessionTemplate.SqlSessionInterceptor
再来看一下getSqlSession,源码如下图所示。
由图可知,从事务管理器中获取SqlSessionHolder,然后获取执行的SqlSession,
如果在同一个事务中,则直接复用SqlSession,满足L1级缓存生效的条件。
位置:org.mybatis.spring.SqlSessionUtils#getSqlSession(org.apache.ibatis.session.SqlSessionFactory, org.apache.ibatis.session.ExecutorType, org.springframework.dao.support.PersistenceExceptionTranslator)
2.2 二级缓存:L2 Cache
二级缓存相对于一级缓存作用域更加宽泛,无需保证同一个SqlSession,只要是同一个namespace即可,即同一份映射文件:*.xml,
L2级缓存查询过程示意图如下图图所示。
由图可知,
2.2.1 L2级缓存生效
在映射的xml文件中添加
<cache></cache>
完整映射文件:
<?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.monkey.standalone.modules.user.dao.UserDAO">
<cache></cache>
<select id="queryUserById" resultType="com.monkey.standalone.modules.user.vo.BaseUserVO">
SELECT
id id,
user_id,
username,
sex
FROM
tb_user
WHERE
id = #{id};
</select>
</mapper>
执行查询:
@Override
public BaseUserVO queryUserById(long id) {
BaseUserVO baseUserVO = userDAO.queryUserById(id);
return Optional.ofNullable(baseUserVO).orElse(new BaseUserVO());
}
查询结果如下图所示。
由图可知,每次查询过程都会给出L2级缓存命中率,
图中标注了计算方式,以及每次查询的过程,
第一次查询时,L2级缓存没有数据,直接通过数据库查询,缓存命中率为0,
第二次查询,在同一个namespace中,走L2级缓存,命中率50%,1/2,
依次类推。
2.2.2 源码分析:为什么添加<cache></cache>
使L2级缓存生效
L2级缓存是基于映射文件的namespace生效的,
因此,首先进入映射文件元素配置,configurationElement,源码如下图所示,
由图可知,通过builderAssistant构建当前namespace,同时,配置cache使L2级缓存生效:cacheElement,接下来进入该方法,探究如何使L2级缓存生效。
位置:org.apache.ibatis.builder.xml.XMLMapperBuilder#configurationElement
L2级缓存生效的方法cacheElement源码如下图所示,
由图可知,通过builderAssistant.useNewCache配置同一namespace使用L2级缓存。
位置:org.apache.ibatis.builder.xml.XMLMapperBuilder#cacheElement
配置namespace使用L2级缓存源码如下图所示,
由图可知,通过currentNamespace构建缓存,并添加到配置中心configuration。
位置:org.apache.ibatis.builder.MapperBuilderAssistant#useNewCache
接下来进入configuration一探究竟,
confituration中的newExecutor源码如下图所示,构建执行器,
由图可知,当缓存开启时,构建缓存执行器,每次查询先从缓存遍历数据,
然后再看查询的策略。
位置:org.apache.ibatis.session.Configuration#newExecutor(org.apache.ibatis.transaction.Transaction, org.apache.ibatis.session.ExecutorType)
最终的查询方法源码如下图所示,
由图可知,执行查询时,先从L2级缓存获取数据,如果命中数据,则直接返回,
若未命中数据,则进一步通过L1级缓存或数据库获取数据。
位置:org.apache.ibatis.executor.CachingExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler, org.apache.ibatis.cache.CacheKey, org.apache.ibatis.mapping.BoundSql)
3 小结
(1)MyBatis一级缓存生效的条件为同一个SqlSession;二级缓存生效的条件为同一个namespace;
(2)一级缓存生效需要使用@Transactional注解,保证同一事务中的操作在同一个SqlSesion中,因为,MyBatis在设计时,不在同一事务中SqlSession会强制自动提交(commit),每次操作都会新建SqlSession,这样就无法保证所有操作在同一个SqlSession中,破坏了L1级缓存的生效条件;
(3)二级缓存生效的范围是同一份映射文件,即同一个namespace,映射文件配置是,会读取cache属性,当存在cache属性时,开启二级缓存,新建缓存执行器,该执行器保证,每次查询数据时,都会先从二级缓存查询;
(4)MyBatis使用缓存机制会出现脏数据问题,即分布式架构/体系中,不同的SqlSession或者namespace对数据变更后,其他的服务仍旧会从缓存中获取数据,导致数据更新不及时,出现脏数据。