Mybatis的一级缓存踩坑

Mybatis一级缓存

首先是在双检锁的实现过程中,mybatis的两条一摸一样语句为什么第二条不执行。后续排查可能是mybatis的一级缓存导致不执行第二条SQL

解决办法是,通过更换mapper的接口名称使缓存的key不一致,就可以执行第二条SQL查询。

MyBatis 中的缓存机制是基于缓存键(Cache Key)的。每次查询的时候,MyBatis 会使用一个缓存键来查找是否已经有缓存的结果。缓存键的生成是根据以下几个因素:

  1. Mapped Statement 的 ID: 即 SQL 语句的唯一标识符。
  2. Statement Type: SQL 语句的类型,包括 SELECT、UPDATE、DELETE、INSERT。
  3. SQL 语句的参数: SQL 语句的参数值,这些参数值会被用于生成缓存键。
  4. RowBounds: 如果有使用 RowBounds 进行分页,RowBounds 也会用于生成缓存键。
  5. Environment ID: 数据源的环境 ID。

这些因素一起构成了一个缓存键,用于在缓存中查找或存储结果。当 MyBatis 执行一个查询时,它会首先生成一个缓存键,然后使用这个缓存键在缓存中查找是否已经有对应的结果。如果找到了缓存的结果,MyBatis 将直接返回缓存的结果,而不执行实际的 SQL 查询。

这个缓存键的生成方式确保了不同的查询会生成不同的缓存键,以防止查询结果被错误地从缓存中返回。

需要注意的是,一级缓存是 SqlSession 级别的,而二级缓存是 Mapper 级别的。一级缓存的生命周期是与 SqlSession 相关的,而二级缓存的生命周期是与整个应用程序的 Mapper 相关的。

需要注意的是,这与可重读的隔离级别是不一样的,可重复读是基于数据库的快照,而一级缓存仅仅是基于SQLSESSION的缓存。

SqlSession的坑

昨天线上出了一个问题,场景是有一个事务A,事务B,事务C。事务A包含事务B1和事务B2。采用@Transactional默认的Propagation.REQUIRED的隔离级别,当前隔离级别是如果存在事务就加入当前事务,若不存在事务,就新建事务执行。也就是说,

事务A以及事务B1以及事务B2都在一个大事务内。问题就来了,事务B2读取不到事务B1修改之后的值,直接就导致大事务A的数据产生错误。

事务B1以及事务B2的SQL为

sql 复制代码
-- SQL1
SELECT id, serial_no FROM COM_NO_SERIAL WHERE rule_id = '23e344f6206441dc8d2ae23197759e65' AND rule_format = 'YSPF-240308%s' FOR UPDATE

-- SQL2
UPDATE COM_NO_SERIAL SET serial_no = 2 , update_time = '2024-03-08 09:43:49'  WHERE id = '1d68eba2b64748cd953b16776ee0fb64'

按照数据库的事务来说,B1,B2都属于大事务A,A以及B1以及B2都在一个事务内,也同属一个SqlSession。B2肯定能获取到B1的更改。

由分析判断属于一个事务,那么在一个事务内依据数据库验证:隔离级别为:read committed,事务提交方式修改为手动

sql 复制代码
-- 在事务未提交的情况下依次执行一下SQL

SELECT id, serial_no FROM COM_NO_SERIAL WHERE rule_id = '23e344f6206441dc8d2ae23197759e65' AND rule_format = 'YSPF-240308%s' FOR UPDATE;
-- '1d68eba2b64748cd953b16776ee0fb64', 2
UPDATE COM_NO_SERIAL SET serial_no = 3 , update_time = '2024-03-08 09:43:50'  WHERE id = '1d68eba2b64748cd953b16776ee0fb64'

SELECT id, serial_no FROM COM_NO_SERIAL WHERE rule_id = '23e344f6206441dc8d2ae23197759e65' AND rule_format = 'YSPF-240308%s' FOR UPDATE;
-- '1d68eba2b64748cd953b16776ee0fb64', 3
UPDATE COM_NO_SERIAL SET serial_no = 3 , update_time = '2024-03-08 09:43:50'  WHERE id = '1d68eba2b64748cd953b16776ee0fb64'

由以上结构证明在同一事物内,绝对是可以获取到自身事务修改后的数据的,无论当前事务是否提交。那么为什么在代码中,事务B2为啥读取不到B1的数据呢?下面编写单元测试验证,采用循环调用去查询与更新操作

csharp 复制代码
  	// 调用方  
	@Transactional(rollbackFor = Exception.class)
    public void testNo() {
		// 循环十次,相当于有十个事务加入到大事务中
        for (int i = 0; i < 10; i++) {
            String billNo = xxxService.executeService();
        }
    }

	// 业务方executeService();
	public String executeService() {
		// SQL1
    	XXX xxx = comNoSerialMapper.selectXXX(XXX, XXX);
		Long xxx = xxx.get() + 1L;
		// SQL2
	    UpdateChain.of(XXX.class)
			.set(XXX::xxx, xxx)
		    .set(XXX::UpdateTime, DateUtil.formatDate(new Date()))
		    .where(XXX::Id).eq(xxx.Id())
		    .update();
	}

通过单元测试的日志发现,SQL1只在首次执行了一次,后续的SQL日志都是SQL2的更新日志。为什么SQL1只在首次执行呢?

排查数据库的隔离级别,并且在同一事务内,那么应该就是Mybatis的一级缓存问题。一级缓存是默认开启的,由于在同一个事务内,也就是同一个SqlSession会话内,肯定是会存在查询缓存的,并且只会查询一次

有两种解决方案:

  1. 修改事务隔离级别:从默认的required改为required_new每次都新建一个事务执行。事务新建之后会自动提交,所以每次事务查询都会是从数据库获取的最新值。
  2. 使一级缓存的失效:一级缓存默认会在同一会话进行增删改后失效。(下文再提及为什么执行了修改一级缓存也没有失效的问题)或者就是关闭默认的一级缓存。

为什么执行了修改,SqlSession的一级缓存没有失效,仍然存在?

通过业务代码可以看出,SQL2是通过工具类去更新,类似于直接SQL,这种可能就没有触发清除一级缓存。因为mapper​是代理对象在切面清理缓存,个人推测应该至少通过mapper​方式去执行更新的方式才能清除掉一级缓存,避免数据不一值。

相关推荐
摇滚侠3 小时前
Spring Boot 3零基础教程,IOC容器中组件的注册,笔记08
spring boot·笔记·后端
程序员小凯6 小时前
Spring Boot测试框架详解
java·spring boot·后端
你的人类朋友6 小时前
什么是断言?
前端·后端·安全
程序员小凯7 小时前
Spring Boot缓存机制详解
spring boot·后端·缓存
i学长的猫8 小时前
Ruby on Rails 从0 开始入门到进阶到高级 - 10分钟速通版
后端·ruby on rails·ruby
用户21411832636028 小时前
别再为 Claude 付费!Codex + 免费模型 + cc-switch,多场景 AI 编程全搞定
后端
茯苓gao8 小时前
Django网站开发记录(一)配置Mniconda,Python虚拟环境,配置Django
后端·python·django
Cherry Zack8 小时前
Django视图进阶:快捷函数、装饰器与请求响应
后端·python·django
爱读源码的大都督9 小时前
为什么有了HTTP,还需要gPRC?
java·后端·架构
码事漫谈9 小时前
致软件新手的第一个项目指南:阶段、文档与破局之道
后端