mybatis_cache系列
建议按顺序阅读,有一些代码沿用之前的code,与一级缓存完全一致的内容或结果就不再操作了
前言
本文主要阐述mybatis一级缓存如果使用,命中规则介绍及缓存生命周期。
最后再从源码刨析缓存创建销毁的底层实现。
先贴一下基础代码
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.954l.blog</groupId>
<artifactId>mybatisCache</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.6</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.38</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.58</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
mybatis.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>
<settings>
<setting name="logImpl" value="STDOUT_LOGGING" />
</settings>
<environments default="dev">
<environment id="dev">
<transactionManager type="JDBC"></transactionManager>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql:///blog_mybatis_cache"/>
<property name="username" value="root"/>
<property name="password" value="password"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mapper/UserMapper.xml"/>
</mappers>
</configuration>
blog_mybatis_cache.sql
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`password` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4;
INSERT INTO `user` VALUES ('1', '张三', '123');
INSERT INTO `user` VALUES ('2', '李四', '321');
INSERT INTO `user` VALUES ('3', '王五', '456');
代码结构
Coding
由于mybatis一级缓存默认开启,我们先通过testDemo看一下一级缓存的效果
@Test
public void testOne() throws IOException {
InputStream inputStream = Resources.getResourceAsStream("mybatis.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.queryById(1);
log.info(JSONObject.toJSONString(user));
User user1 = mapper.queryById(1);
log.info(JSONObject.toJSONString(user1));
}
通过配置的打印mybatis的执行sql,可以看到我们代码里查询了两次db,但是控制台执行sql只打印了一次。
猜测第二次查询是从一级缓存中获取的,而没有再去从db查询。
这些都会在文章的最后通过跟源码来验证我们的猜测。
一级缓存命中流程
缓存命中规则
一般我们遇到能影响一级缓存命中的因素主要有以下两点
- sql不同
- statementId不同
sql不同
何为sql不同,不用我解释了把,直接来看效果
当第二次查询的时候,sql语句加了一个order id,可以看到尽管查询结果完全一致,不同sql,也无法命中缓存
statementId不同
statementId指的是我们mapper.xml文件对应的id
不同id的情况下,即使sql一致,也无法命中缓存
可以看到sql查询语句打印了两次,即使我们的执行sql完全一致,它也无法命中一级缓存
缓存生命周期
生命周期顾名思义就是缓存何时创建,缓存的作用域,缓存何时会销毁,以及遇到什么情况会销毁
何时创建
当sqlSession执行select时会创建一级缓存
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.queryById(1);
这是最上面的第一个例子,当我们通过反射拿到Mapper接口之后,
mybatis通过判断我们mapper接口的返回值,来确定如何执行,当执行的是select后就会创建一级缓存
详细可参考这篇博文,我觉得写的不错:小白mybatis源码看这一遍就够了(2)| getMapper与sqlSession.selectList区别
缓存的作用范围
一级缓存只在同一个sqlsession的前提下才有效,如果两个不同的sqlsession,不管什么情况都不会命中缓存
这里我open了两个sqlsession,可以看到执行sql语句返回结果集完全一致,但是没有命中缓存
何时销毁,以及什么情况会销毁
- session关闭
- session提交
- session回滚
- 执行update/insert/delete语句
- 手动销毁
session关闭
在第一次查询后,执行了sqlSession.close();来关闭session,但我们执行相同的sql也无法命中缓存,这个其实跟上面提到的不同的sqlsession无法共享一级缓存差不多意思,因为我们close之后无法再使用第一个sqlsession了。
session提交/session回滚
回滚也是同样的效果,相同代码懒得写了,同学可以自己试验一下
在第一次查询之后执行了一次commit,再次查询就无法命中缓存了,可以看到commit也是销毁了我们的一级缓存。
多说一句,这里我们是手动commit,在真实开发中,于spring整合之后,我们在serviceImpl层进行sql操作,如果没有配置事务AOP或者声明式事务的话,在每次执行sql之后都会自动commit,也就是在没有事务的函数里,每执行一条sql都会清空我们的缓存。
再多说一句!在没有事务的函数里,当一条sql里的子查询可以使用一级缓存,也就是我们form我们子查询,这个子查询只会查询一次,下一次是从一级缓存中命中的,join同样也会命中缓存。
执行update/insert/delete语句
这里就不挨个试了,因为mybatis不管你是insert还是delete,最后其实都是执行的update。
贴一下源码吧:
update
这里修改特意执行的id为2的数据,更进一步说明修改任何数据都会销毁一级缓存,只要执行的是update。
手动销毁
mybatis提供了一个手动销毁一级缓存的函数,执行clearCache后即可清空sqlsession内所有一级缓存
源码看一级缓存
这里就执行用sqlsession执行的方式看源码了,因为反射获取mapper执行的方式兜兜转转之后还是执行sqlsession.select。
缓存创建
执行到这一步时候,sqlsession的实现是DefaultSqlSession
DefaultSqlSession里selectOne实际走的是selectList
第一步这里跟缓存没多大关系,就是把mapper.xml文件转成MappedStatement对象。
主要看下面的查询,根据debug可以看到走的是CachingExecutor实现类
再点下一步就看到了一直在找的关键性代码,这个命名猜都猜得到做了啥事,根据查询的参数及sql以及分页信息去生成一个一级缓存的key,这里先看生成key的代码,之后还得返回来看下一步return的query,一级缓存是否命中都在里头
进来之后可以看到是一个delegate对象,这个对象是Executor接口,这个接口的实现走的是SimpeExecutor,接着下一步。
但是!下一步发现没有SimpeExecutor的实现
查看源码发现这simpleExecutor继承BaseExecutor,那在刚刚那上面也可以看到BaseExecutor中有createCacheKey的实现,那这就是javase的基础了,这里走的是父类的实现,我们进BaseExecutor找到createCacheKey继续下一步
这里面就是一级缓存的key的生成规则,通过每一个update去更新这个key,可以看到参与更新的有statementId,两个分页参数,一个sql语句,再下面是遍历我们所有的sql参数,然后进行更新这个key,最后拿了mybatis的环境去更新了一下这个key。
这些代码就可以说明我们最上面的命中规则,除了列的主要这两点以外还有分页参数跟环境的不同也会导致一级缓存无法命中。
源码调用过程
好了,这里看完接着翻上去看创建key的下一步
这里有一个关键性的单词,localCache本地缓存,先不说它是不是缓存,我们看它下一步的代码逻辑。
如果list == null,它就queryFromDatabase,这个命名也太明白了,如果list为空就去数据库查询。
根据这步操作我们就能知道localCache里存的就是我们的缓存数据,我们进去localCache里看看它是啥
这回真相大白了!这鬼东西神神秘秘,最后这个HashMap原来就是我们的一级缓存。
最后再去看一下刚刚那个如果list为null执行的函数体,就是查询数据库的操作,可以看到里面有这么一行代码,就是把查询的结果集put到HashMap中。这回就都通了…
源码调用过程
缓存销毁
刚刚我们提到了缓存什么时候销毁,以及什么情况会销毁,接下来就跟下源码看看它是如何销毁的。
session关闭
第一步先进DefaultSqlSession的实现
接着下一步再进executor的实现类:CacheingExecutor
这里判断了一下是否回滚还是提交,但都不是我们关心的,我们看finallly里的close,这里又遇到SimpleExecutor,在之前我们已经知道它是BaseExecutor的子类,这里close它也没有,就不贴代码了,还是走的父类的close代码
这回关键的代码浮出水面,从刚刚创建的源码中得知这个localCache就是我们的一级缓存实现,这里把它赋值为null,这不是清空缓存那是什么?
session提交
跟上面一样的代码步骤就不贴了,都是走一样的实现类,可以看到最后这里也是clearLocalCache,清空了一级缓存
session回滚
跟提交同理
执行update/insert/delete
可以看到是先清空了一级缓存,然后再去修改数据库
手动销毁
这个就更没什么可说的了,这源码很明白