前言
主题是Mybatis一级和二级缓存的应用及源码分析。希望在本场chat结束后,能够帮助读者朋友明白以下三点。
本次分析中涉及到的代码和数据库表均放在Github上,地址:
https://github.com/kailuncen/mybatis-cache-demo
目录
为达到以上三个目的,本文按照以下顺序展开。
Mybatis的基础概念。 一级缓存介绍及相关配置。 一级缓存工作流程及源码分析。 一级缓存总结。 二级缓存介绍及相关配置。 二级缓存源码分析。 二级缓存总结。 全文总结。
Mybatis的基础概念
本章节会对Mybatis进行大体的介绍,分为官方定义和核心组件介绍。
首先是Mybatis官方定义,如下所示。
MyBatis是支持定制化SQL、存储过程以及高级映射的优秀的持久层框架。MyBatis避免了几乎所有的JDBC代码和手动设置参数以及获取结果集。MyBatis可以对配置和原生Map使用简单的XML或注解,将接口和Java 的POJOs(Plain Old Java Objects,普通的 Java对象)映射成数据库中的记录。
其次是Mybatis的几个核心概念。
SqlSession : 代表和数据库的一次会话,向用户提供了操作数据库的方法。 MappedStatement: 代表要发往数据库执行的指令,可以理解为是Sql的抽象表示。 Executor: 具体用来和数据库交互的执行器,接受MappedStatement作为参数。 映射接口: 在接口中会要执行的Sql用一个方法来表示,具体的Sql写在映射文件中。 映射文件: 可以理解为是Mybatis编写Sql的地方,通常来说每一张单表都会对应着一个映射文件,在该文件中会定义Sql语句入参和出参的形式。
下图就是一个针对Student表操作的接口文件StudentMapper,在StudentMapper中,我们可以若干方法,这个方法背后就是代表着要执行的Sql的意义。
通常也可以把涉及多表查询的方法定义在StudentMapper中,如果查询的主体仍然是Student表的信息。也可以将涉及多表查询的语句单独抽出一个独立的接口文件。
在定义完接口文件后,我们会开发一个Sql映射文件,主要由mapper元素和select|insert|update|delete元素构成,如下图所示。
mapper元素代表这个文件是一个映射文件,使用namespace和具体的映射接口绑定起来,namespace的值就是这个接口的全限定类名。select|insert|update|delete代表的是Sql语句,映射接口中定义的每一个方法也会和映射文件中的语句通过id的方式绑定起来,方法名就是语句的id,同时会定义语句的入参和出参,用于完成和Java对象之间的转换。
在Mybatis初始化的时候,每一个语句都会使用对应的MappedStatement代表,使用namespace+语句本身的id来代表这个语句。如下代码所示,使用mapper.StudentMapper.getStudentById代表其对应的Sql。
SELECT id,name,age FROM student WHERE id = #{id}
在Mybatis执行时,会进入对应接口的方法,通过类名加上方法名的组合生成id,找到需要的MappedStatement,交给执行器使用。 至此,Mybatis的基础概念介绍完毕。
一级缓存
一级缓存介绍
在系统代码的运行中,我们可能会在一个数据库会话中,执行多次查询条件完全相同的Sql,鉴于日常应用的大部分场景都是读多写少,这重复的查询会带来一定的网络开销,同时select查询的量比较大的话,对数据库的性能是有比较大的影响的。
如果是Mysql数据库的话,在服务端和Jdbc端都开启预编译支持的话,可以在本地JVM端缓存Statement,可以在Mysql服务端直接执行Sql,省去编译Sql的步骤,但也无法避免和数据库之间的重复交互。关于Jdbc和Mysql预编译缓存的事情,可以看我的这篇博客JDBC和Mysql那些事。
https://my.oschina.net/kailuncen/blog/905395
Mybatis提供了一级缓存的方案来优化在数据库会话间重复查询的问题。实现的方式是每一个SqlSession中都持有了自己的缓存,一种是SESSION级别,即在一个Mybatis会话中执行的所有语句,都会共享这一个缓存。一种是STATEMENT级别,可以理解为缓存只对当前执行的这一个statement有效。如果用一张图来代表一级查询的查询过程的话,可以用下图表示。
每一个SqlSession中持有了自己的Executor,每一个Executor中有一个Local Cache。当用户发起查询时,Mybatis会根据当前执行的MappedStatement生成一个key,去Local Cache中查询,如果缓存命中的话,返回。如果缓存没有命中的话,则写入Local Cache,最后返回结果给用户。
一级缓存配置
上文介绍了一级缓存的实现方式,解决了什么问题。在这个章节,我们学习如何使用Mybatis的一级缓存。只需要在Mybatis的配置文件中,添加如下语句,就可以使用一级缓存。共有两个选项,SESSION或者STATEMENT,默认是SESSION级别。
<setting name="localCacheScope" value="SESSION"/>
一级缓存实验
配置完毕后,通过实验的方式了解Mybatis一级缓存的效果。每一个单元测试后都请恢复被修改的数据。
首先是创建了一个示例表student,为其创建了对应的POJO类和增改的方法,具体可以在entity包和Mapper包中查看。
CREATE TABLE `student` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(200) COLLATE utf8_bin DEFAULT NULL, `age` tinyint(3) unsigned DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
在以下实验中,id为1的学生名称是凯伦。
实验1
开启一级缓存,范围为会话级别,调用三次getStudentById,代码如下所示: [Java] 纯文本查看 复制代码
public void getStudentById() throws Exception {
SqlSession sqlSession = factory.openSession(true); // 自动提交事务
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
System.out.println(studentMapper.getStudentById(1));
System.out.println(studentMapper.getStudentById(1));
System.out.println(studentMapper.getStudentById(1));
}
执行结果:
我们可以看到,只有第一次真正查询了数据库,后续的查询使用了一级缓存。
实验2
在这次的试验中,我们增加了对数据库的修改操作,验证在一次数据库会话中,对数据库发生了修改操作,一级缓存是否会失效。 [Java] 纯文本查看 复制代码
@Test
public void addStudent() throws Exception {
SqlSession sqlSession = factory.openSession(true); // 自动提交事务
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
System.out.println(studentMapper.getStudentById(1));
System.out.println("增加了" + studentMapper.addStudent(buildStudent()) + "个学生");
System.out.println(studentMapper.getStudentById(1));
sqlSession.close();
}
执行结果:
我们可以看到,在修改操作后执行的相同查询,查询了数据库,一级缓存失效。
实验3
开启两个SqlSession,在sqlSession1中查询数据,使一级缓存生效,在sqlSession2中更新数据库,验证一级缓存只在数据库会话内部共享。 [Java] 纯文本查看 复制代码
@Test
public void testLocalCacheScope() throws Exception {
SqlSession sqlSession1 = factory.openSession(true);
SqlSession sqlSession2 = factory.openSession(true);
StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
System.out.println("studentMapper读取数据: " + studentMapper.getStudentById(1));
System.out.println("studentMapper读取数据: " + studentMapper.getStudentById(1));
System.out.println("studentMapper2更新了" + studentMapper2.updateStudentName("小岑",1) + "个学生的数据");
System.out.println("studentMapper读取数据: " + studentMapper.getStudentById(1));
System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentById(1));
}
我们可以看到,sqlSession2更新了id为1的学生的姓名,从凯伦改为了小岑,但session1之后的查询中,id为1的学生的名字还是凯伦,出现了脏数据,也证明了我们之前就得到的结论,一级缓存只存在于只在数据库会话内部共享。
未完待续。。。。。。
|