踩坑日记:Mybatis一级缓存

背景

在一个平平无奇的下午,发现线上有个出现了一条死信息,排查日志发现原因是,消息中某个对象的状态为A,但是查询数据发现状态已经变成了B,意识到不对,赶紧查看代码,结果发现一个很奇怪的现象,代码的实现逻辑如下:

  1. 对象加锁
  1. 查询数据库对象状态,如果为A,发送消息。

这就奇怪了,明明实现逻辑中对象加锁了,并且查询状态为A了,对象状态的变更都需要获取锁才能变更,感觉像是锁的问题,一段排查下发现不是锁的问题,变更为状态B是也有加锁,并且是在问题逻辑时间之前已经完成变更状态并且解锁了。

后面通过公司trace全链路打点排查,发现竟然在步骤1.对象加锁后,并没有执行查询数据库的sql打点,顿感奇怪,询问老司机一起排查后,发现竟然是Mybatis一级缓存导致。

问题原因

下面直接先说明下为什么Mybatis一级缓存或导致上述问题,在执行加锁逻辑之前,提前查询数据判断了对象状态是否为A, 代码实现可以简单概括如下:

java 复制代码
数据库:mysql 事务隔离级别设置为读已提交

ORM框架:Mybatis

@Transactional(...//事务隔离级别使用数据库默认, 数据库事务隔离级别为读已提交)

public void doSomething() {

// 第一次查询数据库获取对象

Object obj1 = mapper.queryById(id);

....逻辑省略

lock(obj1)

....逻辑省略

// 第二次查询数据库获取对象, sql与第一次查询完全一致

Object obj2 = mapper.queryById(id);

// 校验状态是否为A, 如果为A做一些业务处理

if (statusIsA(obj2)) {

// 执行业务逻辑

....

}

}

第一次查询obj1时对象的状态为A, 在加锁之前,其他业务逻辑中将对象的状态修改成为了B, 然后再第二次查询对象obj2时,触发了mybatis缓存机制,并未实际查询数据库,而是直接从缓存中取值,也就是前面第一次查询返回的对象obj, 没错obj1与obj2是完全相同的对象,obj1 == obj2, 这就导致了statusIsA(obj2)判断状态是否仍未A时,返回了true,而状态实际已经修改了成了B,导致接下来的处理逻辑与预期不一致。

说实话之前并没有了解到过mybatis缓存相关的知识,因此立即查阅资料学习了下mybatis缓存相关的知识。

Mybatis缓存机制

MyBatis 提供了两级缓存机制来提高应用程序的性能:

  1. 一级缓存(Local Cache)
  1. 二级缓存(Second Level Cache)

一级缓存(Local Cache)

  • 作用范围 :同一 SqlSession
  • 缓存位置SqlSession 对象内部
  • 缓存行为
    • 读取:当您执行查询时,MyBatis 会先检查一级缓存是否存在结果集。如果存在,直接返回缓存结果。
    • 写入 :当您执行增删改操作(insertupdatedelete)时,MyBatis 会清空当前 SqlSession 的一级缓存。
    • 生命周期 :一级缓存的生命周期与 SqlSession 相同。当 SqlSession 关闭或提交时,一级缓存被清空。
  • 配置:一级缓存是默认启用的,无需额外配置。

二级缓存(Second Level Cache)

  • 作用范围 :同一 Mapper 接口或同一 namespace 内的所有 SqlSession
  • 缓存位置Mapper 接口或 namespace 级别
  • 缓存行为
    • 读取:当一级缓存未命中时,MyBatis 会检查二级缓存是否存在结果集。如果存在,直接返回缓存结果。
    • 写入 :当 SqlSession 提交或关闭时,如果配置了二级缓存,MyBatis 会将结果写入二级缓存。
    • 生命周期:二级缓存的生命周期取决于配置,可以是整个应用程序的生命周期,也可以根据配置定期清空。
  • 配置 :需要在 mapper 配置文件中显式启用二级缓存。

MyBatis 查询时 SqlSession 的创建和复用机制

在 MyBatis 中,SqlSession 是一个关键对象,负责执行 SQL 查询和操作数据库。了解 SqlSession 的创建和复用机制,有助于您更好地使用 MyBatis。下面我们来探讨 MyBatis 查询时 SqlSession 的创建和复用规则。

SqlSession 的作用
  • 执行 SQL : SqlSession 负责将 MyBatis 配置文件(如 XML 或注解)中定义的 SQL 语句发送到数据库执行。
  • 管理事务 : SqlSession 提供了对数据库事务的管理,包括提交、回滚等。
  • 缓存管理 : SqlSession 也参与 MyBatis 的缓存机制,包括一级缓存(本地缓存)和二级缓存(全局缓存)。
SqlSession 的创建和复用规则

1. 默认行为:每次查询新建 SqlSession

  • 无显式事务管理 : 如果您没有显式地管理事务(如使用 @Transactional 注解或手动调用 sqlSession.commit()/sqlSession.rollback()),MyBatis 会为每个查询创建一个新的 SqlSession
  • 示例:
java 复制代码
// 每次调用 selectUser() 都会新建一个 SqlSession
User user = sqlMapper.selectUser(id);

2. 显式事务管理:复用 SqlSession

  • 使用 @Transactional 注解: 当您使用 Spring 的 @Transactional 注解(或其他事务管理框架)包裹您的服务方法时,MyBatis 会在事务范围内复用 SqlSession
  • 手动事务管理: 如果您手动调用 sqlSession.commit()sqlSession.rollback() 来管理事务,同样会在事务范围内复用 SqlSession
3. 线程绑定(ThreadLocal)
  • MyBatis 内部机制 : MyBatis 使用 ThreadLocal 机制来存储当前线程的 SqlSession。这意味着,如果在同一线程内执行多个查询,MyBatis 会自动复用 SqlSession,除非您显式地关闭或提交了事务。
4. 手动关闭 SqlSession
  • 显式关闭 : 如果您手动调用 sqlSession.close(),则当前 SqlSession 将被关闭,不再被复用。

问题反思与总结

通过对mybitis缓存机制学习了解到,mybatisi的1级缓存其实是默认开启了的,并且只有在使用相同SqlSession并且查询sql完全相同是才会跳过查询数据库,直接使用缓存。并且一般情况下,通过mybatis mapper查询数据库,每次查询都会新建sqlSession, 因此一般情况下重复查询多次数据库其实没有使用到mybatis1级缓存。而开启事务时,在事务范围内才会复用SqlSession,此时则可能会使用缓存。

而我遇到的问题正式如此,开了个大事务,然后事务内多次查询,第二次查询开始返回的为缓存结果,这里其实可以通过减小事务的范围来解决,血的教训阿,以后再不随便框个大事务了。

额外提一点:只要使用了@Transactional注解,就有可能复用sqlSession,本人这个case中其实事务注解配置的transactionManger并非查询sql访问数据库的事务管理器,但是查询时也复用同一个sqlSession.

相关推荐
Bling_3 分钟前
Springboot Bean创建流程、三种Bean注入方式(构造器注入、字段注入、setter注入)、循坏依赖问题
java·spring boot·spring·容器
开疆智能17 分钟前
机器人技术:ModbusTCP转CCLINKIE网关应用
java·服务器·科技·机器人·自动化
心向阳光的天域36 分钟前
黑马跟学.苍穹外卖.Day03
java·开发语言·spring boot
对酒当歌丶人生几何38 分钟前
SpringBoot实现国际化
java·spring boot·后端·il8n
雪芽蓝域zzs39 分钟前
JavaWeb开发(九)JSP技术
java·开发语言
上海拔俗网络1 小时前
“智能筛查新助手:AI智能筛查分析软件系统如何改变我们的生活
java·团队开发
代码代码快快显灵1 小时前
Redis之秒杀活动
数据库·redis·缓存·秒杀活动
weixin_437398211 小时前
Elasticsearch学习(1) : 简介、索引库操作、文档操作、RestAPI、RestClient操作
java·大数据·spring boot·后端·学习·elasticsearch·全文检索
MasterNeverDown1 小时前
spring boot controller放到那一层
java·spring boot·后端
Yang-Never1 小时前
Kotlin->Kotlin协程的取消机制
android·java·开发语言·kotlin·android studio·idea