A股上市公司传智教育(股票代码 003032)旗下技术交流社区北京昌平校区

 找回密码
 加入黑马

QQ登录

只需一步,快速开始

前言

主题是Mybatis一级和二级缓存的应用及源码分析。希望在本场chat结束后,能够帮助读者朋友明白以下三点。

  • Mybatis是什么。
  • Mybatis一级和二级缓存如何配置使用。
  • Mybatis一级和二级缓存的工作流程及源码分析。


本次分析中涉及到的代码和数据库表均放在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的学生的名字还是凯伦,出现了脏数据,也证明了我们之前就得到的结论,一级缓存只存在于只在数据库会话内部共享。

未完待续。。。。。。

更多图片 小图 大图
组图打开中,请稍候......

0 个回复

您需要登录后才可以回帖 登录 | 加入黑马