Mybatis二级缓存机制(含源码阅读)


  • 0x01_二级缓存使用

  • 0x02_二级缓存介绍

  • 0x03_测试

    • 测试一:是否设置事务提交

    • 测试二:有增删改等操作

    • 测试三:mybatis二级缓存,不适于映射文件中有多表查询的情况

  • 0x04_二级缓存源码分析

  • 0x05_二级缓存机制总结


Mybatis二级缓存机制

二级缓存是以namespace为标记的缓存,可以是由一个SqlSessionFactory创建的SqlSession之间共享缓存数据。默认并不开启。下面的代码中创建了两个SqlSession,执行相同的SQL语句,尝试让第二个SqlSession使用第一个SqlSession查询后缓存的数据。要求实体类必须实现序列化接口(所以一直以来我都养成一个习惯,只要是实体类就使其实现序列化接口Serializable)

0x01_二级缓存使用

首先确保实体类实现了序列化接口Serializable

二级缓存未必完全使用内存,有可能占用硬盘存储,缓存中存储的JavaBean对象必须实现序列化接口

Mybatis二级缓存机制(含源码阅读)

【1】开启全局开关:在mybatis核心配置文件sqlMapConfig.xml中的<settings>标签配置开启二级缓存

<settings>
<setting name="cacheEnabled" value="true"/>
</settings>

cacheEnabled的默认值就是true,所以这步的设置可以省略。

【2】分开关:在要开启二级缓存的mapper文件中开启缓存,在mapper标签中加入以下内容

<cache/>

Mybatis二级缓存机制(含源码阅读)

经过设置后,查询结果如图所示。发现第一个SqlSession会首先去二级缓存中查找,如果不存在,就查询数据库,在commit()或者close()的时候将数据放入到二级缓存。第二个SqlSession执行相同SQL语句查询时就直接从二级缓存中获取了。

注意:

  1. MyBatis的二级缓存的缓存介质有多种多样,而并不一定是在内存中(可能存在磁盘),所以需要对JavaBean对象实现序列化接口。

  2. 二级缓存是以 namespace 为单位的,不同 namespace 下的操作互不影响

  3. 加入Cache元素后,会对相应命名空间所有的select元素查询结果进行缓存,而其中的insertupdatedelete在操作是会清空整个namespace的缓存。

  4. 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进行二级缓存的查询,具体的工作流程如下所示。

Mybatis二级缓存机制(含源码阅读)
image-20221018185547653

二级缓存开启后,同一个namespace下的所有操作语句,都影响着同一个Cache,即二级缓存被多个SqlSession共享,是一个全局的变量。

当开启缓存后,数据的查询执行的流程就是 二级缓存 -> 一级缓存 -> 数据库

回顾一下二级缓存的配置:

【1】mybatis核心配置文件中需要settings标签里面配置二级缓存:

Mybatis二级缓存机制(含源码阅读)

        <setting name="cacheEnabled" value="true"/>

【2】在相关的映射文件中,mapper标签下,声明二级缓存:

Mybatis二级缓存机制(含源码阅读)

可以自定义配置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次:

Mybatis二级缓存机制(含源码阅读)
image-20221018193538371

如果上一个SqlSession查询完提交事务,则第二次查询就会命中:

Mybatis二级缓存机制(含源码阅读)
image-20221018193605751
Mybatis二级缓存机制(含源码阅读)
image-20221018193711341

并且相同的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二级缓存机制(含源码阅读)
image-20221018195632136

测试三: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();
}
}

根据执行结果,发现明显读取到了脏数据:

Mybatis二级缓存机制(含源码阅读)
image-20221018201952087

如果想要解决上面的问题,可以用Cache ref,让ClassMapper引用StudenMapper命名空间,这样两个映射文件对应的SQL操作都使用的是同一块缓存了。

不过这样做的后果是,缓存的粒度变粗了,多个Mapper namespace下的所有操作都会对缓存使用造成影响。

0x04_二级缓存源码分析

MyBatis二级缓存的工作流程和上一篇提到的一级缓存类似,只是在一级缓存处理前,用CachingExecutor装饰了BaseExecutor的子类,在委托具体职责给delegate之前,实现了二级缓存的查询和写入功能,具体类关系图如下图所示。

Mybatis二级缓存机制(含源码阅读)
image-20221018211353954

Mybatis二级缓存机制(含源码阅读)
image-20221018234725957
Mybatis二级缓存机制(含源码阅读)
image-20221018234807848

Mybatis二级缓存机制(含源码阅读)

源码分析从CachingExecutorquery方法展开,源代码走读过程中涉及到的知识点较多,不能一一详细说明,读者朋友可以自行查询相关资料来学习。

CachingExecutorquery方法,首先会从MappedStatement中获得在配置初始化时赋予的Cache。

Mybatis二级缓存机制(含源码阅读)
image-20221019000719431

本质上是装饰器模式的使用,具体的装饰链是:

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。

然后是判断是否需要刷新缓存,代码如下所示:

Mybatis二级缓存机制(含源码阅读)
image-20221019000847513

在默认的设置中SELECT语句不会刷新缓存,insert/update/delte会刷新缓存。进入该方法。代码如下所示:

Mybatis二级缓存机制(含源码阅读)

Mybatis二级缓存机制(含源码阅读)
image-20221019001001222

MyBatis的CachingExecutor持有了TransactionalCacheManager,即上述代码中的tcm。

TransactionalCacheManager是专门用来管理CachingExecutor 使用二级缓存对象的。

TransactionalCacheManager中持有了一个Map,代码如下所示:

Mybatis二级缓存机制(含源码阅读)
image-20221019102431115

这个Map保存了Cache和用TransactionalCache包装后的Cache的映射关系。

TransactionalCache实现了Cache接口,CachingExecutor会默认使用他包装初始生成的Cache,作用是如果事务提交,对缓存的操作才会生效,如果事务回滚或者不提交事务,则不对缓存产生影响。

TransactionalCache的clear,有以下两句。清空了需要在提交时加入缓存的列表,同时设定提交时清空缓存,代码如下所示:

Mybatis二级缓存机制(含源码阅读)
image-20221019102640000

CachingExecutor继续往下走,ensureNoOutParams主要是用来处理存储过程的,暂时不用考虑。

Mybatis二级缓存机制(含源码阅读)
image-20221019104352670
    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中获取缓存的列表。

Mybatis二级缓存机制(含源码阅读)
image-20221019104503535

getObject方法中,会把获取值的职责一路传递,最终到PerpetualCache。如果没有查到,会把key加入Miss集合,这个主要是为了统计命中率。

CachingExecutor

Mybatis二级缓存机制(含源码阅读)
image-20221019143738839

TransactionalCacheManager

Mybatis二级缓存机制(含源码阅读)
image-20221019143802193

TransactionalCache

Mybatis二级缓存机制(含源码阅读)
image-20221019143829892

SynchronizedCache

Mybatis二级缓存机制(含源码阅读)
image-20221019143851178

LoggingCache

Mybatis二级缓存机制(含源码阅读)
image-20221019143914982

SerializedCache

Mybatis二级缓存机制(含源码阅读)
image-20221019144049159

LruCache

Mybatis二级缓存机制(含源码阅读)
image-20221019144121277

PerpetualCache

Mybatis二级缓存机制(含源码阅读)
image-20221019144214514

CachingExecutor继续往下走,如果查询到数据,则调用tcm.putObject方法,往缓存中放入值。

Mybatis二级缓存机制(含源码阅读)
image-20221019144803172

tcm的put方法也不是直接操作缓存,只是在把这次的数据和key放入待提交的Map中。

Mybatis二级缓存机制(含源码阅读)
image-20221019144917770
Mybatis二级缓存机制(含源码阅读)
image-20221019144951173

从以上的代码分析中,我们可以明白,如果不调用commit方法的话,由于TranscationalCache的作用,并不会对二级缓存造成直接的影响。因此我们看看Sqlsessioncommit方法中做了什么。代码如下所示:

打上断点:

Mybatis二级缓存机制(含源码阅读)

首先进入DefaultSqlSessioncommit方法:

Mybatis二级缓存机制(含源码阅读)
image-20221019145253706

然后因为使用了CachingExecutor,首先会进入CachingExecutor实现的commit方法

Mybatis二级缓存机制(含源码阅读)
image-20221019145334866

进入CachingExecutor实现的commit方法,会把具体commit的职责委托给包装的Executor。主要是看下tcm.commit(),tcm最终又会调用到TrancationalCache

Mybatis二级缓存机制(含源码阅读)
image-20221019145452847
Mybatis二级缓存机制(含源码阅读)
image-20221019145555755
Mybatis二级缓存机制(含源码阅读)
image-20221019145635108

看到这里的clearOnCommit就想起刚才TrancationalCacheclear方法设置的标志位,真正的清理Cache是放到这里来进行的。具体清理的职责委托给了包装的Cache类。之后进入flushPendingEntries方法。代码如下所示:

Mybatis二级缓存机制(含源码阅读)
image-20221019145730850

flushPendingEntries中,将待提交的Map进行循环处理,委托给包装的Cache类,进行putObject的操作。

Mybatis二级缓存机制(含源码阅读)
image-20221019145832763

后续的查询操作会重复执行这套流程。如果是insert|update|delete的话,会统一进入CachingExecutorupdate方法,其中调用了这个函数,代码如下所示:

先打上断点:

Mybatis二级缓存机制(含源码阅读)
image-20221019150008625
Mybatis二级缓存机制(含源码阅读)
image-20221019150132101
Mybatis二级缓存机制(含源码阅读)
image-20221019150150130
Mybatis二级缓存机制(含源码阅读)
image-20221019150437189

在二级缓存执行流程后就会进入一级缓存的执行流程,因此不再赘述。

0x05_二级缓存机制总结

  1. MyBatis的二级缓存相对于一级缓存来说,实现了SqlSession之间缓存数据的共享,同时粒度更加的细,能够到namespace级别,通过Cache接口实现类不同的组合,对Cache的可控性也更强。
  2. MyBatis在多表查询时,极大可能会出现脏数据,有设计上的缺陷,安全使用二级缓存的条件比较苛刻。
  3. 分布式环境下,由于默认的MyBatis Cache实现都是基于本地的,分布式环境下必然会出现读取到脏数据,需要使用集中式缓存将MyBatis的Cache接口实现,有一定的开发成本,直接使用Redis、Memcached等分布式缓存可能成本更低,安全性也更高。

emm所以这两天对于Mybatis的缓存做了一堆分析,感觉开启缓存机制下,很容易读到脏数据,所以MyBatis缓存特性在生产环境中估计最好还是关闭,单纯作为一个ORM框架使用可能更为合适。


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

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

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

(0)
小半的头像小半

相关推荐

发表回复

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