本篇博客主要讲了 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 源码分析(四)一二级缓存分析的更多相关文章

  1. Mybatis源码解析(四) —— SqlSession是如何实现数据库操作的?

    Mybatis源码解析(四) -- SqlSession是如何实现数据库操作的?   如果拿一次数据库请求操作做比喻,那么前面3篇文章就是在做请求准备,真正执行操作的是本篇文章要讲述的内容.正如标题一 ...

  2. Mybatis源码研究7:缓存的设计和实现

    Mybatis源码研究7:缓存的设计和实现 2014年11月19日 21:02:14 酷酷的糖先森 阅读数:1020   版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog ...

  3. mybatis源码学习:一级缓存和二级缓存分析

    目录 零.一级缓存和二级缓存的流程 一级缓存总结 二级缓存总结 一.缓存接口Cache及其实现类 二.cache标签解析源码 三.CacheKey缓存项的key 四.二级缓存TransactionCa ...

  4. Mybatis源码学习第六天(核心流程分析)之Executor分析

    今Executor这个类,Mybatis虽然表面是SqlSession做的增删改查,其实底层统一调用的是Executor这个接口 在这里贴一下Mybatis查询体系结构图 Executor组件分析 E ...

  5. 手把手带你阅读Mybatis源码(三)缓存篇

    前言 大家好,这一篇文章是MyBatis系列的最后一篇文章,前面两篇文章:手把手带你阅读Mybatis源码(一)构造篇 和 手把手带你阅读Mybatis源码(二)执行篇,主要说明了MyBatis是如何 ...

  6. mybatis源码探索笔记-4(缓存原理)

    前言 mybatis的缓存大家都知道分为一级和二级缓存,一级缓存系统默认使用,二级缓存默认开启,但具体用的时候需要我们自己手动配置.我们依旧还是先看一个demo.这儿只贴出关键代码 public in ...

  7. 浩哥解析MyBatis源码(四)——DataSource数据源模块

    原创作品,可以转载,但是请标注出处地址:http://www.cnblogs.com/V1haoge/p/6634880.html 1.回顾 上一文中解读了MyBatis中的事务模块,其实事务操作无非 ...

  8. mybatis源码学习(三)-一级缓存二级缓存

    本文主要是个人学习mybatis缓存的学习笔记,主要有以下几个知识点 1.一级缓存配置信息 2.一级缓存源码学习笔记 3.二级缓存配置信息 4.二级缓存源码 5.一级缓存.二级缓存总结 1.一级缓存配 ...

  9. Mybatis源码学习第六天(核心流程分析)之Executor分析(补充)

    补充上一章没有讲解的三个Executor执行器; 还是贴一下之前的代码吧;我发现其实有些分析注释还是写在代码里面比较好,方便大家理解,之前是我的疏忽,不好意思 @Override public < ...

  10. mybatis源码解读(四)——事务的配置

    上一篇博客我们介绍了mybatis中关于数据源的配置原理,本篇博客介绍mybatis的事务管理. 对于事务,我们是在mybatis-configuration.xml 文件中配置的: 关于解析 < ...

随机推荐

  1. javaee+tomcat新特性,乱码问题

    Tomcat版本问题,servlet乱码问题 我在学习的时候,老师用的是Tomcat1.7版本,在jsp发送get请求的时候,Servlet中还要对get请求传递过来的参数进行解码编码,因为tomca ...

  2. c语言进阶3-有参函数

    一.       有参函数的定义 有参函数的定义格式如下: 类型标识符  函数名(形式参数表列) { 语句: } 如 void fun(int a,int b) { printf(“a+b=%d”,a ...

  3. MySQL常用工具、日志及读写分离

    MySQL常用工具.日志及读写分离 1.MySQL中常用工具 1.1 mysql 1.1.1连接选项 1.1.2 执行选项 1.2 mysqladmin 1.3 mysqlbinlog 1.4 mys ...

  4. python基础之list列表的增删改查以及循环、嵌套

    Python的列表在JS中又叫做数组,是基础数据类型之一,以[]括起来,以逗号隔开,可以存放各种数据类型.嵌套的列表.对象.列表是有序的,即有索引值,可切片,方便取值.列表的操作和对字符串的操作是一样 ...

  5. web-fragment模块化使用

    用eclipse右键new->other->web->web fragment project 确定后修改dynamic web project name为你要输出到的项目,当然可以 ...

  6. php之布尔类型判断

    字符串只要不为空且不为0都为true 执行结果为 执行结果为false 因为===不仅比较值,还比较类型,所以输出为false.如果使用===号比较,最好先将变量强转为bool类型,不然可能得不到想要 ...

  7. [系列] Go - chan 通道

    目录 概述 声明 chan 写入 chan 读取 chan 关闭 chan 示例 推荐阅读 概述 原来分享基础语法的时候,还未分享过 chan 通道,这次把它补上. chan 可以理解为队列,遵循先进 ...

  8. hdoj 4712 Hamming Distance(靠人品过的)

    我先解释一下汉明距离  以下来自百度百科 在信息论中,两个等长字符串之间的汉明距离是两个字符串对应位置的字符不同的个数.换句话说,它就是将 一个字符串变换成另外一个字符串所需要替换的字符个数. 例如: ...

  9. windows server 2008 R2中建立ftp站点

    在windows server 2008 R2中建立ftp站点,要遵循以下步骤: (1) 开启IIS中的ftp服务: (2) 在IIS中建立ftp站点. 具体过程如下: (1) 开启IIS中的ftp服 ...

  10. 后端基于方法的权限控制--Spirng-Security

    后端基于方法的权限控制--Spirng-Security 默认情况下, Spring Security 并不启用方法级的安全管控. 启用方法级的管控后, 可以针对不同的方法通过注解设置不同的访问条件: ...