mybatis二级缓存详解
1 二级缓存简介
二级缓存是在多个SqlSession在同一个Mapper文件中共享的缓存,它是Mapper级别的,其作用域是Mapper文件中的namespace,默认是不开启的。看如下图:
1.1 整个流程是这样的(不考虑第三方缓存库):
当开启二级缓存后,在配置文件中配置<setting name="cacheEnabled" value="true"/>这行代码,Mybatis会为SqlSession对象生成Executor对象时,还会生成一个对象:CachingExecutor,我们称之为装饰者,这里用到了装饰器模式。那么CachingExecutor的作用是什么呢?就是当一个查询请求过来时,CachingExecutor会接到请求,先进行二级缓存的查询,如果没命中,就交给真正的Executor(默认是SimpleExecutor,但是会调用它的父类BaseExecutor的query方法,因为要进行一级缓存的查询)来查询,再到一级缓存中查询,如果还没命中,再到数据库中查询。然后把查询到的结果再返回CachingExecutor,它进行二级缓存,最后再返回给请求方。它是executor的装饰者,增强executor的功能,具有查询缓存的作用。当配置<setting name="cacheEnabled" value="false"/>时,请求过来时,BaseExecutor这个抽象类会接到请求,就不进行二级缓存的查询。
1.2 如何开启二级缓存,分三步:
一是在配置文件中开启,这是开启二级缓存的总开关,默认是开启状态的:
<setting name="cacheEnabled" value="true"/>
二是在Mapper文件中开启缓存,默认是不开启的,需要手动开启:
<!-- 每个Mapper文件使用一个缓存对象 -->
<cache/> <!-- 如果是多个Mapper文件共用一个缓存对象 -->
<cache-ref />
三是针对要查询的statement使用缓存,即在<select>节点中配置如下属性:
useCache="true"
对于二级缓存有以下说明:
- 映射语句文件中的所有 select 语句将会被缓存。
- 映射语句文件中的所有 insert,update 和 delete 语句会刷新缓存。
- 缓存会使用 Least Recently Used(LRU,最近最少使用的)算法来收回。
- 根据时间表(比如 no Flush Interval,没有刷新间隔), 缓存不会以任何时间顺序 来刷新。
- 缓存会存储列表集合或对象(无论查询方法返回什么)的 1024 个引用。
- 缓存会被视为是 read/write(可读/可写)的缓存,意味着对象检索不是共享的,而 且可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。
2 二级缓存存储取出清除过程:
2.1 二级缓存相关类的讲解:
在讲解二级缓存的存储取出清除的过程前,先了解下以下几个类:
2.1.1 TransactionalCache
TransactionalCache和TransactionalCacheManager是CachingExecutor依赖的两个组件。TransactionalCache实现了Cache接口,作用是保存某个sqlSession的某个事务中需要向某个二级缓存中添加的缓存数据,换句话说就是:某些缓存数据会先保存在这里,然后再提交到二级缓存中。源码如下:
public class TransactionalCache implements Cache { private Cache delegate; // 底层封装的二级缓存所对应的Cache对象,用到了装饰器模式 如下图1-1
private boolean clearOnCommit; // 该字段为true时,则表示当前TransactionalCache不可查询,且提交事务时,会将底层的Cache清空
// 暂时记录添加都TransactionalCahce中的数据,在事务提交时,会将其中的数据添加到二级缓存中
private Map<Object, AddEntry> entriesToAddOnCommit;
private Map<Object, RemoveEntry> entriesToRemoveOnCommit; public TransactionalCache(Cache delegate) {
this.delegate = delegate;
this.clearOnCommit = false;
this.entriesToAddOnCommit = new HashMap<Object, AddEntry>();
this.entriesToRemoveOnCommit = new HashMap<Object, RemoveEntry>();
}
// 查询底层的二级缓存
@Override
public Object getObject(Object key) {
if (clearOnCommit) return null; // issue #146
return delegate.getObject(key);
} // 该方法并没有直接将查询的结果对象存储到其封装的二级缓存Cache对象中,而是暂时保存到entriesToAddOnCommit集合中,在事务提交时才会将这些结果从entriesToAddOnCommit集合中添加到二级缓存中
@Override
public void putObject(Object key, Object object) {
entriesToRemoveOnCommit.remove(key);
entriesToAddOnCommit.put(key, new AddEntry(delegate, key, object));
} @Override
public Object removeObject(Object key) {
entriesToAddOnCommit.remove(key);
entriesToRemoveOnCommit.put(key, new RemoveEntry(delegate, key));
return delegate.getObject(key);
} @Override
public void clear() {
reset();
clearOnCommit = true;
}
// 事务提交时,先根据clearOnCommit字段的值决定是否清空二级缓存,然后将entriesToAddOnCommit集合中的结果对象保存到二级缓存中
public void commit() {
// 事务提交前,清空二级缓存
if (clearOnCommit) {
delegate.clear();
} else {
for (RemoveEntry entry : entriesToRemoveOnCommit.values()) {
entry.commit();
}
}
// 将entriesToAddOnCOmmit集合中的结果对象添加到二级缓存中
for (AddEntry entry : entriesToAddOnCommit.values()) {
entry.commit();
}
reset();
}
private static class AddEntry {
private Cache cache;
private Object key;
private Object value; public AddEntry(Cache cache, Object key, Object value) {
this.cache = cache;
this.key = key;
this.value = value;
}
// 将entriesToAddOnCOmmit集合中的结果对象添加到二级缓存中,准确的说是PerpetualCache类的HashMap中
public void commit() {
cache.putObject(key, value);
}
} private static class RemoveEntry {
private Cache cache;
private Object key; public RemoveEntry(Cache cache, Object key) {
this.cache = cache;
this.key = key;
} public void commit() {
cache.removeObject(key);
}
} }
(图1-1)
2.1.2 TranactionalCacheManager
TransactionalCacheManager是用于管理二级缓存对象Cache和TransactionCache的,它定义有transactionalCaches属性,看它的源码部分:
private Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();
2.2 二级缓存的存储和取出过程
为了说明二级缓存存储取出的整个过程,通过下面demo中代码的执行顺序来分析源码:
@Test
public void selectGoodsTest(){
// 分三步进行源码分析:
SqlSession sqlSession = getSqlSessionFactory().openSession(true);
SqlSession sqlSession2 = getSqlSessionFactory().openSession(true);
GoodsDao goodsMapper = sqlSession.getMapper(GoodsDao.class);
GoodsDao goodsMapper2 = sqlSession2.getMapper(GoodsDao.class);
// 第一步:第一次查询
goodsMapper.selectGoodsById("1");
// 第二步:事务提交
sqlSession.commit();
// 第三步:第二次查询
goodsMapper2.selectGoodsById("1");
}
2.2.1 第一步:第一次查询
当配置二级缓存时,CachingExecutor会接到请求,调用它的query方法:先进行二级缓存的查询,如果没命中,再由BaseExecutor的query方法查询。看源码:
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
// 进行二级缓存的查询
// 此处的cache就是当mybatis初始化加载mapper映射文件时,如果配置了<cache/>,就会有该cache对象;下面会对MappedStatement这个类进行分析
Cache cache = ms.getCache();
if (cache != null) {
//是否需要刷新缓存,默认情况下,select不需要刷新缓存,insert,delete,update要刷新缓存
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, parameterObject, boundSql);
@SuppressWarnings("unchecked")
// 查询二级缓存,二级缓存是存放在PerpetualCache类中的HashMap中的,使用到了装饰器模式 分析此方法
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
// 如果二级缓存没命中,则调用这个方法:这方法中是先查询一级缓存,如果还没命中,则会查询数据库
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
// 把查询出的数据放到TransactionCache的entriesToAddOnCommit这个HashMap中,要注意,只是暂时存放到这里,只有当事务提交后,这里的数据才会真正的放到二级缓存中,后面会介绍这个 分析此方法
tcm.putObject(cache, key, list); // issue #578. Query must be not synchronized to prevent deadlocks
}
return list;
}
}
// 如果不使用缓存,则调用BaseExecutor的方法
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
在这个过程中,有两个方法需要分析:首先是查询二级缓存的方法tcm.getObject,通过跟踪源码最终发现,查询二级缓存是从PerpetualCache类的HashMap中获取数据的,也就是说二级缓存真正存放到了这个地方。另外一个处理查询出的数据tcm.putObject这个方法,这个方法最终是把查询出的数据存放到了TransactionalCache这个类中的HashMap中,以Cache接口的对象为key,查询结果集的映射对象为value。到这里,需要明白一点:在执行查询操作时,查询二级缓存的地点和存储查询数据的地点是不相同的。为什么是这样呢?这就引出了第二步,sqlSession.commit事务提交这个过程。
2.2.2 第二步:事务提交
现在我们知道,在第一次查询的时候,会把从数据库中查询的数据放到TransactionCache中,但这里并不是二级缓存存放数据的地方,那么二级缓存的数据什么时候怎么来的呢?这就要分析sqlSession.commit()这个方法了,这个方法就是把之前存放在TransactionCache中的数据提交到二级缓存中,然后清空该数据。通过源码,看下commit方法到底做了哪些事情?进入CachingExecutor的commit方法:
public void commit(boolean required) throws SQLException {
// 清除一级缓存,执行缓存的SQL
delegate.commit(required);
// 将存放在TransactionCache中的数据对象提交到PerpetualCache中 进入此方法
tcm.commit();
}
进入TransactionalCacheManager类的commit方法:
public void commit() {
// 把涉及到的TransactionCache都进行处理:提交到二级缓存,并清空数据
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit(); // 进入该方法
}
}
public void commit() {
if (clearOnCommit) {
delegate.clear();
} else {
for (RemoveEntry entry : entriesToRemoveOnCommit.values()) {
entry.commit();
}
}
// 把之前存放到entiriesToAddOnCommit中的数据提交到二级缓存中,具体的说是存放到PerpetualCache类的一个HashMap中
for (AddEntry entry : entriesToAddOnCommit.values()) {
// 进入该方法
entry.commit();
}
//清空该TransactionCache中的数据
reset();
}
进入entry.commit()方法:
public void commit() {
cache.putObject(key, value); //放到PerpetualCache类中的HashMap中
}
到这里二级缓存的原理应该理解个大概了,总结下:当第一次从数据库中查出数据后,会放到TransactionCache类中;当调用sqlSession.commit()方法,进行事务提交后,TransactionCache中的数据会提交到PerpetualCache中,查询二级缓存的数据就是在这个类中,同时,TransactionCache中的数据会清空。
2.2.3 第三步:第二次查询
在事务提交之后,数据结果集对象就存放在了二级缓存中,所以第二次查询时,就可以从二级缓存中查询到数据了。进入TransactionalCache的getObject方法:
@Override
public Object getObject(Object key) {
if (clearOnCommit) return null; // issue #146
// 用到了装饰器模式,从PerpetualCache中取出数据
return delegate.getObject(key);
}
2.3 二级缓存的清除过程
先运行以下demo:
public class GoodsDaoTest { private static SqlSessionFactory sqlSessionFactory = null; @Test
public void selectGoodsTest(){ SqlSession sqlSession = getSqlSessionFactory().openSession(true);
SqlSession sqlSession2 = getSqlSessionFactory().openSession(true);
SqlSession sqlSession3 = getSqlSessionFactory().openSession(true);
GoodsDao goodsMapper = sqlSession.getMapper(GoodsDao.class) ;
GoodsDao goodsMapper2 = sqlSession2.getMapper(GoodsDao.class) ;
GoodsDao goodsMapper3 = sqlSession3.getMapper(GoodsDao.class) ; goodsMapper.selectGoodsById("1");
sqlSession.commit(); Goods goods = new Goods();
goods.setName("java1");
goods.setId("1");
goodsMapper3.updateGoodsById(goods); // 第一步 更新操作
sqlSession3.commit(); // 第二步 提交事务 goodsMapper2.selectGoodsById("1"); } public static SqlSessionFactory getSqlSessionFactory() {
String resource = "spring-ibatis.xml";
if(sqlSessionFactory == null){
try {
sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources
.getResourceAsReader(resource));
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return sqlSessionFactory;
} }
看日志:
==> Preparing: select * from goods WHERE id = ?
==> Parameters: 1(String)
<== Columns: id, name, price, detail, remark
<== Row: 1, java1, 30.00, null, null
<== Total: 1
Opening JDBC Connection
Created connection 1998228836.
==> Preparing: update goods set name = ? where id = ?
==> Parameters: java1(String), 1(String)
<== Updates: 1
Cache Hit Ratio [com.yht.mybatisTest.dao.GoodsDao]: 0.0
Opening JDBC Connection
Created connection 1945928717.
==> Preparing: select * from goods WHERE id = ?
==> Parameters: 1(String)
<== Columns: id, name, price, detail, remark
<== Row: 1, java1, 30.00, null, null
<== Total: 1
总结:在更新操作,并提交事务后,清除了二级缓存,所以第二次查询时,是从数据库中查询的数据。接下来,就针对更新操作和提交事务这两个过程作分析。
2.3.1 第一步 更新操作
进入CachingExecutor类的update方法:
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
// 进入该方法,可知:清空了TransactionalCache中entriesToAddOnCommit和entriesToRemoveOnCommit的数据,同时clearOnCommit设置为true
flushCacheIfRequired(ms);
return delegate.update(ms, parameterObject);
但是二级缓存中的数据对象并未清除,所以进入第二步事务提交。
2.3.2 第二步 事务提交
最终进入TransactionalCache的commit方法:
public void commit() {
if (clearOnCommit) {
// 由于在上一步更新操作中,clearOnCommit设置为了true,所以进入此方法:清除二级缓存中的数据
delegate.clear();
} else {
for (RemoveEntry entry : entriesToRemoveOnCommit.values()) {
entry.commit();
}
}
for (AddEntry entry : entriesToAddOnCommit.values()) {
entry.commit();
}
reset();
}
这就是清除二级缓存的过程。
总结一下:其实主要就是把这几个类之间的关系及其作用搞清楚就行了:CachingExecutor,BaseExecutor,SimpleExecutor和TransactionalCache,PerpetualCache。这几个类也是整个请求过程中比较重要的类。
mybatis二级缓存详解的更多相关文章
- SpringBoot+Mybatis一级缓存和二级缓存详解
本文主要介绍在SpringBoot项目中如何使用Mybatis的一级.二级缓存,为了演示方便,本文的数据库采用H2内存数据库,数据库连接池默认使用SpringBoot2.X自带的hikariCP. 正 ...
- Hibernate缓存简介和对比、一级缓存、二级缓存详解
一.hibernate缓存简介 缓存的范围分为3类: 1.事务范围(单Session即一级缓存) 事务范围的缓存只能被当前事务访问,每个事务都有各自的缓存,缓存内的数据通常采用相互关联的对象 ...
- mybatis一级缓存详解
mybatis缓存分为一级缓存,二级缓存和自定义缓存.本文重点讲解一级缓存 一:前言 在介绍缓存之前,先了解下mybatis的几个核心概念: * SqlSession:代表和数据库的一次会话,向用户提 ...
- Hibernate一级缓存和二级缓存详解
(1)一级缓存 是Session级别的缓存,一个Session做了一个查询操作,它会把这个操作的结果放在一级缓存中,如果短时间内这个session(一定要同一个session)又做了同一个操作,那么h ...
- hibernate的一级缓存和二级缓存详解
hibernate为我们提供了一级缓存和二级缓存,目的是为了减少应用程序对数据库的访问次数. 一级缓存: (1)所谓一级缓存就是session级别的缓存,当我们使用他的范围是当前的session,当s ...
- MyBatis 二级缓存全详解
目录 MyBatis 二级缓存介绍 二级缓存开启条件 探究二级缓存 二级缓存失效的条件 第一次SqlSession 未提交 更新对二级缓存影响 探究多表操作对二级缓存的影响 二级缓存源码解析 二级缓存 ...
- MyBatis(七):MyBatis缓存详解(一级缓存/二级缓存)
一级缓存 MyBatis一级缓存上SqlSession缓存,即在统一SqlSession中,在不执行增删改操作提交事务的前提下,对同一条数据进行多次查询时,第一次查询从数据库中查询,完成后会存入缓 ...
- MyBatis Mapper XML 详解
MyBatis Mapper XML 详解 MyBatis 真正的力量是在映射语句中.这里是奇迹发生的地方.对于所有的力量,SQL 映射的 XML 文件是相当的简单.当然如果你将它们和对等功能的 JD ...
- MyBatis核心配置文件详解
------------------------siwuxie095 MyBatis 核心配置文件详解 1.核心 ...
随机推荐
- MySQL高级知识系列目录
MySQL高级知识(一)——基础 MySQL高级知识(二)——Join查询 MySQL高级知识(三)——索引 MySQL高级知识(四)——Explain MySQL高级知识(五)——索引分析 MySQ ...
- 【CQOI2011】放棋子
[CQOI2011]放棋子 在一个n行m列的棋盘里放一些彩色的棋子,使得每个格子最多放一个棋子,且不同颜色的棋子不能在同一行或者同一列.有多少种方法? 例如\(,n=m=3\),有两个白棋子和一个灰棋 ...
- 详解区块链P2P网络
根据前一篇文章<从微观到宏观理解区块链>我们已经了解到,微观上,区块链本质就是一种不可篡改且可追踪溯源的哈希链条:宏观上,还具备了另外三个基本特征:分布式存储.P2P 网络和共识机制.分布 ...
- 8.02-json_use
import json # 1.字符串和 dic list转换 # 字符串(json)----dict list data = '[{"name":"张三",& ...
- 文件是数据的流式IO抽象,mmap是对文件的块式IO抽象
文件是数据的流式IO抽象,mmap是对文件的块式IO抽象
- 并发控制--Concurrency control--乐观、悲观及方法
In information technology and computer science, especially in the fields of computer programming, op ...
- centos7下安装docker(15.2跨主机网络-overlay)
为支持容器跨主机通信,Docker提供了overlay driver,使用户可以创建基于VxLAN的overlay网络.VxLAN可将二层数据封装到UDP进行传输,VxLAN提供与VLAN相同的以太网 ...
- 转://Oracle中定义者权限和调用者权限案例分析
定义者权限:定义者权限指使用它所有者的权限,而不是当前用户来执行过程.因此,你可以限制用户执行的数据库操作,允许他们仅通过运行定义者权限的过程和函数访问数据.创建过程.函数和程序包的默认权限是定义者权 ...
- Unexpected end of JSON input while parsing near
运行 npm cache clean --force 即可解决pm install出现”Unexpected end of JSON input while parsing near”错误.
- JPA和分布式事务简介
1. Transaction 分两种,Local Transaction 和 Global Transaction. 涉及到一个Connection的Commit,称为Local Transactio ...