-
0x01_二级缓存使用
-
0x02_二级缓存介绍
-
0x03_测试
-
测试一:是否设置事务提交
-
测试二:有增删改等操作
-
测试三:mybatis二级缓存,不适于映射文件中有多表查询的情况
-
0x04_二级缓存源码分析
-
0x05_二级缓存机制总结
Mybatis二级缓存机制
二级缓存是以namespace
为标记的缓存,可以是由一个SqlSessionFactory
创建的SqlSession
之间共享缓存数据。默认并不开启。下面的代码中创建了两个SqlSession
,执行相同的SQL语句,尝试让第二个SqlSession
使用第一个SqlSession
查询后缓存的数据。要求实体类必须实现序列化接口(所以一直以来我都养成一个习惯,只要是实体类就使其实现序列化接口Serializable
)
0x01_二级缓存使用
首先确保实体类实现了序列化接口Serializable
:
二级缓存未必完全使用内存,有可能占用硬盘存储,缓存中存储的JavaBean对象必须实现序列化接口
【1】开启全局开关:在mybatis核心配置文件sqlMapConfig.xml
中的<settings>
标签配置开启二级缓存
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
cacheEnabled
的默认值就是true
,所以这步的设置可以省略。
【2】分开关:在要开启二级缓存的mapper文件中开启缓存,在mapper标签中加入以下内容
<cache/>
经过设置后,查询结果如图所示。发现第一个SqlSession会首先去二级缓存中查找,如果不存在,就查询数据库,在commit()或者close()的时候将数据放入到二级缓存。第二个SqlSession执行相同SQL语句查询时就直接从二级缓存中获取了。
注意:
-
MyBatis的二级缓存的缓存介质有多种多样,而并不一定是在内存中(可能存在磁盘),所以需要对JavaBean对象实现序列化接口。
-
二级缓存是以
namespace
为单位的,不同namespace
下的操作互不影响 -
加入
Cache
元素后,会对相应命名空间所有的select元素查询结果进行缓存,而其中的insert
、update
、delete
在操作是会清空整个namespace的缓存。 -
cache 有一些可选的属性
type
,eviction
,flushInterval
,size
,readOnly
,blocking
。
<cache type="" readOnly="" eviction=""flushInterval=""size=""blocking=""/>
属性 | 含义 | 默认值 |
---|---|---|
type | 可以是自定义缓存类,也可以用实现Cache接口的实现类 | PerpetualCache |
readOnly | 是否只读 true:给所有调用者返回缓存对象的相同实例。因此这些对象不能被修改。这提供了很重要的性能优势。 false:会返回缓存对象的拷贝(通过序列化) 。这会慢一些,但是安全 |
false |
eviction | 缓存策略LRU(默认) – 最近最少使用:移除最长时间不被使用的对象。FIFO – 先进先出:按对象进入缓存的顺序来移除它们。SOFT – 软引用:基于垃圾回收器状态和软引用规则移除对象。WEAK – 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。 | LRU |
flushInterval | 刷新间隔,毫秒为单位。默认为null,也就是没有刷新间隔,只有执行update、insert、delete语句才会刷新 | null |
size | 缓存对象个数 | 1024 |
blocking | 是否使用阻塞性缓存BlockingCachetrue:在查询缓存时锁住对应的Key,如果缓存命中了则会释放对应的锁,否则会在查询数据库以后再释放锁,保证只有一个线程到数据库中查找指定key对应的数据false:不使用阻塞性缓存,性能更好 | false |
5.如果在加入Cache元素的前提下让个别select 元素不使用缓存,可以使用useCache属性,设置为false。
useCache控制当前sql语句是否启用缓存
flushCache控制当前sql执行一次后是否刷新缓存
flushCache
要慎用,如果在select
标签中加入这个属性,那么每一次执行SQL之后,就会刷新缓存,会给服务器带来很大的性能消耗。
<select id="findByEmpno" resultType="emp" useCache="true" flushCache="false">
0x02_二级缓存介绍
在上一篇中提到的一级缓存中,其最大的共享范围就是一个SqlSession
内部,如果多个SqlSession
之间需要共享缓存,则需要使用到二级缓存。开启二级缓存后,会使用CachingExecutor
装饰Executor
,进入一级缓存的查询流程前,先在CachingExecutor
进行二级缓存的查询,具体的工作流程如下所示。
二级缓存开启后,同一个namespace
下的所有操作语句,都影响着同一个Cache,即二级缓存被多个SqlSession
共享,是一个全局的变量。
当开启缓存后,数据的查询执行的流程就是 二级缓存 -> 一级缓存 -> 数据库。
回顾一下二级缓存的配置:
【1】mybatis核心配置文件中需要settings
标签里面配置二级缓存:
<setting name="cacheEnabled" value="true"/>
【2】在相关的映射文件中,mapper
标签下,声明二级缓存:
可以自定义配置cache标签:
-
type
:cache使用的类型,默认是PerpetualCache
,这在一级缓存中提到过。 -
eviction
: 定义回收的策略,常见的有FIFO,LRU。 -
flushInterval
: 配置一定时间自动刷新缓存,单位是毫秒。 -
size
: 最多缓存对象的个数。 -
readOnly
: 是否只读,若配置可读写,则需要对应的实体类能够序列化。 -
blocking
: 若缓存中找不到对应的key,是否会一直blocking,直到有对应的数据进入缓存。
在Mybatis官方文档中,还指明,还可以用:
<cache-ref namespace="com.someone.application.data.SomeMapper"/>
在多个命名空间中共享相同的缓存配置和实例。
还要注意:在一次查询完SqlSession之后提交事务。
0x03_测试
测试一:是否设置事务提交
准备测试方法:
@Test
public void testQueryByDeptno(){
SqlSession sqlSession1 = sqlSessionFactory.openSession();
DeptMapper mapper = sqlSession1.getMapper(DeptMapper.class);
Dept dept = mapper.queryByDeptno(30);
//提交事务
//sqlSession1.commit();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
DeptMapper mapper1 = sqlSession2.getMapper(DeptMapper.class);
Dept dept1 = mapper1.queryByDeptno(30);
sqlSession2.close();
System.out.println("mapper==mapper1:"+(mapper==mapper1));
System.out.println("dept==dept1:"+(dept==dept1));
}
可以看到没有使用事务提交的时候,没有用到二级缓存,查询了2次,未命中2次:
如果上一个SqlSession查询完提交事务,则第二次查询就会命中:
并且相同的SQL语句只是查询了一次。
测试二:有增删改等操作
@Test
public void testQueryByDeptno(){
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
SqlSession sqlSession3 = sqlSessionFactory.openSession();
//sqlSession1查询
DeptMapper mapper1 = sqlSession1.getMapper(DeptMapper.class);
Dept dept = mapper1.queryByDeptno(30);
sqlSession1.commit();
//sqlSession2查询
DeptMapper mapper2 = sqlSession2.getMapper(DeptMapper.class);
Dept dept1 = mapper2.queryByDeptno(30);
//修改数据
DeptMapper mapper3 = sqlSession3.getMapper(DeptMapper.class);
mapper3.updateDept(new Dept(44,"人事部","江苏省扬州市"));
sqlSession3.commit();//增删改必须commit
//再次查询数据
Dept dept2 = mapper2.queryByDeptno(30);
System.out.println("sqlSession1查询数据:"+dept);
System.out.println("sqlSession2查询数据:"+dept1);
System.out.println("修改数据之后,sqlSession1再次查询数据:"+dept2);
//close sqlSession
sqlSession1.close();
sqlSession2.close();
sqlSession3.close();
}
测试三:mybatis二级缓存,不适于映射文件中有多表查询的情况
通常我们会为每个单表创建单独的映射文件,由于MyBatis的二级缓存是基于namespace
的,多表查询语句所在的namspace
无法感应到其他namespace
中的语句对多表查询中涉及的表进行的修改,引发脏数据问题。
package com.bones.test01;
import com.bones.mapper.DeptMapper;
import com.bones.mapper.ProjectMapper;
import com.bones.pojo.Dept;
import com.bones.pojo.Emp;
import com.bones.pojo.Project;
import com.bones.pojo.ProjectRecord;
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 org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
/**
* @author : bones
* @version : 1.0
*/
public class Test3 {
SqlSession sqlSession1 ;
SqlSession sqlSession2 ;
SqlSession sqlSession3 ;
@Before
public void init(){
SqlSessionFactoryBuilder ssfb = new SqlSessionFactoryBuilder();
InputStream resourceAsStream = null;
try {
resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
} catch (IOException e) {
e.printStackTrace();
}
SqlSessionFactory factory = ssfb.build(resourceAsStream);
sqlSession1 = factory.openSession(true);
sqlSession2 = factory.openSession(true);
sqlSession3 = factory.openSession(true);
}
@Test
public void test2Cache(){
DeptMapper mapper1 = sqlSession1.getMapper(DeptMapper.class);
Dept dept1 = mapper1.findDeptJoinEmpByDeptno(44);
System.out.println(dept1);
DeptMapper mapper2 = sqlSession2.getMapper(DeptMapper.class);
Dept dept2 = mapper2.findDeptJoinEmpByDeptno(44);
System.out.println(dept2);
//更新数据
DeptMapper mapper3 = sqlSession3.getMapper(DeptMapper.class);
mapper3.updateDept(new Dept(44,"后勤部","腾讯"));
sqlSession3.commit();
//再次查询,得到脏数据
Dept dept3 = mapper2.findDeptJoinEmpByDeptno(44);
System.out.println(dept3);
}
@After
public void close(){
sqlSession1.close();
sqlSession2.close();
}
}
根据执行结果,发现明显读取到了脏数据:
如果想要解决上面的问题,可以用Cache ref
,让ClassMapper
引用StudenMapper
命名空间,这样两个映射文件对应的SQL操作都使用的是同一块缓存了。
不过这样做的后果是,缓存的粒度变粗了,多个Mapper namespace
下的所有操作都会对缓存使用造成影响。
0x04_二级缓存源码分析
MyBatis二级缓存的工作流程和上一篇提到的一级缓存类似,只是在一级缓存处理前,用CachingExecutor
装饰了BaseExecutor
的子类,在委托具体职责给delegate
之前,实现了二级缓存的查询和写入功能,具体类关系图如下图所示。
源码分析从CachingExecutor
的query
方法展开,源代码走读过程中涉及到的知识点较多,不能一一详细说明,读者朋友可以自行查询相关资料来学习。
CachingExecutor
的query
方法,首先会从MappedStatement
中获得在配置初始化时赋予的Cache。
本质上是装饰器模式的使用,具体的装饰链是:
SynchronizedCache -> LoggingCache -> SerializedCache -> LruCache -> PerpetualCache。
![image-20221019000453358](/Users/apple/Library/Application Support/typora-user-images/image-20221019000453358.png)
以下是具体这些Cache实现类的介绍,他们的组合为Cache赋予了不同的能力。
-
SynchronizedCache
:同步Cache,实现比较简单,直接使用synchronized修饰方法。 -
LoggingCache
:日志功能,装饰类,用于记录缓存的命中率,如果开启了DEBUG模式,则会输出命中率日志。 -
SerializedCache
:序列化功能,将值序列化后存到缓存中。该功能用于缓存返回一份实例的Copy,用于保存线程安全。 -
LruCache
:采用了Lru算法的Cache实现,移除最近最少使用的Key/Value。 -
PerpetualCache
: 作为为最基础的缓存类,底层实现比较简单,直接使用了HashMap。
然后是判断是否需要刷新缓存,代码如下所示:
在默认的设置中SELECT
语句不会刷新缓存,insert/update/delte
会刷新缓存。进入该方法。代码如下所示:
MyBatis的CachingExecutor
持有了TransactionalCacheManager
,即上述代码中的tcm。
TransactionalCacheManager
是专门用来管理CachingExecutor
使用二级缓存对象的。
TransactionalCacheManager
中持有了一个Map,代码如下所示:
这个Map保存了Cache和用TransactionalCache
包装后的Cache的映射关系。
TransactionalCache
实现了Cache接口,CachingExecutor
会默认使用他包装初始生成的Cache,作用是如果事务提交,对缓存的操作才会生效,如果事务回滚或者不提交事务,则不对缓存产生影响。
在TransactionalCache
的clear,有以下两句。清空了需要在提交时加入缓存的列表,同时设定提交时清空缓存,代码如下所示:
CachingExecutor
继续往下走,ensureNoOutParams
主要是用来处理存储过程的,暂时不用考虑。
private void ensureNoOutParams(MappedStatement ms, BoundSql boundSql) {
if (ms.getStatementType() == StatementType.CALLABLE) {
Iterator var3 = boundSql.getParameterMappings().iterator();
while(var3.hasNext()) {
ParameterMapping parameterMapping = (ParameterMapping)var3.next();
if (parameterMapping.getMode() != ParameterMode.IN) {
throw new ExecutorException("Caching stored procedures with OUT params is not supported. Please configure useCache=false in " + ms.getId() + " statement.");
}
}
}
}
之后会尝试从tcm中获取缓存的列表。
在getObject
方法中,会把获取值的职责一路传递,最终到PerpetualCache
。如果没有查到,会把key加入Miss集合,这个主要是为了统计命中率。
CachingExecutor
TransactionalCacheManager
TransactionalCache
SynchronizedCache
LoggingCache
SerializedCache
LruCache
PerpetualCache
CachingExecutor
继续往下走,如果查询到数据,则调用tcm.putObject
方法,往缓存中放入值。
tcm的put
方法也不是直接操作缓存,只是在把这次的数据和key放入待提交的Map中。
从以上的代码分析中,我们可以明白,如果不调用commit
方法的话,由于TranscationalCache
的作用,并不会对二级缓存造成直接的影响。因此我们看看Sqlsession
的commit
方法中做了什么。代码如下所示:
打上断点:
首先进入DefaultSqlSession
的commit
方法:
然后因为使用了CachingExecutor
,首先会进入CachingExecutor
实现的commit
方法
进入CachingExecutor
实现的commit
方法,会把具体commit的职责委托给包装的Executor
。主要是看下tcm.commit()
,tcm最终又会调用到TrancationalCache
。
看到这里的clearOnCommit
就想起刚才TrancationalCache
的clear
方法设置的标志位,真正的清理Cache是放到这里来进行的。具体清理的职责委托给了包装的Cache类。之后进入flushPendingEntries
方法。代码如下所示:
在flushPending
Entries中,将待提交的Map进行循环处理,委托给包装的Cache类,进行putObject
的操作。
后续的查询操作会重复执行这套流程。如果是insert|update|delete
的话,会统一进入CachingExecutor
的update
方法,其中调用了这个函数,代码如下所示:
先打上断点:
在二级缓存执行流程后就会进入一级缓存的执行流程,因此不再赘述。
0x05_二级缓存机制总结
-
MyBatis的二级缓存相对于一级缓存来说,实现了 SqlSession
之间缓存数据的共享,同时粒度更加的细,能够到namespace
级别,通过Cache接口实现类不同的组合,对Cache的可控性也更强。 -
MyBatis在多表查询时,极大可能会出现脏数据,有设计上的缺陷,安全使用二级缓存的条件比较苛刻。 -
在分布式环境下,由于默认的MyBatis Cache实现都是基于本地的,分布式环境下必然会出现读取到脏数据,需要使用集中式缓存将MyBatis的Cache接口实现,有一定的开发成本,直接使用Redis、Memcached等分布式缓存可能成本更低,安全性也更高。
emm所以这两天对于Mybatis的缓存做了一堆分析,感觉开启缓存机制下,很容易读到脏数据,所以MyBatis缓存特性在生产环境中估计最好还是关闭,单纯作为一个ORM框架使用可能更为合适。
原文始发于微信公众号(小东方不败):Mybatis二级缓存机制(含源码阅读)
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/47301.html