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

相关推荐
凡人的AI工具箱2 小时前
15分钟学 Python 第38天 :Python 爬虫入门(四)
开发语言·人工智能·后端·爬虫·python
丶21362 小时前
【SQL】深入理解SQL:从基础概念到常用命令
数据库·后端·sql
木子02042 小时前
Nacos的应用
后端
哎呦没2 小时前
Spring Boot框架在医院管理中的应用
java·spring boot·后端
陈序缘3 小时前
Go语言实现长连接并发框架 - 消息
linux·服务器·开发语言·后端·golang
络73 小时前
Spring14——案例:利用AOP环绕通知计算业务层接口执行效率
java·后端·spring·mybatis·aop
2401_857600954 小时前
明星周边销售网站开发:SpringBoot技术全解析
spring boot·后端·php
AskHarries4 小时前
Spring Cloud 3.x 集成admin快速入门Demo
java·后端·spring cloud
程序员大金4 小时前
基于SpringBoot+Vue+MySQL的在线学习交流平台
java·vue.js·spring boot·后端·学习·mysql·intellij-idea
qq_2518364574 小时前
基于SpringBoot vue 医院病房信息管理系统设计与实现
vue.js·spring boot·后端