-
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语句,会优先命中一级缓存,避免直接对数据库进行查询,提高性能。具体执行过程如下图所示。
每个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));
}
测试结果:
可以看到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);
}
测试结果:
以上实验证明了:一级缓存只在数据库会话内部共享—》由此便会带来脏数据问题。
0x05_一级缓存机制的工作流程
一级缓存执行的时序图,如下图所示。
0x06_一级缓存机制的源码分析
接下来将对MyBatis查询相关的核心类和一级缓存的源码进行走读。这对后面学习二级缓存也有帮助。
SqlSession: 对外提供了用户和数据库之间交互需要的所有方法,隐藏了底层的细节。默认实现类是DefaultSqlSession
。SqlSession
类似于一个 JDBC
的 Connection
对象
Executor: SqlSession
向用户提供操作数据库的方法,但和数据库操作有关的职责都会委托给Executor。
如下图所示,Executor有若干个实现类,为Executor赋予了不同的能力,大家可以根据类名,自行学习每个类的基本作用。
![image-20221018130839316](/Users/apple/Library/Application Support/typora-user-images/image-20221018130839316.png)
(6个实现类)
SqlSession 执行 SQL的业务逻辑,都是委托给了 Executor 来实现。Executor 相关的类主要是用来执行 SQL。
其中,Executor 本身是一个接口;BaseExecutor 是一个抽象类,实现了 Executor 接口;而 BatchExecutor、SimpleExecutor、ReuseExecutor 三个类继承 BaseExecutor 抽象类。
下面不细说这7个类,以后有空再细读。
在一级缓存的源码分析中,主要学习BaseExecutor
的内部实现。
BaseExecutor: BaseExecutor
是一个实现了Executor接口的抽象类,定义若干抽象方法,在执行的时候,把具体的操作委托给子类进行执行。
在一级缓存的介绍中提到对Local Cache
的查询和写入是在Executor
内部完成的。在阅读BaseExecutor
的代码后发现Local Cache
是BaseExecutor
内部的一个成员变量,如下代码所示。
Cache: MyBatis中的Cache接口,提供了和缓存相关的最基本的操作,如下图所示:
有若干个实现类,使用装饰器模式互相组装,提供丰富的操控缓存的能力,部分实现类如下图所示:
BaseExecutor
成员变量之一的PerpetualCache
,是对Cache接口最基本的实现,其实现非常简单,内部持有HashMap,对一级缓存的操作实则是对HashMap的操作。如下代码所示:
补充:
在 MyBatis 中,缓存功能由接口 Cache 定义。PerpetualCache 类是最基础的缓存类,是一个大小无限的缓存。除此之外,MyBatis 还设计了 9 个包裹 PerpetualCache 类的装饰器类,用来实现功能增强。它们分别是:FifoCache、LoggingCache、LruCache、ScheduledCache、SerializedCache、SoftCache、SynchronizedCache、WeakCache、TransactionalCache。
之所以 MyBatis 采用装饰器模式来实现缓存功能,是因为装饰器模式采用了组合,而非继承,更加灵活,能够有效地避免继承关系的组合爆炸。
在阅读相关核心类代码后,从源代码层面对一级缓存工作中涉及到的相关代码,出于篇幅的考虑,对源码做适当删减,读者朋友可以结合本文,后续进行更详细的学习。
为执行和数据库的交互,首先需要初始化SqlSession
,通过DefaultSqlSessionFactory
开启SqlSession
:
注意:
SqlSessionFactory
是一个接口,DefaultSqlSessionFactory
是它的实现类之一。
在DefaultSqlSessionFactory
类中有一个openSessionFromDataSource
方法,该方法中创建了Executor
对象
所以说 SqlSessionFactory
创建的SqlSession
向用户提供操作数据库的方法,但和数据库操作有关的职责都会委托给Executor
(上面的对于数据源的组件,对这个功能进行了封装)。
在初始化SqlSesion
时,会使用Configuration
类创建一个全新的Executor
,作为DefaultSqlSession
构造函数的参数,创建Executor代码如下所示:(点进上图的newExecutor
方法进行查看)
Executor
,CachingExecutor
,BaseExecutor
这3个类以及BaseExecutor
的子类的关系如下:
上面代码中:
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
语句的话,最后会执行到SqlSession
的selectList
,代码如下所示:
首先回到openSessionFromDataSource
方法:
创建sqlSession完成之后,需要调用相应的查询(根据不同的Statement),selectXXX,比如selectList
方法(有了查询,才会使得Executor去做查询呀):
在看selectList抽象方法具体的实现之前,先分析一下
SqlSession
这个接口的实现,这样找selectList
这个方法的实现才能更加明确:
SqlSession
这个接口有两个实现类: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方法的实现:
由上面的代码看出:SqlSession
把具体的查询职责委托给了Executor
var6 = this.executor.query(ms, this.wrapCollection(parameter), rowBounds, handler);
如果只开启了一级缓存的话,首先会进入BaseExecutor
的query
方法。代码如下所示:
在上述代码中,会先根据传入的参数生成CacheKey,进入该方法查看CacheKey是如何生成的,代码如下所示:
从上面代码看出:将MappedStatement
的Id、SQL的offset、SQL的limit、SQL本身以及SQL中的参数传入了CacheKey这个类,最终构成CacheKey。以下是这个类的内部结构:
同时重写了CacheKey
的equals
方法,代码如下所示:
只要两条SQL的下列五个值相同,即可以认为是相同的SQL。
Statement Id + Offset + Limmit + Sql + Params
BaseExecutor
的query
方法继续往下走,代码如下所示:
在query
方法执行的最后,会判断一级缓存级别是否是STATEMENT
级别,如果是的话,就清空缓存,这也就是STATEMENT
级别的一级缓存无法共享localCache
的原因。代码如下所示:
在源码分析的最后,我们确认一下,如果是增删改insert/delete/update
方法,缓存就会刷新的原因。
SqlSession
的insert
方法和delete
方法,都会统一走update
的流程,代码如下所示:
每次执行update
前都会清空localCache
。
至此,一级缓存的工作流程以及源码分析完毕。很丰富的过程。
0x07_一级缓存总结
-
MyBatis一级缓存的生命周期和SqlSession一致。 -
MyBatis一级缓存内部设计简单,只是一个没有容量限定的HashMap,在缓存的功能性上有所欠缺。 -
MyBatis的一级缓存最大范围是SqlSession内部,有多个SqlSession或者分布式的环境下,数据库写操作会引起脏数据,建议设定缓存级别为Statement。
原文始发于微信公众号(小东方不败):mybatis一级缓存机制(含源码阅读)
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/47355.html