mybatis缓存分为一级缓存,二级缓存和自定义缓存。本文重点讲解一级缓存

一:前言

在介绍缓存之前,先了解下mybatis的几个核心概念:

* SqlSession:代表和数据库的一次会话,向用户提供了操作数据库的方法

* MapperedStatement:代表要往数据库发送的要执行的指令,可以理解为sql的抽象表示

* Executor:用来和数据库交互的执行器,接收MapperedStatement作为参数

二:一级缓存

1.一级缓存的介绍:

mybatis一级缓存有两种:一种是SESSION级别的,针对同一个会话SqlSession中,执行多次条件完全相同的同一个sql,那么会共享这一缓存,默认是SESSION级别的缓存;一种是STATEMENT级别的,缓存只针对当前执行的这一statement有效。

对于一级缓存的流程,看下图:

整个流程是这样的:

* 针对某个查询的statement,生成唯一的key

* 在Local Cache 中根据key查询数据是否存在

* 如果存在,则命中,跳过数据库查询,继续往下走

* 如果没命中:

* 去数据库中查询,得到查询结果

* 将key和查询结果放到Local Cache中

* 将查询结果返回

* 判断是否是STATEMENT级别缓存,如果是,则清除缓存

接下来针对一级缓存的几种情况,来进行验证。

情况1:SESSION级别缓存,同一个Mapper代理对象执行条件相同的同一个查询sql

SqlSession sqlSession = getSqlSessionFactory().openSession();
GoodsDao goodsMapper = sqlSession.getMapper(GoodsDao.class);
goodsMapper.selectGoodsById("1");
goodsMapper.selectGoodsById("1");

结果:

Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@3dd44d5e]
==> Preparing: select * from goods where id = ?
==> Parameters: 1(String)
<== Columns: id, name, detail, remark
<== Row: 1, title1, null, null
<== Total: 1

总结:只向数据库进行了一次查询,第二次用了缓存

情况2:SESSION级别缓存,同一个Mapper代理对象执行条件不同的同一个查询sql

    public void selectGoodsTest(){
SqlSession sqlSession = getSqlSessionFactory().openSession();
GoodsDao goodsMapper = sqlSession.getMapper(GoodsDao.class);
goodsMapper.selectGoodsById("1");
goodsMapper.selectGoodsById("2");
}

结果:

==>  Preparing: select * from goods where id = ?
==> Parameters: 1(String)
<== Columns: id, name, detail, remark
<== Row: 1, title1, null, null
<== Total: 1
==> Preparing: select * from goods where id = ?
==> Parameters: 2(String)
<== Columns: id, name, detail, remark
<== Row: 2, title2, null, null
<== Total: 1

总结:因为查询条件不同,所以是两个不同的statement,生成了两个不同key,缓存中是没有的

情况3:SESSION级别缓存,针对同一个Mapper接口生成两个代理对象,然后执行查询条件完全相同的同一条sql

    public void selectGoodsTest(){
SqlSession sqlSession = getSqlSessionFactory().openSession();
GoodsDao goodsMapper = sqlSession.getMapper(GoodsDao.class);
GoodsDao goodsMapper2 = sqlSession.getMapper(GoodsDao.class);
goodsMapper.selectGoodsById("1");
goodsMapper2.selectGoodsById("1");
}

结果:

==> Preparing: select * from goods where id = ?
==> Parameters: 1(String)
<== Columns: id, name, detail, remark
<== Row: 1, title1, null, null
<== Total: 1

总结:这种情况满足:同一个SqlSession会话,查询条件完全相同的同一条sql。所以,第二次查询是从缓存中查找的。

情况4:SESSION级别缓存,在同一次会话中,对数据库进行了修改操作,一级缓存是否是失效。

    // 这里对id=2的数据进行了upate操作,发现id=1的一级缓存也被清除,因为它们是在同一个SqlSession中
@Test
public void selectGoodsTest(){
SqlSession sqlSession = getSqlSessionFactory().openSession();
GoodsDao goodsMapper = sqlSession.getMapper(GoodsDao.class);
Goods goods = new Goods();
goods.setId("2");
goods.setName("篮球");
goodsMapper.selectGoodsById("1");
goodsMapper.updateGoodsById(goods);
goodsMapper.selectGoodsById("1");
}

结果:

==>  Preparing: select * from goods where id = ?
==> Parameters: 1(String)
<== Columns: id, name, detail, remark
<== Row: 1, title1, null, null
<== Total: 1
==> Preparing: update goods set name = ? where id = ?
==> Parameters: 篮球(String), 2(String)
<== Updates: 1
==> Preparing: select * from goods where id = ?
==> Parameters: 1(String)
<== Columns: id, name, detail, remark
<== Row: 1, title1, null, null
<== Total: 1

总结:在同一个SqlSession会话中,如果对数据库进行了修改操作,那么该会话中的缓存都会被清除。但是,并不会影响其它会话中的缓存。

情况5:SESSION级别缓存,开启两个SqlSession,在SqlSession1中查询操作,在SqlSession2中执行修改操作,那么SqlSession1中的一级缓存是否仍然有效?

@Test
public void selectGoodsTest(){
SqlSession sqlSession = getSqlSessionFactory().openSession();
GoodsDao goodsMapper = sqlSession.getMapper(GoodsDao.class);
SqlSession sqlSession2 = getSqlSessionFactory().openSession();
GoodsDao goodsMapper2 = sqlSession2.getMapper(GoodsDao.class);
Goods goods = new Goods();
goods.setId("1");
goods.setName("篮球");
Goods goods1 = goodsMapper.selectGoodsById("1");
System.out.println("name="+goods1.getName());
System.out.println("******************************************************");
goodsMapper2.updateGoodsById(goods);
Goods goodsResult = goodsMapper.selectGoodsById("1");
System.out.println("******************************************************");
System.out.println("name="+goodsResult.getName());
}

结果:

==>  Preparing: select * from goods where id = ?
==> Parameters: 1(String)
<== Columns: id, name, detail, remark
<== Row: 1, title1, null, null
<== Total: 1
name=title1
******************************************************
Opening JDBC Connection
Created connection 644010817.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@2662d341]
==> Preparing: update goods set name = ? where id = ?
==> Parameters: 篮球(String), 1(String)
<== Updates: 1
******************************************************
name=title1

总结:在SqlSession2中对id=1的数据做了修改,但是在SqlSession1中的最后一次查询中,仍然是从一级缓存中取得数据,说明了一级缓存只在SqlSession内部共享,SqlSession对数据库的修改操作不影响其它SqlSession中的一级缓存。

情况6:SqlSession的缓存级别设置为STATEMENT,即在配置文件中添加如下代码:

<settings>
<setting name="localCacheScope" value="STATEMENT"/>
</settings>

执行代码:

    @Test
public void selectGoodsTest(){
SqlSession sqlSession = getSqlSessionFactory().openSession();
GoodsDao goodsMapper = sqlSession.getMapper(GoodsDao.class);
goodsMapper.selectGoodsById("1");
System.out.println("****************************************");
goodsMapper.selectGoodsById("1");
}

结果:

==>  Preparing: select * from goods where id = ?
==> Parameters: 1(String)
<== Columns: id, name, detail, remark
<== Row: 1, title1, null, null
<== Total: 1
****************************************
==> Preparing: select * from goods where id = ?
==> Parameters: 1(String)
<== Columns: id, name, detail, remark
<== Row: 1, title1, null, null
<== Total: 1

总结:STATEMENT级别的缓存,只针对当前执行的这一statement有效

2.一级缓存是如何被存取的?

我们知道,当与数据库建立一次连接,就会创建一个SqlSession对象,默认是DefaultSqlSession这个实现,这个对象给用户提供了操作数据库的各种方法,与此同时,也会创建一个Executor执行器,缓存信息就是维护在Executor中,Executor有一个抽象子类BaseExecutor,这个类中有个属性PerpetualCache类,这个类就是真正用于维护一级缓存的地方。通过看源码,可以知道如何根据cacheKey,取出和存放缓存的。

在查询数据库前,先从缓存中查找,进入BaseExecutor类的query方法:

//这是BaseExecutor的一个属性,用于存放一级缓存
protected PerpetualCache localCache; @SuppressWarnings("unchecked")
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++;
// 根据CacheKey作为key,查询HashMap中的value值,也就是缓存,这就是取出缓存的过程
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();
}
deferredLoads.clear(); // issue #601
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
clearLocalCache(); // issue #482
}
}
return list;
}

当从数据库中查询到数据后,需要把数据存放到缓存中的,然后再返回数据,这个就是存放缓存的过程,进入queryFromDatabase方法:

 private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
// 从数据库中查询到数据
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
// 把数据放到缓存中,这就是存房缓存的动作
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}

3.CacheKey是如何确定唯一的?

我们知道,如果两次查询完全相同,那么第二次查询就从缓存中取数据,换句话说,怎么判断两次查询是不是相同的?是否相同是根据CacheKey来判断的,那么看下CacheKey的生成过程,就知道影响CacheKey是否相同的元素有哪些了。

进入BaseExecutor类的createCacheKey方法:

public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) throw new ExecutorException("Executor was closed.");
CacheKey cacheKey = new CacheKey();
// statement id
cacheKey.update(ms.getId());
// rowBounds.offset
cacheKey.update(rowBounds.getOffset());
// rowBounds.limit
cacheKey.update(rowBounds.getLimit());
// sql语句
cacheKey.update(boundSql.getSql());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
for (int i = 0; i < parameterMappings.size(); i++) { // mimic DefaultParameterHandler logic
ParameterMapping parameterMapping = parameterMappings.get(i);
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) {
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
// 传递的每一个参数
cacheKey.update(value);
}
}
return cacheKey;
}

所以影响Cachekey是否相同的因素有:statementId,offset,limit,sql语句,参数

接下来进入cacheKey.update方法,看它如何处理以上这五个元素的:

  private void doUpdate(Object object) {
// 获取对象的HashCode
int baseHashCode = object == null ? 1 : object.hashCode();
// 计数器+1
count++;
checksum += baseHashCode;
// baseHashCode扩大count倍
baseHashCode *= count;
// 对HashCode进一步做处理
hashcode = multiplier * hashcode + baseHashCode;
// 把以上五个元素存放到集合中
updateList.add(object);
}

CahceKey的属性和构造方法:

private int multiplier;

private int hashcode;

private long checksum; 

  private int count;

  private List<Object> updateList;

public CacheKey() {
this.hashcode = DEFAULT_HASHCODE;
this.multiplier = DEFAULT_MULTIPLYER;
this.count = 0;
this.updateList = new ArrayList<Object>();
}

CacheKey中最重要的一个方法来了,如何判断两个CacheKey是否相等?

public boolean equals(Object object) {
if (this == object)
return true;
if (!(object instanceof CacheKey))
return false; final CacheKey cacheKey = (CacheKey) object;
// 判断HashCode是否相等
if (hashcode != cacheKey.hashcode)
return false;
// 判断checksum是否相等
if (checksum != cacheKey.checksum)
return false;
// 判断count是否相等
if (count != cacheKey.count)
return false;
// 逐一判断以上五个元素是否相等
for (int i = 0; i < updateList.size(); i++) {
Object thisObject = updateList.get(i);
Object thatObject = cacheKey.updateList.get(i);
if (thisObject == null) {
if (thatObject != null)
return false;
} else {
if (!thisObject.equals(thatObject))
return false;
}
}
return true;
}
// 只有以上所有的判断都相等时,两个CacheKey才相等

4.一级缓存的生命周期是多长?

开始:mybatis建立一次数据库会话时,就会生成一系列对象:SqlSession--->Executor--->PerpetualCache,也就开启了对一级缓存的维护。

结束: 

         * 会话结束,会释放掉以上生成的一系列对象,缓存也就不可用了。

* 调用sqlSession.close方法,会释放掉PerpetualCache对象,一级缓存不可用

* 调用sqlSession.clearCache方法,会清空PerpetualCache对象中的缓存数据,该对象可用,一级缓存不可用

* 调用sqlSession的update,insert,delete方法,会清空PerpetualCache对象中的缓存数据,该对象可用,一级缓存不可用

参考资料:https://blog.csdn.net/luanlouis/article/details/41280959

mybatis一级缓存详解的更多相关文章

  1. mybatis二级缓存详解

    1  二级缓存简介 二级缓存是在多个SqlSession在同一个Mapper文件中共享的缓存,它是Mapper级别的,其作用域是Mapper文件中的namespace,默认是不开启的.看如下图: 1. ...

  2. MyBatis 一级缓存、二级缓存全详解(一)

    目录 MyBatis 一级缓存.二级缓存全详解(一) 什么是缓存 什么是MyBatis中的缓存 MyBatis 中的一级缓存 初探一级缓存 探究一级缓存是如何失效的 一级缓存原理探究 还有其他要补充的 ...

  3. Mybatis 一级缓存和二级缓存原理区别 (图文详解)

    Java面试经常问到Mybatis一级缓存和二级缓存,今天就给大家重点详解Mybatis一级缓存和二级缓存原理与区别@mikechen Mybatis缓存 缓存就是内存中的数据,常常来自对数据库查询结 ...

  4. MyBatis(七):MyBatis缓存详解(一级缓存/二级缓存)

    一级缓存 ​ MyBatis一级缓存上SqlSession缓存,即在统一SqlSession中,在不执行增删改操作提交事务的前提下,对同一条数据进行多次查询时,第一次查询从数据库中查询,完成后会存入缓 ...

  5. MyBatis 一级缓存实现详解及使用注意事项

    一级缓存介绍 在应用运行过程中,我们有可能在一次数据库会话中,执行多次查询条件完全相同的SQL,MyBatis提供了一级缓存的方案优化这部分场景,如果是相同的SQL语句,会优先命中一级缓存,避免直接对 ...

  6. Mybatis一级缓存、二级缓存详讲

    Mybatis 一级缓存.二级缓存 作者 : Stanley 罗昊 [转载请注明出处和署名,谢谢!] 查询缓存 首先,我们先看一下这个标题“查询缓存”,那就说明跟增.删.改是没有任何关联的,只有在查询 ...

  7. MyBatis一级缓存(转载)

    <深入理解mybatis原理> MyBatis的一级缓存实现详解 及使用注意事项 http://demo.netfoucs.com/luanlouis/article/details/41 ...

  8. MyBatis核心配置文件详解

    ------------------------siwuxie095                                     MyBatis 核心配置文件详解         1.核心 ...

  9. Mybatis案例超详解(上)

    Mybatis案例超详解(上) 前言: 本来是想像之前一样继续跟新Mybatis,但由于种种原因,迟迟没有更新,快开学了,学了一个暑假,博客也更新了不少,我觉得我得缓缓,先整合一些案例练练,等我再成熟 ...

随机推荐

  1. DP h回文子串 LCS

    题目背景 IOI2000第一题 题目描述 回文词是一种对称的字符串.任意给定一个字符串,通过插入若干字符,都可以变成回文词.此题的任务是,求出将给定字符串变成回文词所需要插入的最少字符数. 比如 “A ...

  2. c# base64编码解码

    1.base64转pdf

  3. 网络协议 反扒机制 fidder 抓包工具

    协议 http 协议: client 端 server 端交互的 一种形式 请求头信息: User-Agent: 情求载体的身份标识 connection: 'close' 连接状态 请求成功后 断开 ...

  4. ElasticSearch(五):Java操作ElasticSearch执行查询

    package com.gxy.ESChap01; import java.net.InetAddress; import org.elasticsearch.action.search.Search ...

  5. java读取properties中文乱码

    1 确认properties文件的编码是utf-8 2 采用流的方式读取文件,设置编码为utf-8 public class ErrorCodeConfig { static Properties p ...

  6. Problem UVA11134-Fabled Rooks(贪心)

    Problem UVA11134-Fabled Rooks Accept: 716  Submit: 6134Time Limit: 3000mSec Problem Description We w ...

  7. unique

    作用: 元素去重,即“删除”序列中所有相邻的重复元素(只保留一个) (此处的删除,并不是真的删除,而是指重复元素的位置被不重复的元素占领了) (其实就是把多余的元素放到了最后面) 由于它“删除”的是相 ...

  8. 1、c++对c语言的扩展

    1.类型增强 检查更加严格 比如,把一个 const 类型的指针赋给非 const 类型的指针.c 语言中可以通的过,但是在 c++中则编不过去 ; int b = a; const int *pa ...

  9. Keil软件常见配置

    1.tab键占据字节数 Edit-->Configuration-->Tab Size-->安装上默认2个空格,这里改为4,符合通用代码编辑器的处理. 2.编码配置 Edit--&g ...

  10. 记一次Dubbo服务注册异常

            公司项目重构,把dubbo版本从2.5.8升级为2.6.2.升级后在本地运行一点问题都没有:可是通过公司自研的发布系统将项目发布到测试环境的linux服务器下面后,出现了dubbo服务 ...