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​方式去执行更新的方式才能清除掉一级缓存,避免数据不一值。

相关推荐
不再幻想,脚踏实地22 分钟前
Spring AOP从0到1
java·后端·spring
编程乐学(Arfan开发工程师)23 分钟前
07、基础入门-SpringBoot-自动配置特性
java·spring boot·后端
会敲键盘的猕猴桃很大胆39 分钟前
Day11-苍穹外卖(数据统计篇)
java·spring boot·后端·spring·信息可视化
极客智谷41 分钟前
Spring Cloud动态配置刷新:@RefreshScope与@Component的协同机制解析
后端·spring·spring cloud
Lizhihao_1 小时前
Spring MVC 接口的访问方法如何设置
java·后端·spring·mvc
Code哈哈笑5 小时前
【图书管理系统】用户注册系统实现详解
数据库·spring boot·后端·mybatis
用手手打人5 小时前
SpringBoot(一)--- Maven基础
spring boot·后端·maven
Code哈哈笑7 小时前
【基于Spring Boot 的图书购买系统】深度讲解 用户注册的前后端交互,Mapper操作MySQL数据库进行用户持久化
数据库·spring boot·后端·mysql·mybatis·交互
Javatutouhouduan7 小时前
线上问题排查:JVM OOM问题如何排查和解决
java·jvm·数据库·后端·程序员·架构师·oom
多多*8 小时前
Spring之Bean的初始化 Bean的生命周期 全站式解析
java·开发语言·前端·数据库·后端·spring·servlet