mybatis一级缓存机制(含源码阅读)


  • 0x01_一级缓存演示

  • 0x02_一级缓存的配置

  • 0x03_一级缓存机制何时失效?

  • 0x04_一级缓存机制导致的脏数据问题

  • 0x05_一级缓存机制的工作流程

  • 0x06_一级缓存机制的源码分析

  • 0x07_一级缓存总结


Mybatis一级缓存(含源码阅读)

本篇涉及到源码的阅读,这里提供mybatis3.5.11的API:https://mybatis.org/mybatis-3/apidocs/index.html

MyBatis是常见的Java数据库访问层框架。在日常工作中,开发人员多数情况下是使用MyBatis的默认缓存配置,但是MyBatis缓存机制有一些不足之处,在使用中容易引起脏数据,形成一些潜在的隐患。

缓存机制是一种临时存储少量数据至内存或者是磁盘的一种技术.减少数据的加载次数,可以降低工作量,提高程序响应速度

缓存的重要性是不言而喻的。mybatis的缓存将相同查询条件的SQL语句执行一遍后所得到的结果存在内存或者某种缓存介质当中,当下次遇到一模一样的查询SQL时不再执行SQL与数据库交互,而是直接从缓存中获取结果,减少服务器的压力;尤其是在查询越多、缓存命中率越高的情况下,使用缓存对性能的提高更明显。

MyBatis允许使用缓存,缓存一般放置在高速读/写的存储器上,比如服务器的内存,能够有效的提供系统性能。MyBatis分为一级缓存和二级缓存,同时也可配置关于缓存设置。

一级存储是SqlSession上的缓存,二级缓存是在SqlSessionFactory(namespace)上的缓存。默认情况下,MyBatis开启一级缓存,没有开启二级缓存。当数据量大的时候可以借助一些第三方缓存框架或Redis缓存来协助保存Mybatis的二级缓存数据。

下面记录自己现阶段学习的mybatis缓存。

0x01_一级缓存演示

在应用运行过程中,我们有可能在一次数据库会话中,执行多次查询条件完全相同的SQL,MyBatis提供了一级缓存的方案优化这部分场景,如果是相同的SQL语句,会优先命中一级缓存,避免直接对数据库进行查询,提高性能。具体执行过程如下图所示。

mybatis一级缓存机制(含源码阅读)
image-20221018120451477

每个SqlSession中持有了Executor,每个Executor中有一个LocalCache。当用户发起查询时,MyBatis根据当前执行的语句生成MappedStatement,在Local Cache进行查询,如果缓存命中的话,直接返回结果给用户,如果缓存没有命中的话,查询数据库,结果写入Local Cache,最后返回结果给用户。

下面以一个具体的案例说明:现在对于dept表根据员工编号进行查询,接口中的抽象方法如下:

Dept queryByDeptno(int deptno);

Mapper映射文件中相关的语句:

<sql id="deptColumns">
deptno,dname,loc
</sql>

......

<!-- Dept queryByDeptno(int deptno);-->
<select id="queryByDeptno" resultType="dept">
select <include refid="deptColumns"/> from dept where deptno = #{deptno}
</select>

在进行测试的时候:如果是同一个SqlSession生成的不同的mapper对象,但是如果查询的是同一个namespace下面的同一个sql语句,并且传入参数一样的话,只会访问数据库一次:(namespace+sql语句的id+参数)

@Test
public void testQueryByDeptno(){
DeptMapper mapper = sqlSession.getMapper(DeptMapper.class);
Dept dept = mapper.queryByDeptno(30);


DeptMapper mapper1 = sqlSession.getMapper(DeptMapper.class);
Dept dept1 = mapper1.queryByDeptno(30);

System.out.println("mapper==mapper1:"+(mapper==mapper1));
System.out.println("dept==dept1:"+(dept==dept1));
}

测试结果:

mybatis一级缓存机制(含源码阅读)
image-20221018121435146

可以看到sql语句只对于数据库查询了1次,mapper对象是不一样的,但是查询得到的dept对象是同一个(内存中地址是同一个)。

如果是不同的SqlSession去查询:

@Test
public void testQueryByDeptno(){
SqlSession sqlSession1 = sqlSessionFactory.openSession();
DeptMapper mapper = sqlSession1.getMapper(DeptMapper.class);
Dept dept = mapper.queryByDeptno(30);


SqlSession sqlSession2 = sqlSessionFactory.openSession();
DeptMapper mapper1 = sqlSession2.getMapper(DeptMapper.class);
Dept dept1 = mapper1.queryByDeptno(30);

System.out.println("mapper==mapper1:"+(mapper==mapper1));
System.out.println("dept==dept1:"+(dept==dept1));
}

测试结果:

![image-20221018121912147](/Users/apple/Library/Application Support/typora-user-images/image-20221018121912147.png)

同样的sql语句执行了2次,得到的dept对象在内存中也是不同的。

所以可以得出结论:

一级存储是SqlSession上的缓存,默认开启,是一种内存型缓存,不要求实体类对象实现Serializable接口。

缓存中的数据使用键值对形式存储数据

键值对是指:namespace+sqlid+args+offset>>> hash值作为键,查询出的结果作为值(实际是5个值,0x06_一级缓存机制的源码分析中有说明)

0x02_一级缓存的配置

对于上面的实验,并没有去有意识地开启mybatis一级缓存,因为mybatis是默认开启的。

开发者只需在MyBatis的配置文件中,添加如下语句,就可以使用一级缓存。共有两个选项,SESSION或者STATEMENT,默认是SESSION级别,即在一个MyBatis会话中执行的所有语句,都会共享这一个缓存。一种是STATEMENT级别,可以理解为缓存只对当前执行的这一个Statement有效。(建议将参数改为Statement,否则容易出现脏数据问题)

<setting name="localCacheScope" value="SESSION"/>

0x03_一级缓存机制何时失效?

在增删改(insert/delete/update)以及提交事务(commit)之后,会清空一级缓存,导致一级缓存失效。(为什么在读源码的时候会说明)

0x04_一级缓存机制导致的脏数据问题

下面以一个小实验,说明一级缓存只在数据库会话内部共享导致的读取脏数据的问题

@Test
public void testdirty(){
//开启两个SqlSession,并且设置提交事务
SqlSession sqlSession1 = sqlSessionFactory.openSession(true);
SqlSession sqlSession2 = sqlSessionFactory.openSession(true);

//在sqlSession1中查询数据,使一级缓存生效
DeptMapper mapper1 = sqlSession1.getMapper(DeptMapper.class);
Dept dept = mapper1.queryByDeptno(44);
System.out.println("mapper1读取数据:"+dept);

//在sqlSession2中更改数据
DeptMapper mapper2 = sqlSession2.getMapper(DeptMapper.class);
System.out.println("mapper2更新了"+mapper2.updateDept(new Dept(44, "后勤部", "浙江省嘉兴市"))+"条数据");

//再次在sqlSession1中查询数据
Dept dept1 = mapper1.queryByDeptno(44);
System.out.println("mapper1再次读取数据:"+dept1);

//sqlSession2中读取数据
Dept dept2 = mapper2.queryByDeptno(44);
System.out.println("mapper2读取数据"+dept2);
}

测试结果:

mybatis一级缓存机制(含源码阅读)
image-20221018124433224

以上实验证明了:一级缓存只在数据库会话内部共享—》由此便会带来脏数据问题。

0x05_一级缓存机制的工作流程

一级缓存执行的时序图,如下图所示。

mybatis一级缓存机制(含源码阅读)
一级缓存机制的工作流程

0x06_一级缓存机制的源码分析

接下来将对MyBatis查询相关的核心类和一级缓存的源码进行走读。这对后面学习二级缓存也有帮助。

SqlSession: 对外提供了用户和数据库之间交互需要的所有方法,隐藏了底层的细节。默认实现类是DefaultSqlSessionSqlSession 类似于一个 JDBC Connection 对象

mybatis一级缓存机制(含源码阅读)

ExecutorSqlSession向用户提供操作数据库的方法,但和数据库操作有关的职责都会委托给Executor。

mybatis一级缓存机制(含源码阅读)
image-20221018125455492

如下图所示,Executor有若干个实现类,为Executor赋予了不同的能力,大家可以根据类名,自行学习每个类的基本作用。

![image-20221018130839316](/Users/apple/Library/Application Support/typora-user-images/image-20221018130839316.png)

(6个实现类)

mybatis一级缓存机制(含源码阅读)
image-20221018130928543

SqlSession 执行 SQL的业务逻辑,都是委托给了 Executor 来实现。Executor 相关的类主要是用来执行 SQL。

其中,Executor 本身是一个接口;BaseExecutor 是一个抽象类,实现了 Executor 接口;而 BatchExecutor、SimpleExecutor、ReuseExecutor 三个类继承 BaseExecutor 抽象类。

下面不细说这7个类,以后有空再细读。

在一级缓存的源码分析中,主要学习BaseExecutor的内部实现。

mybatis一级缓存机制(含源码阅读)
image-20221018131736378

BaseExecutorBaseExecutor是一个实现了Executor接口的抽象类,定义若干抽象方法,在执行的时候,把具体的操作委托给子类进行执行。

mybatis一级缓存机制(含源码阅读)
image-20221018131835723
mybatis一级缓存机制(含源码阅读)
image-20221018132009249

在一级缓存的介绍中提到对Local Cache的查询和写入是在Executor内部完成的。在阅读BaseExecutor的代码后发现Local CacheBaseExecutor内部的一个成员变量,如下代码所示。

mybatis一级缓存机制(含源码阅读)
image-20221018131917927

Cache: MyBatis中的Cache接口,提供了和缓存相关的最基本的操作,如下图所示:

mybatis一级缓存机制(含源码阅读)

有若干个实现类,使用装饰器模式互相组装,提供丰富的操控缓存的能力,部分实现类如下图所示:

mybatis一级缓存机制(含源码阅读)
image-20221018132219903

mybatis一级缓存机制(含源码阅读)

BaseExecutor成员变量之一的PerpetualCache,是对Cache接口最基本的实现,其实现非常简单,内部持有HashMap,对一级缓存的操作实则是对HashMap的操作。如下代码所示:

mybatis一级缓存机制(含源码阅读)
image-20221018132451336
mybatis一级缓存机制(含源码阅读)
image-20221018132556881

补充:

在 MyBatis 中,缓存功能由接口 Cache 定义PerpetualCache 类是最基础的缓存类,是一个大小无限的缓存。除此之外,MyBatis 还设计了 9 个包裹 PerpetualCache 类的装饰器类,用来实现功能增强。它们分别是:FifoCache、LoggingCache、LruCache、ScheduledCache、SerializedCache、SoftCache、SynchronizedCache、WeakCache、TransactionalCache。

之所以 MyBatis 采用装饰器模式来实现缓存功能,是因为装饰器模式采用了组合,而非继承,更加灵活,能够有效地避免继承关系的组合爆炸。

在阅读相关核心类代码后,从源代码层面对一级缓存工作中涉及到的相关代码,出于篇幅的考虑,对源码做适当删减,读者朋友可以结合本文,后续进行更详细的学习。

为执行和数据库的交互,首先需要初始化SqlSession,通过DefaultSqlSessionFactory开启SqlSession

mybatis一级缓存机制(含源码阅读)
image-20221018132817996

注意:SqlSessionFactory 是一个接口,DefaultSqlSessionFactory 是它的实现类之一。

mybatis一级缓存机制(含源码阅读)

DefaultSqlSessionFactory类中有一个openSessionFromDataSource 方法,该方法中创建了Executor对象

mybatis一级缓存机制(含源码阅读)
image-20221018133014216

所以说  SqlSessionFactory创建的SqlSession向用户提供操作数据库的方法,但和数据库操作有关的职责都会委托给Executor(上面的对于数据源的组件,对这个功能进行了封装)。


在初始化SqlSesion时,会使用Configuration类创建一个全新的Executor,作为DefaultSqlSession构造函数的参数,创建Executor代码如下所示:(点进上图的newExecutor方法进行查看)

mybatis一级缓存机制(含源码阅读)
image-20221018133708294

Executor,CachingExecutor,BaseExecutor这3个类以及BaseExecutor的子类的关系如下:

mybatis一级缓存机制(含源码阅读)
image-20221018134755363

上面代码中:

        if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}

最终executor会被赋值成BatchExecutor,ReuseExecutor,SimpleExecutor这三个类中的其中一个类,这个三个类都继承自BaseExecutor,所以说如果二级缓存开关开启的话,是使用CahingExecutor装饰BaseExecutor的子类

        if (this.cacheEnabled) {
executor = new CachingExecutor((Executor)executor);
}

SqlSession创建完毕后,根据Statment的不同类型,会进入SqlSession的不同方法中,如果是Select语句的话,最后会执行到SqlSessionselectList,代码如下所示:

首先回到openSessionFromDataSource方法:

mybatis一级缓存机制(含源码阅读)
image-20221018135729614

创建sqlSession完成之后,需要调用相应的查询(根据不同的Statement),selectXXX,比如selectList方法(有了查询,才会使得Executor去做查询呀):

mybatis一级缓存机制(含源码阅读)
image-20221018140342720

在看selectList抽象方法具体的实现之前,先分析一下SqlSession这个接口的实现,这样找selectList这个方法的实现才能更加明确:

SqlSession这个接口有两个实现类:

mybatis一级缓存机制(含源码阅读)

SqlSession接口定义的方法已经在前面给出,基本就是对数据库的CURD和获取Mapper映射文件的字节码文件,提交事务等,包含selectXXX(),update(),insert(),delete(),commit(),getMapper()等。

  • DefaultSqlSession

SqlSessionManager对比,DefaultSqlSession是线程不安全的Sqlsession,但是是SqlSession的默认实现。

  • SqlSessionManager:
implements SqlSessionFactory, SqlSession

SqlSessionFactory这个接口定义的方法主要就是openSession.

SqlSessionManager这个类定义了3个属性:

private final SqlSessionFactory sqlSessionFactory;
private final SqlSession sqlSessionProxy;
private final ThreadLocal<SqlSession> localSqlSession = new ThreadLocal();

(1)sqlSessionFactory:主要是用于在 openSession 方法中创建 SqlSession

(2)sqlSessionProxy:用于与 mapper 交互,注意其属性名带有 Proxy,所以它是通过反射创建的

(3)localSqlSession:用于用户自己控制 SqlSession 时使用,通过 SqlSessionFactory 工厂创建

第三个属性值得关注:这个属性和线程安全相关。

来看看SqlSession实现类DefaultSqlSession中对于selectList方法的实现:

mybatis一级缓存机制(含源码阅读)
image-20221018144146617

由上面的代码看出:SqlSession把具体的查询职责委托给了Executor

var6 = this.executor.query(ms, this.wrapCollection(parameter), rowBounds, handler);

如果只开启了一级缓存的话,首先会进入BaseExecutorquery方法。代码如下所示:

mybatis一级缓存机制(含源码阅读)
image-20221018144522368

在上述代码中,会先根据传入的参数生成CacheKey,进入该方法查看CacheKey是如何生成的,代码如下所示:

mybatis一级缓存机制(含源码阅读)
image-20221018144656720

从上面代码看出:将MappedStatement的Id、SQL的offset、SQL的limit、SQL本身以及SQL中的参数传入了CacheKey这个类,最终构成CacheKey。以下是这个类的内部结构:

mybatis一级缓存机制(含源码阅读)
image-20221018144951082
mybatis一级缓存机制(含源码阅读)
image-20221018145112322

同时重写了CacheKeyequals方法,代码如下所示:

mybatis一级缓存机制(含源码阅读)
image-20221018145322444

只要两条SQL的下列五个值相同,即可以认为是相同的SQL。

Statement Id + Offset + Limmit + Sql + Params

BaseExecutorquery方法继续往下走,代码如下所示:

mybatis一级缓存机制(含源码阅读)
image-20221018145711610

query方法执行的最后,会判断一级缓存级别是否是STATEMENT级别,如果是的话,就清空缓存,这也就是STATEMENT级别的一级缓存无法共享localCache的原因。代码如下所示:

mybatis一级缓存机制(含源码阅读)
image-20221018145804446

在源码分析的最后,我们确认一下,如果是增删改insert/delete/update方法,缓存就会刷新的原因。

SqlSessioninsert方法和delete方法,都会统一走update的流程,代码如下所示:

每次执行update前都会清空localCache

mybatis一级缓存机制(含源码阅读)
image-20221018145923704
mybatis一级缓存机制(含源码阅读)
image-20221018150106179
mybatis一级缓存机制(含源码阅读)
image-20221018150147939

至此,一级缓存的工作流程以及源码分析完毕。很丰富的过程。

0x07_一级缓存总结

  1. MyBatis一级缓存的生命周期和SqlSession一致。
  2. MyBatis一级缓存内部设计简单,只是一个没有容量限定的HashMap,在缓存的功能性上有所欠缺。
  3. MyBatis的一级缓存最大范围是SqlSession内部,有多个SqlSession或者分布式的环境下,数据库写操作会引起脏数据,建议设定缓存级别为Statement。


原文始发于微信公众号(小东方不败):mybatis一级缓存机制(含源码阅读)

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/47355.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!