为了增加查询的性能,mybatis 提供了二级缓存架构,分为一级缓存和二级缓存。

这两级缓存最大的区别就是:一级缓存是会话级别的,只要出了这个 SqlSession,缓存就没用了。而二级缓存可以跨会话,多个会话可以使用相同的缓存!

一级缓存使用简单,默认就开启。二级缓存需要手动开启,相对复杂,而且要注意的事项也多,否则可能有隐患。

一级缓存

应用场景

订单表与会员表是存在一对多的关系,为了尽可能减少join查询,进行了分阶段查询。即先查询出订单表,再根据member_id字段查询出会员表,最后进行数据整合。而如果订单表中存在重复的member_id,就会出现很多重复查询

针对这种情况,mybatis通过一级缓存来解决:在同一次查询会话(SqlSession)中如果出现相同的语句及参数,就会从缓存中取出,不再走数据库查询。

一级缓存只能作用于查询会话中,所以也叫做会话缓存

生效的条件

一级缓存要生效,必须满足以下条件条件:

  1. 必须是相同的会话
  2. 必须是同一个 mapper,即同一个 namespace
  3. 必须是相同的 statement,即同一个 mapper 中的同一个方法
  4. 必须是相同的 sql 和参数
  5. 查询语句中间没有执行 session.clearCache() 方法
  6. 查询语句中间没有执行 insert/update/delete 方法(无论变动记录是否与缓存数据有无关系)

与springboot集成时一级缓存不生效原因

因为一级缓存是会话级别的,要生效的话,必须要在同一个 SqlSession 中。但是与 springboot 集成的 mybatis,默认每次执行sql语句时,都会创建一个新的 SqlSession!所以一级缓存才没有生效。

当调用 mapper 的方法时,最终会执行到 SqlSessionUtils 的 getSqlSession 方法,在这个方法中会尝试在事务管理器中获取 SqlSession,如果没有开启事务,那么就会 new 一个 DefaultSqlSession。

所以说,即便在同一个方法中,通过同一个 mapper 连续调用两次相同的查询方法,也不会触发一级缓存。

解决与springboot集成时一级缓存不生效问题

在上面的代码中也看到了,mybatis 在查询时,会先从事务管理器中尝试获取 SqlSession,取不到才会去创建新的 SqlSession。所以可以猜测只要将方法开启事务,那么一级缓存就会生效。

加上 @Transactional 注解,看下效果:

没错,的确生效了。在代码中可以看到,从事务管理器中,获取到了 SqlSession:

再看看源码中是什么时候将 SqlSession 设置到事务管理器中的。

SqlSessionUtils 中,在获取到 SqlSession 后,会调用 registerSessionHolder 方法注册 SessionHolder 到事务管理器:

具体是在 TransactionSynchronizationManager 的 bindResource 方法中操作的,将 SessionHolder 保存到线程本地变量(ThreadLocal) resources 中,这是每个线程独享的。:

然后在下次查询时,就可以从这里取出此 SqlSession,使用同一个 SqlSession 查询,一级缓存就生效了。

所以基本原理就是:如果当前线程存在事物,并且存在相关会话,就从 ThreadLocal 中取出。如果没有事务,就重新创建一个 SqlSession 并存储到 ThreadLocal 当中,共下次查询使用。

至于缓存查询数据的地方,是在 BaseExecutor 中的 queryFromDatabase 方法中。执行 doQuery 从数据库中查询数据后,会立马缓存到 localCache(PerpetualCache类型) 中:

二级缓存

应用场景

业务系统中存在很多的静态数据如,字典表、菜单表、权限表等,这些数据的特性是不会轻易修改但又是查询的热点数据

一级缓存针对的是同一个会话当中相同SQL,并不适合这情热点数据的缓存场景。

为了解决这个问题引入了二级缓存,它脱离于会话之外,多个会话可以使用相同的缓存。

看一个例子:

@RestController
@RequestMapping("item")
public class ItemController {@Autowiredprivate ItemMapper itemMapper;@GetMapping("/{id}")public void getById(@PathVariable("id") Long id) {System.out.println("==================== begin ====================");Item item = itemMapper.selectById(id);System.out.println(JSON.toJSONString(item));}} 

当发送两次 get 请求时(两个不同的会话),通过日志可以发现第二次查询使用的是缓存

开启的方法

二级缓存需要手动来开启,mybatis 默认没有开启二级缓存。

1)在 yaml 中配置 cache-enabled 为 true

mybatis-plus:
  configuration:
    cache-enabled: true

2)Mapper 接口上添加 @CacheNamespace 注解

3)实体类实现 Serializable 接口

生效的条件

  1. 会话提交或关闭之后才会填充二级缓存
  2. 必须是同一个 mapper,即同一个命名空间
  3. 必须是相同的 statement,即同一个 mapper 中的同一个方法
  4. 必须是相同的 SQL 语句和参数
  5. 如果 readWrite=true(默认就是true),实体对像必须实现 Serializable 接口

缓存清除条件

  1. 只有修改会话提交之后,才会执行清空操作
  2. xml 中配置的 update 不能清空 @CacheNamespace 中的缓存数据
  3. 任何一种增删改操作都会清空整个 namespace 中的缓存

源码中是如何填充二级缓存的?

在生效条件中提到了,二级缓存必须要在会话提交或关闭之后,才能生效!

在查询到结果后,会调用 SqlSession 的 commit 方法进行提交(如果开启事务的话,提交 SqlSession 走的不是这里了,但最终填充二级缓存的地方是一样的。):

在此方法中,最终会调用到 TransactionalCache 的 flushPendingEntries 方法中填充二级缓存:

springboot 集成 mybatis 的话,如果没有开启事务,每次执行查询,都会创建新的 SqlSession,所以即使是在同一个方法中进行查询操作,那也是跨会话的。

查询时如何使用二级缓存?

在查询的时候,最终会调用 MybatisCachingExecutor 的 query 方法,里面会从 TransactionalCacheManager 中尝试根据 key 获取二级缓存的内容。

可以看到,这个 key 很长,由 mapper、调用的查询方法、SQL 等信息拼接而成,这也是为什么想要二级缓存生效,必须满足前面所说的条件。

如果能在二级缓存中查询到,就直接返回了,不需要访问数据库。

具体的调用层数实在太多,用到了装饰者模式,最终是在 PerpetualCache 中获取缓存的:

打印日志是在 LoggingCache 中:

为什么mybatis默认不开启二级缓存?

答案就是,不推荐使用二级缓存

二级缓存虽然能带来一定的好处,但是有很大的隐藏危害!

它的缓存是以 namespace(mapper) 为单位的,不同 namespace 下的操作互不影响。且 insert/update/delete 操作会清空所在 namespace 下的全部缓存。

那么问题就出来了,假设现在有 ItemMapper 以及 XxxMapper,在 XxxMapper 中做了表关联查询,且做了二级缓存。此时在 ItemMapper 中将 item 信息给删了,由于不同 namespace 下的操作互不影响,XxxMapper 的二级缓存不会变,那之后再次通过 XxxMapper 查询的数据就不对了,非常危险。

来看一个例子:

@Mapper
@Repository
@CacheNamespace
public interface XxxMapper {@Select("select i.id itemId,i.name itemName,p.amount,p.unit_price unitPrice " +"from item i JOIN payment p on i.id = p.item_id where i.id = #{id}")List<PaymentVO> getPaymentVO(Long id);}@Autowired
private XxxMapper xxxMapper;@Test
void test() {System.out.println("==================== 查询PaymentVO ====================");List<PaymentVO> voList = xxxMapper.getPaymentVO(1L);System.out.println(JSON.toJSONString(voList.get(0)));System.out.println("====================  更新item表的name ==================== ");Item item = itemMapper.selectById(1);item.setName("java并发编程");itemMapper.updateById(item);System.out.println("====================  重新查询PaymentVO ==================== ");List<PaymentVO> voList2 = xxxMapper.getPaymentVO(1L);System.out.println(JSON.toJSONString(voList2.get(0)));
}

上面的代码,test()方法中前后两次调用了 xxxMapper.getPaymentVO 方法,因为没有加 @Transactional 注解,所以前后两次查询,是两个不同的会话,第一次查询完后,SqlSession 会自动 commit,所以二级缓存能够生效;

然后在中间进行了 Item 表的更新操作,修改了下名称;

由于 itemMapper 与 xxxMapper 不是同一个命名空间,所以 itemMapper 执行的更新操作不会影响到 xxxMapper 的二级缓存;

再次调用 xxxMapper.getPaymentVO,发现取出的值是走缓存的,itemName 还是老的。但实际上 itemName 在上面已经被改了!

执行日志如下:

所以说,二级缓存的隐藏危害是比较大的,当有表关联时,一个不注意就会出问题,不建议使用。

mybatis二级缓存架构原理相关推荐

  1. mybatis 二级缓存失效_给我五分钟,带你彻底掌握MyBatis的缓存工作原理

    前言 在计算机的世界中,缓存无处不在,操作系统有操作系统的缓存,数据库也会有数据库的缓存,各种中间件如Redis也是用来充当缓存的作用,编程语言中又可以利用内存来作为缓存.自然的,作为一款优秀的ORM ...

  2. Mybatis的一、二级缓存的原理与使用、禁止指定方法的二级缓存与刷新缓存、Mybatis整合Ehcache、二级缓存的使用场景与局限性-day03

    目录 第一节 Mybatis的缓存 1.1 Mybatis的缓存理解 1.2 一级缓存 原理 使用与测试 1.3 二级缓存 原理 使用与测试 禁用指定方法的二级缓存 刷新缓存 总结 1.4 整合ehc ...

  3. Mybatis二级缓存原理

    记录是一种精神,是加深理解最好的方式之一. 最近看了下Mybatis的源码,分析了二级缓存的实现方式,在这里把他记下来.虽然这不复杂,对这方面的博客也有很多,写的也很好.但我坚信看懂了是其一,能够教别 ...

  4. Mybatis的一级缓存和二级缓存机制原理和区别

    程序中为什么使用缓存? 实际上适用于缓存的数据:经常查询并且不经常改变的,并且的数据的正确与否对最终结果影响不大的.不适用于缓存的数据:经常改变的数据,数据的正确与否对最终 结果影响很大的. Myba ...

  5. SpringBoot整合Redis配置MyBatis二级缓存

    目录 写在前面 源码获取 一.MyBatis缓存机制 1.1.一级缓存 1.2.二级缓存 二.集成Redis 2.1.安装Redis 2.2.项目引入Redis 2.2.1.Maven依赖 2.2.2 ...

  6. 深入了解MyBatis二级缓存

    深入了解MyBatis二级缓存 一.创建Cache的完整过程 我们从SqlSessionFactoryBuilder解析mybatis-config.xml配置文件开始: Reader reader ...

  7. MyBatis复习(六):MyBatis二级缓存

    Mybatis缓存分为一级缓存和二级缓存 MyBatis一级缓存是默认开启的,数据存储范围是SqlSession会话这个级别,当SqlSession关闭后,缓存就会被清除,生命周期非常短. MyBat ...

  8. Mybatis一级缓存、整合第三方缓存ehcache、Mybatis二级缓存

    8. 缓存 8.1 一级缓存 在对数据库的一次会话中,我们有可能会反复地执行完全相同的查询语句,如果不采取一些措施的话,每一次查询都会查询一次数据库,而我们在极短的时间内做了完全相同的查询,那么它们的 ...

  9. Mybatis 二级缓存简单示例

    简介 简单接收Mybatis中二级缓存的使用示例 概览 主要部分如下: 引入Maven依赖 SpringBoot配置文件配置.建库与初始化SQL语句 实体类与Mapper编写 测试 Maven依赖 完 ...

最新文章

  1. 学习Python往哪个方向发展好
  2. Linux性能监控和调试
  3. 【EventBus】EventBus 源码解析 ( 注册订阅者 | 订阅方法 | 查找订阅方法 )
  4. mysql更新一条语句_MySQL一条更新语句是如何执行的
  5. BugkuCTF-MISC题神奇宝贝
  6. 41. 理解ptr_fun、mem_fun和mem_fun_ref的来由
  7. JQuery插件iScroll实现下拉刷新,滚动翻页特效
  8. POJ2109-Power of Cryptography
  9. L7805CV-ASEMI三端稳压管L7805CV
  10. vs code 让界面占满全屏的快捷键
  11. Linux基础知识小结(一)
  12. mac全选文字的快捷键_mac全选的快捷键是什么?苹果电脑全选的快捷键是什么?...
  13. 如何让溢出的文字省略号显示
  14. python3笔记_Python3入门笔记
  15. Day020 - pdf合并与excel/csv读取写入
  16. 背书小程序正式上线啦
  17. 【资源分享】少女爱上姐姐汉化版游戏下载,附图文攻略
  18. Android 读取手机SD卡根目录下某个txt文件的文件内容
  19. DELMIA软件:使用机器人本体语言示教编程
  20. 星星之火-19:在基站没有给手机终端分配资源前,手机是如何先基站申请资源?随机接入过程!

热门文章

  1. python进行excle表格脱敏批处理
  2. mysql1251错误
  3. chrome 删除厌烦的桔梗导航
  4. web安全之暴力破解漏洞02
  5. 机器学习中的特征工程
  6. JAVA正则表达式(详细,转载内容)
  7. python语法讲解小白入门第二篇
  8. PHP多条件模糊查询代码查询,PHP多条件模糊查询
  9. 尺寸测量 亚像素_使用像素尺进行测量
  10. oracle rename 失败,Oracle Rename表