MyBatis特性详解
缓存简介
一般我们在系统中使用缓存技术是为了提升数据查询的效率。当我们从数据库中查询到一批数据后将其放入到混存中(简单理解就是一块内存区域),下次再查询相同数据的时候就直接从缓存中获取数据就行了。
这样少了一步和数据库的交互,可以提升查询的效率。
但是一个硬币都具有两面性,缓存在带来性能提升的同时也“悄悄”引入了很多问题,比如缓存同步、缓存失效、缓存雪崩等等。当然这些问题不是本文讨论的重点。
本文主要讨论MyBatis缓存这个比较鸡肋的功能。虽然说MyBatis的缓存功能比较鸡肋,但是为了全面了解MyBatis这个框架,学习下缓存这个功能还是挺有必要的。MyBatis的缓存分为一级缓存和二级缓存,
下面就分别来介绍下这两个特性。
一级缓存
在应用运行过程中,我们有可能在一次数据库会话中,执行多次查询条件完全相同的SQL,MyBatis提供了一级缓存的方案优化这部分场景,如果是相同的SQL语句,会优先命中一级缓存,避免直接对数据库进行查询,提高性能。
什么是MyBatis一级缓存
一级缓存是 SqlSession级别 的缓存。在操作数据库时需要构造 sqlSession 对象,在对象中有一个(内存区域)数据结构(HashMap)用于存储缓存数据。不同的 sqlSession 之间的缓存数据区域(HashMap)是互相不影响的。
在应用运行过程中,我们有可能在一次数据库会话中,执行多次查询条件完全相同的SQL,MyBatis 提供了一级缓存的方案优化这部分场景,如果是相同的SQL语句,会优先命中一级缓存,避免直接对数据库进行查询,提高性能。
怎么开启一级缓存
MyBatis中一级缓存默认是开启的,不需要我们做额外的操作。
如果你需要关闭一级缓存的话,可以在Mapper映射文件中将flushCache属性设置为true,这种做法只会针对单个SQL操作生效
<select id="selectByPrimaryKey" parameterType="java.lang.String" resultMap="BaseResultMap" flushCache="true">
select
<include refid="Base_Column_List" />
from cbondissuer
where OBJECT_ID = #{objectId,jdbcType=VARCHAR}
</select>
> 还有一种做法是在MyBatis的主配置文件中,关闭所有的一级缓存
> ```xml
> 默认是SESSION,也就是开启一级缓存
> <setting name="localCacheScope" value="STATEMENT"/>
> ```
下面我们来写代码验证下MyBatis的一级缓存。
```java
String id = "123";
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
//同一个sqlSession创建的Mapper
CbondissuerMapper cbondissuerMapper10 = sqlSession1.getMapper(CbondissuerMapper.class);
CbondissuerMapper cbondissuerMapper11 = sqlSession1.getMapper(CbondissuerMapper.class);
//另外一个sqlSession创建的Mapper
CbondissuerMapper cbondissuerMapper20 = sqlSession2.getMapper(CbondissuerMapper.class);
//同一个Mapper,同样的SQL查了两次
Cbondissuer cbondissuer10 = cbondissuerMapper10.selectByPrimaryKey(id);
Cbondissuer cbondissuer101 = cbondissuerMapper10.selectByPrimaryKey(id);
//同一个sqlSession创建的Mapper,又查询了一次同样的SQL
Cbondissuer cbondissuer11 = cbondissuerMapper11.selectByPrimaryKey(id);
//不一样的sqlSession创建的Mapper查询了一次同样的SQL
Cbondissuer cbondissuer20 = cbondissuerMapper20.selectByPrimaryKey(id);
System.out.println("cbondissuer10 equals cbondissuer101 :"+(cbondissuer10==cbondissuer101));
System.out.println("cbondissuer10 equals cbondissuer11 :"+(cbondissuer10==cbondissuer11));
System.out.println("cbondissuer10 equals cbondissuer21 :"+(cbondissuer10==cbondissuer20));
sqlSession1.close();
sqlSession2.close();
System.out.println("end...");
上面进行了四次查询,如果你观察日志的话。会发现只进行了两个数据库查询。因为第二和第三次的查询都查询了一级缓存,查出的其实是缓存中的结果。所以输出的结果是
cbondissuer10 equals cbondissuer101 :true
cbondissuer10 equals cbondissuer11 :true
cbondissuer10 equals cbondissuer21 :false
哪些因素会使一级缓存失效
上面的一级缓存初探让我们感受到了 MyBatis 中一级缓存的存在,那么现在你或许就会有疑问了,那么什么时候缓存失效呢?
- 通过同一个SqlSession执行更新操作时,这个更新操作不仅仅指代update操作,还指插入和删除操作;
- 事务提交时会删除一级缓存;
- 事务回滚时也会删除一级缓存;
一级缓存源码解析
其实MyBatis一级缓存的实质就是一个Executor的一个类似Map的属性,分析源码的方法就是看在哪些地方从这个Map中查询了缓存,又是在哪些清空了这些缓存。
1. 查询时使用缓存分析
public abstract class BaseExecutor implements Executor {
private static final Log log = LogFactory.getLog(BaseExecutor.class);
protected Transaction transaction;
protected Executor wrapper;
protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
//这个localCache变量就是一级缓存变量
protected PerpetualCache localCache;
protected PerpetualCache localOutputParameterCache;
protected Configuration configuration;
//..省略下面代码
}
全局搜索代码中哪些地方使用了这个变量,很容易找到BaseExecutor.query方法使用了这个缓存:
public abstract class BaseExecutor implements Executor {
// 省略其他代码
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
//先从缓存中查询结果,如果缓存中已经存在结果直接使用缓存的结果
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
//缓存中没有结果从数据库查询
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
//..省略下面代码
}
上面的代码展示了,BaseExecutor的query方法使用缓存的过程。需要注意的是查询缓存时是根据cacheKey进行查询的,我们可以将这个key简单的
理解为sql语句,不同的sql语句能查出不同的缓存。(注意sql语句中的参数不同也会被认为是不同的sql语句)。
2. 导致一级缓存失效的代码分析
查看BaseExecutor的代码,我们很容易发现是下面的方法清空了一级缓存。(不要问我是怎么发现这个代码的,看代码能力需要自己慢慢提升)
@Override
public void clearLocalCache() {
if (!closed) {
localCache.clear();
localOutputParameterCache.clear();
}
}
那么我们只要查看哪些地方调用了这个方法就知道哪些情况下会导致一级缓存失效了。跟踪下来,最后发现下面三处地方会使得一级缓存失效
BaseExecutor的update方法,使用MyBatis的接口进行增、删、改操作都会调用到这个方法,这个也印证了上面的说法。
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
clearLocalCache();
return doUpdate(ms, parameter);
}
BaseExecutor的commit方法,事务提交会导致一级缓存失败。如果我们使用Spring的话,一般事务都是自动提交的,所以好像MyBatis的一级缓存一直没怎么被考虑过
@Override
public void commit(boolean required) throws SQLException {
if (closed) {
throw new ExecutorException("Cannot commit, transaction is already closed");
}
clearLocalCache();
flushStatements();
if (required) {
transaction.commit();
}
}
BaseExecutor的rollback方法,事务回滚也会导致一级缓存失效。
@Override
public void rollback(boolean required) throws SQLException {
if (!closed) {
try {
clearLocalCache();
flushStatements(true);
} finally {
if (required) {
transaction.rollback();
}
}
}
}
一级缓存使用建议
平时使用MyBatis时都是和Spring结合使用的,在整个Spring容器中一般只有一个SqlSession实现类。而Spring一般都是主动提交事务的,所以说一级缓存经常失效。
还有就是我们也很少在一个事务范围内执行同一个SQL两遍,上面的这些原因导致我们在开发过程中很少注意到MyBatis一级缓存的存在。
不怎么用并不是说不用,作为一个合格的开发者需要对这些心知肚明,要清楚的知道MyBatis一级缓存的工作流程。
二级缓存
什么是MyBatis二级缓存
MyBatis 一级缓存最大的共享范围就是一个SqlSession内部,那么如果多个 SqlSession 需要共享缓存,则需要开启二级缓存,开启二级缓存后,会使用 CachingExecutor 装饰 Executor,
进入一级缓存的查询流程前,先在CachingExecutor 进行二级缓存的查询,具体的工作流程如下所示:
当二级缓存开启后,同一个命名空间(namespace) 所有的操作语句,都影响着一个 共同的 cache(一个Mapper映射文件对应一个Cache),也就是二级缓存被多个 SqlSession 共享,是一个全局的变量。当开启缓存后,数据的查询执行的流程就是 二级缓存 -> 一级缓存 -> 数据库。
从上面的图可以看出,MyBatis的二级缓存实现可以有很多种,可以是MemCache、Ehcache等。也可以是Redis等,但是需要额外的Jar包。
怎么开启二级缓存
二级缓存默认是不开启的,需要手动开启二级缓存,实现二级缓存的时候,MyBatis要求返回的POJO必须是可序列化的。开启二级缓存的条件也是比较简单,
step1:通过直接在 MyBatis 配置文件中通过
<settings>
<setting name = "cacheEnabled" value = "true" />
</settings>
step2: 在 Mapper 的xml 配置文件中加入 标签
cache标签下面有下面几种可选项
eviction: 缓存回收策略,支持的策略有下面几种
- LRU - 最近最少回收,移除最长时间不被使用的对象(默认是这个策略)
- FIFO - 先进先出,按照缓存进入的顺序来移除它们
- SOFT - 软引用,移除基于垃圾回收器状态和软引用规则的对象
- WEAK - 弱引用,更积极的移除基于垃圾收集器和弱引用规则的对象
flushinterval:缓存刷新间隔,缓存多长时间刷新一次,默认不清空,设置一个毫秒值;
readOnly: 是否只读;true 只读 ,MyBatis 认为所有从缓存中获取数据的操作都是只读操作,不会修改数据。MyBatis 为了加快获取数据,直接就会将数据在缓存中的引用交给用户。不安全,速度快。读写(默认):MyBatis 觉得数据可能会被修改
size : 缓存存放多少个元素
type: 指定自定义缓存的全类名(实现Cache 接口即可)
blocking:若缓存中找不到对应的key,是否会一直blocking,直到有对应的数据进入缓存。
cache-ref代表引用别的命名空间的Cache配置,两个命名空间的操作使用的是同一个Cache。
哪些因素会使二级缓存失效
从上面的介绍可以知道MyBatis的二级缓存主要是为了SqlSession之间共享缓存设计的。但是我们平时开发过程中都是结合Spring来进行MyBatis的开发。在Spring环境下一般也只有一个SqlSession实例,所以二级缓存使用到的机会不多。所以下面就简单描述下Mybatis的二级缓存。
还是以上面的列子为列
String id = "{0003CCCA-AEA9-4A1E-A3CC-06D884BA3906}";
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
//同一个sqlSession创建的Mapper
CbondissuerMapper cbondissuerMapper10 = sqlSession1.getMapper(CbondissuerMapper.class);
CbondissuerMapper cbondissuerMapper11 = sqlSession1.getMapper(CbondissuerMapper.class);
//另外一个sqlSession创建的Mapper
CbondissuerMapper cbondissuerMapper20 = sqlSession2.getMapper(CbondissuerMapper.class);
//同一个Mapper,同样的SQL查了两次
Cbondissuer cbondissuer10 = cbondissuerMapper10.selectByPrimaryKey(id);
Cbondissuer cbondissuer101 = cbondissuerMapper10.selectByPrimaryKey(id);
//同一个sqlSession创建的Mapper,又查询了一次同样的SQL
Cbondissuer cbondissuer11 = cbondissuerMapper11.selectByPrimaryKey(id);
//这边需要提交事务才能让二级缓存生效
sqlSession1.commit();
//不一样的sqlSession创建的Mapper查询了一次同样的SQL
Cbondissuer cbondissuer20 = cbondissuerMapper20.selectByPrimaryKey(id);
System.out.println("cbondissuer10 equals cbondissuer101 :"+(cbondissuer10==cbondissuer101));
System.out.println("cbondissuer10 equals cbondissuer11 :"+(cbondissuer10==cbondissuer11));
System.out.println("cbondissuer10 equals cbondissuer21 :"+(cbondissuer10==cbondissuer20));
- 二级缓存是以namespace(Mapper)为单位的,不同namespace下的操作互不影响。
- insert,update,delete操作会清空所在namespace下的全部缓存。
- 多表操作一定不要使用二级缓存,因为多表操作进行更新操作,一定会产生脏数据。
二级缓存使用建议
个人觉得MyBatis的二级缓存实用性不是很大。一个原因就是Spring环境下,一本只有一个SqlSession,不存在sqlSession之间共享缓存;还有就是
MyBatis的缓存都不能做到分布式,所以对于MyBatis的二级缓存以了解为主。
简单总结
一级缓存
- 一级缓存的本质是Executor的一个类似Map的属性;
- 一级缓存默认开启,将flushCache设置成true或者将全局配置localCacheScope设置成Statement可以关闭一级缓存;
- 在一级缓存开启的情况下,查询操作会先查询一级缓存,再查询数据库;
- 增删改操作和事务提交回滚操作会导致一级缓存失效;
- 由于Spring中事务是自动提交的,因此Spring下的MyBatis一级缓存经常失效。(但是并不表示不生效,除非你手动关闭一级缓存)
- 不能实现分布式。
二级缓存
- namesapce级别的缓存(Mapper级别或者叫做表级别的缓存),设计的主要目的是实现sqlSession之间的缓存共享;
- 开启二级缓存后,查询的逻辑是二级缓存->已经缓存->数据库;
- insert,update,delete操作会清空所在namespace下的全部缓存;
- 多表查询一定不要使用二级缓存,因为多表操作进行更新操作,可能会产生脏数据。
总体来说,MyBatis的缓存功能比较鸡肋。想要使用缓存的话还是建议使用spring-cache等框架。
参考
- https://blog.csdn.net/zb313982521/article/details/79689169
- https://mp.weixin.qq.com/s?__biz=MzI4NDY5Mjc1Mg==&mid=2247489120&idx=2&sn=4694c4a359849d17354f85206768c25b&chksm=ebf6ce1fdc81470918515ff76c41d7aea9434226ef05e930fec59ed22dcc709030a6683c0d80&mpshare=1&scene=1&srcid=&sharer_sharetime=1566873637232&sharer_shareid=2040c1b4c62e1f430c804ebd0fe79fa3#rd
MyBatis特性详解的更多相关文章
- C#中的 特性 详解(转载)
本篇幅转载于:http://www.cnblogs.com/rohelm/archive/2012/04/19/2456088.html C#中特性详解 特性提供了功能强大的方法,用于将元数据或声明信 ...
- iOS开发——高级特性&Runtime运行时特性详解
Runtime运行时特性详解 本文详细整理了 Cocoa 的 Runtime 系统的知识,它使得 Objective-C 如虎添翼,具备了灵活的动态特性,使这门古老的语言焕发生机.主要内容如下: 引言 ...
- ES6,ES2105核心功能一览,js新特性详解
ES6,ES2105核心功能一览,js新特性详解 过去几年 JavaScript 发生了很大的变化.ES6(ECMAScript 6.ES2105)是 JavaScript 语言的新标准,2015 年 ...
- 《Android群英传》读书笔记 (5) 第十一章 搭建云端服务器 + 第十二章 Android 5.X新特性详解 + 第十三章 Android实例提高
第十一章 搭建云端服务器 该章主要介绍了移动后端服务的概念以及Bmob的使用,比较简单,所以略过不总结. 第十三章 Android实例提高 该章主要介绍了拼图游戏和2048的小项目实例,主要是代码,所 ...
- C#各个版本中的新增特性详解
序言 自从2000年初期发布以来,c#编程语言不断的得到改进,使我们能够更加清晰的编写代码,也更加容易维护我们的代码,增强的功能已经从1.0搞到啦7.0甚至7.1,每一次改过都伴随着.NET Fram ...
- ASP.NET Core Web服务器 Kestrel和Http.sys 特性详解
ASP.NET Core Web服务器 Kestrel和Http.sys 特性详解 1.1. 名词解释 1.2. Kestrel基本工作原理 1.2.1. Kestrel的基本架构 1.2.2. Ke ...
- Android群英传笔记——第十二章:Android5.X 新特性详解,Material Design UI的新体验
Android群英传笔记--第十二章:Android5.X 新特性详解,Material Design UI的新体验 第十一章为什么不写,因为我很早之前就已经写过了,有需要的可以去看 Android高 ...
- 单元测试系列之十一:Jmockit之mock特性详解
本文是Jmockit学习过程中,根据官网所列的工具特性进行解读. 1.调用次数约束(Invocation count constraints) 可以通过调用计数约束来指定预期和/或允许匹配给定期望的调 ...
- Java9 新特性 详解
作者:木九天 < Java9 新特性 详解 > Java9 新特性 详解 摘要: 1.目录结构 2.repl工具 jShell命令 3.模块化 4.多版本兼容jar包 5.接口方 ...
随机推荐
- 关于Cookie的一点简单认识
1.Cookie Cookies是服务器在本地机器上存储的小段文本并随每一个请求发送至同一服务器,是在客户端保持状态的方案.通常每个 Cookie 的大小不能超过4KB.客户端每次向服务器发出请求,就 ...
- Java抽象类的学习体会与注意事项
一.定义 抽象类:用abstract声明的class为抽象类. 抽象方法:用abstract声明的方法为抽象方法. 抽象方法特点:只有方法定义,没有方法的实现(函数体) 抽象类的子类都必须实现它的方法 ...
- 设计模式之GOF23原型模式01
原型模式prototype 原型模式: - 通过new产生一个对象需要非常繁琐的数据准备或者访问权限,则可以使用原型模式,比如如果new对象所需时间过长,可以通过克隆产生相同的副本 - Java中的克 ...
- HDU 3874 Necklace 区间查询的离线操作
题目: http://acm.hdu.edu.cn/showproblem.php?pid=3874 对需要查询的区间按右端点排序,然后从左到右依次加入序列中的元素,同时更新,更新的方法是,把上一次出 ...
- 4-JVM 参数
JVM 参数 标准参数:不会随着jdk版本的变化而变化.比如:java -version.java -help 非标准参数:随着JDK版本的变化而变化. -X参数[用的较少]非标准参数,也就是在JDK ...
- CTR学习笔记&代码实现5-深度ctr模型 DeepCrossing -> DCN
之前总结了PNN,NFM,AFM这类两两向量乘积的方式,这一节我们换新的思路来看特征交互.DeepCrossing是最早在CTR模型中使用ResNet的前辈,DCN在ResNet上进一步创新,为高阶特 ...
- 如何通过VMware安装Linux CentOS 7.7系统
如何在Vmware安装Linux CentOS 7.7系统,并且是最小化安装.之后进行必要的配置修改,并实现基础优化.最后做一个快照. 安装Linux CentOS 7.7 安装要求:安装后的虚拟机用 ...
- 数字化制造-基于Plant Simulation的冲压车间数字化仿真平台研究
冲压车间是将板材冲压形成汽车车身.车厢.车底板等部件的过程,冲压是汽车生产四大加工工艺的首个工序,直接影响着汽车焊装.涂装.总装车间的生产.冲压车间生产具有以下特点: 换模时间长:每种冲压件均有一套专 ...
- 数据库范式1NF 2NF 3NF详细阐述
范式:关系数据库中的关系是要满足一定要求的,满足不同程度要求的不同范式.满足最低要求的叫第一范式,简称1NF ,在第一范式中满足进一步要求的为第二范式,其余以此类推.通俗来说是满足数据库关系表中的一套 ...
- 9.2 Go 文件IO操作
9.2 Go 文件IO操作 1.1.1. bufio包 带有缓冲区的IO读写操作,用于读写文件,以及读取键盘输入 func main() { //NewReader返回一个结构体对象指针 reader ...