前言
首先我个人是不怎么使用MyBatis的,原因是我觉得他有些鸡肋。扩展性赶不上原生的JdbcTemplate、ORM赶不上原生Spring-Data-JPA,整个一级缓存、二级缓存
在分布式系统中还有脏读的可能。但是MyBatis也有很多的优点,例如Sql与业务的隔离、结构化的Sql编写等,也有很多人在用,不失为一款非常优秀的持久层框架。
缓存(Cache),原始意义是指访问速度比一般随机存取寄存器(RAM)快的一种高速存储器,在计算机世界中无处不在。CPU的三级缓存、操作系统的缓存、数据库的
缓存、浏览器的缓存等等,很多组件以及编程中的中间件都有缓存。java常用持久层框架JdbcTemplate、Mybatis(Ibatis)、TopLink、Spring-data-Jpa(Hibernate)等,
今天总结的是持久层框架Mybatis的缓存。
这里对Jpa备注一下:JPA Java Persistence API,Java持久化API是Sun公司在Java EE 5规范中提出的Java持久化接口,它定义一系列的注释,Hibernate是JPA规范
的一种实现,两者并不是等号关系!Spring-data-Jpa代表的意思是Spring框架对JPA的整合。
MyBatis缓存
像周彦伟前辈说的那样,在源码面前,任何的总结经验和管用习惯都显得非常苍白无力,源码才是揭开真想的唯一途径,即便是官方发布文档,也不可避免地有这样那样的
疏漏或者跟现有版本不同之处。案例使用MyBatis版本是3.5.3,下面从源码层面看下MyBatis的缓存是怎么玩的。
MyBatis的缓存类在org.apache.ibatis.cache的包下面(MyBatis 本是apache的一个开源项目iBatis, 2010年这个项目由apache software foundation
迁移到了google code,并且改名为MyBatis。他们不仅仅是名字上的区别,iBatis封装了绝大多数JDBC模板,MyBatis提供了更为强大的功能),根接口Cache只有
一个实现类PerpetualCache如下图1:
图1
缓存其实是由HashMap来实现的如图2:
图2
如图3所示cache包下有个特殊的包decorators,其中提供了很多装饰器。都有组合Cache接口,随便打开一个可以看到,最终都是调用了delegate来实现的的,只是
将部分功能做了加强。但是Cache接口的实现只有PerpetualCache,所以装饰器内部使用的实际上是PerpetualCache的对象,因为接口是无法实例化的。
Mybatis中存在两种缓存,一级缓存和二级缓存。
图3
一级缓存
一级缓存又叫本地缓存,是在会话(SqlSession)层面的实现,就是说一级缓存的作用范围只能在同一个SqlSession中,跨SqlSession是无效的。
验证一级缓存
文档接口如图4所示:
图4
mybatis-config.xml1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<configuration>
<!-- 设置驼峰匹配 -->
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
<!-- 配置环境:可以配置多个环境,default:配置某一个环境的唯一标识,表示默认使用哪个环境 -->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<!-- 配置连接信息 -->
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/easy_pool_demo"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
<!-- 配置映射文件:用来配置sql语句和结果集类型等 -->
<mappers>
<mapper resource="mapper/StudentMapper.xml"/>
</mappers>
</configuration>
StudentMapper.xml1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<mapper namespace="com.xieahui.easy.mybatis.dao.StudentMapper">
<resultMap id="BaseResultMap" type="com.xieahui.easy.mybatis.entity.EasyPoolDemoStudentEntity">
<id column="id" jdbcType="BIGINT" property="id"/>
<result column="name" jdbcType="VARCHAR" property="name"/>
<result column="sex" jdbcType="VARCHAR" property="sex"/>
</resultMap>
<sql id="Base_Column_List">
id, `name`, sex
</sql>
<select id="findAll" resultMap="BaseResultMap">
select
<include refid="Base_Column_List"/>
from student
</select>
<insert id="save" keyColumn="id" keyProperty="id"
parameterType="com.xieahui.easy.mybatis.entity.EasyPoolDemoStudentEntity" useGeneratedKeys="true">
insert into student (`name`, sex)
values (#{name,jdbcType=VARCHAR}, #{sex,jdbcType=VARCHAR})
</insert>
</mapper>
假设一级缓存存在并默认开启,那么两次同样的查询调用同一个SqlSession对象,是不会出现两个执行语句的!
MyClass.java1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40package com.xieahui.easy.mybatis;
import com.xieahui.easy.mybatis.dao.StudentMapper;
import com.xieahui.easy.mybatis.entity.EasyPoolDemoStudentEntity;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
/**
* TODO
*
* @author xiehui1956@gmail.com on 2020/12/27 4:05 下午
* @version 1.0.0
*/
public class MyClass {
public static void main(String[] args) throws IOException {
String resource = "mybatis-config.xml";
// 读取配置文件
InputStream inputStream = Resources.getResourceAsStream(resource);
// 常见SqlSessionFactory对象
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 创建SqlSession对象
SqlSession sqlSession1 = sqlSessionFactory.openSession();
// SqlSession sqlSession2 = sqlSessionFactory.openSession();
StudentMapper studentMapper1 = sqlSession1.getMapper(StudentMapper.class);
// StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
List<EasyPoolDemoStudentEntity> list1 = studentMapper1.findAll();
List<EasyPoolDemoStudentEntity> list2 = studentMapper1.findAll();
}
}
执行结果,如图5:
图5
两次查询只有一个sql执行语句,说明假设是基本成立的。
为了再次确认上述结果,假设使用两个SqlSession,那么执行sql应该是要打印两次的。
MyClass.java
1 | package com.xieahui.easy.mybatis; |
图6
执行结果如图6,所以上述结论是成立的。
原理分析
由于一级缓存的作用是SqlSession,那么一级缓存的数据是存储在哪里的呢?很容易可以看到SqlSession接口的实现类只有DefaultSqlSession、SqlSessionManager两
个,他们和SqlSession之间的关系如下图7、图8所示。
图7
图8
实际上SqlSession的实现类算是只有DefaultSqlSession这一个,另外一个是用工厂模式对回话的管理类,进入DefaultSqlSession的查询方法,然后查看对应的
执行逻辑在BaseExecutor.java中可以看出以及缓存的创建逻辑,如下图9所示:
图9
下面进一步分析,缓存的生命周期,即缓存的创建、使用和销毁。
由上图9可以看出,缓存的key是在执行查询的时候创建的。缓存key主要组成有6个部分:
- 将Statement中的id添加到CacheKey对象中的updateList属性;
- 将offset(分页偏移量)添加到CacheKey对象中的updateList属性(如果没有分页则默认0);
- 将limit(每页显示的条数)添加到CacheKey对象中的updateList属性(如果没有分页则默认Integer.MAX_VALUE);
- 将sql语句添加到CacheKey对应中的updateList属性;
- 将用户参数循环添加到CacheKey对象中的updateList属性;
- 如果配置Environment,将Environment中的id添加到CacheKey对象中的updateList属性。
创建key后带着key继续执行查询操作,代码如图10:
图10
可以看到缓存的使用时机、以及加载入口,代码如图11:
图11
销毁主要有三个地方:
- 获取缓存之前会先进行判断用户是否配置了flushCache=true属性,如果配置了则会清除一级缓存;
- 是否全局配置属性localCacheScope为Statement,如果配置那么完成一次查询就会清除缓存;
- 在执行commit、rollback、update方法时会清空一级缓存。这些代码根据上面提到localCache返回可以在源码中看到明显的关联逻辑,这里就不展示了。
二级缓存
一级缓存作用域是SqlSession,二级缓存目的是扩展一级缓存的作用范围,实现夸会话缓存。在MyBatis中二级缓存作用域是namespace,也就是作用范围是命名空间。
在MyBatis中为了实现二级缓存,专门设计了一个叫做CachingExecutor的装饰器。
配置二级缓存
StudentMapper.xml
1 |
|
这里修改两个地方,一个是新增了
MyClass.java1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42package com.xieahui.easy.mybatis;
import com.xieahui.easy.mybatis.dao.StudentMapper;
import com.xieahui.easy.mybatis.entity.EasyPoolDemoStudentEntity;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
/**
* TODO
*
* @author xiehui1956@gmail.com on 2020/12/27 4:05 下午
* @version 1.0.0
*/
public class MyClass {
public static void main(String[] args) throws IOException {
String resource = "mybatis-config.xml";
// 读取配置文件
InputStream inputStream = Resources.getResourceAsStream(resource);
// 常见SqlSessionFactory对象
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 创建SqlSession对象
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
StudentMapper studentMapper1 = sqlSession1.getMapper(StudentMapper.class);
StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
List<EasyPoolDemoStudentEntity> list1 = studentMapper1.findAll();
sqlSession1.commit();
List<EasyPoolDemoStudentEntity> list2 = studentMapper2.findAll();
}
}
执行结果,如图12所示:
图12
说我没有给实体实现序列化接口,添加序列化接口后的执行结果,如图13所示:
图13
虽然夸了SqlSession,但是仍然只有一条sql打印出来了。开启二级缓存总结:
- 需要commit事务之后才会生效;
- 默认缓存,结果集对象需要实现序列化接口。
二级缓存原理
二级缓存是通过CachingExecutor对象来实现的,看看CachingExecutor类的结构,如图14所示:
图14
其中TransactionalCacheManager是用来管理二级缓存的,可以继续进入TransactionalCache,如图15所示:
图15
其中entriesToAddOnCommit存放的是临时的二级缓存数据,因为事务是有回滚操作的。为了避免脏读,所以不会把数据直接放到二级缓存中。二是通过执行commit后
将数据提交到二级缓存,如果事务回滚、则将数据清除,如图16所示:
图16
自定义缓存
在分布式系统中一级缓存和二级缓存都有存在脏读的可能,所以在分布式系统中我们尽量关闭掉MyBatis的缓存,可以使用Redis中间件来存储。官方有提供Redis作为
缓存的支持,引入Redis后只需要将MyBatis配置文件中Cache 的类型定义为RedisCache,然后加上Redis的配置即可。验证配置是否生效可以断点查看,包装实现。
如下图默认包装实现图17,Redis包装实现图18:
图17
图18
以上,欢迎沟通交流。