MyBatis 的一级缓存导致的数据一致性问题分析

老生常谈的异常问题,这里记录一下,涉及MyBatis 的一级缓存和数据库隔离级别

目录

问题说明

下面一段示例的业务逻辑代码:

java 复制代码
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
@Override
public void flushOrderDetail(FlushForm form) {
    // 构建批量更新订单状态的参数集合
    List<FlushOrderParam> flushOrderParamList = payService.createFlushParam(form);
    
    // 查询出本次需要更新的订单记录
    List<OrderEntity> orderList = orderService.query(form);

    SqlSession sqlSession = null;
    try {
        // 采用MyBatis批处理模式,开启批量执行的SqlSession
        sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);

        // 批量处理订单状态信息
        orderService.batchUpdate(sqlSession, flushOrderParamList, orderList);

        // 提交批量处理事务
        sqlSession.commit();
    } catch (Exception e) {
        // 出现异常时,回滚批处理操作
        log.error("批处理订单状态失败", e);
        if (sqlSession != null) sqlSession.rollback();

        // 将异常抛出,触发Spring事务管理的回滚机制
        throw e;
    } finally {
        // 关闭SqlSession,释放资源
        if (sqlSession != null) {
            try {
                sqlSession.close();
            } catch (Exception e) {
                log.error("关闭sqlSession异常", e);
            }
        }
    }

    // 批量操作成功后,再次查询订单,打印订单状态日志,便于排查和验证
    orderList = orderService.query(form);
    for (OrderEntity order : orderList) {
        logger.debug("订单 {} 状态 {}", order.getOrderCode(), order.getStatus());
    }
}

上面这段Spring业务代码主要内容就是想根据参数批量更新订单状态,里面使用了Spring的事务注解,同时方法中另外开启了一个数据库会话用于批处理更新,这样可以加快速度。

这段代码连接的数据库是 MySQL, 且事务隔离级别为 提交读 (READ-COMMITTED),查询MySQL事务隔离方法如下:

sql 复制代码
SELECT @@session.transaction_isolation;

问题现象:
orderList = orderService.query(form); 查询出来的订单状态没有变化,但是数据库中已经更新 !

问题原因

并非AI 给出的回答:因为两个不同的事务管理器导致不一样的结果...

首先,在提交读的隔离级别下,即便不同的事务管理器也可以相互读取到对方数据库会话 已经提交的事务数据

那为什么读取到的最后状态没有变化? 因为 MyBatis 一级缓存导致

  • MyBatis 一级缓存默认绑定在一个 SqlSession 的生命周期内下,
  • 上面代码中的开头和结尾的 orderService.query(form) 是在同一个SqlSession生命周期下
  • 且查询参数一样,这样导致两次查询结果一样,但数据库中其实已经状态更新了

问题解决

修改一级缓存的隔离级别为 ·statement· 级别,这样等同于关闭一级缓存。

yml 复制代码
mybatis:
  type-aliases-package: com.middol.*.model.**.dao
  mapper-locations: classpath:mapper/**/*Mapper.xml
  configuration:
    map-underscore-to-camel-case: true # 开启驼峰功能
    local-cache-scope: statement

主要关注 local-cache-scope: statement

或者上面代码放弃使用批处理模式,采用同一个SqlSession下操作数据库,

或者直接使用批处理的SqlSession查询订单表。

到此,问题原因和处理说明完毕!

相关推荐
Wang's Blog42 分钟前
Linux小课堂: Squid代理缓存服务器部署与访问控制实战指南
linux·服务器·缓存
洲覆4 小时前
缓存异常:缓存穿透、缓存击穿、缓存雪崩
开发语言·数据库·mysql·缓存
Deamon Tree4 小时前
如何保证缓存与数据库更新时候的一致性
java·数据库·缓存
..过云雨5 小时前
11.【Linux系统编程】文件系统详解——从磁盘硬件到文件系统
linux·c++·后端·缓存
bst@微胖子7 小时前
阿里通义千问推理优化上下文缓存之隐式缓存和显式缓存
java·spring·缓存
新手小白*7 小时前
Redis Cluster集群理论
数据库·redis·缓存
无聊的小坏坏8 小时前
Redis 从基础到实战
数据库·redis·缓存
m0_5642641817 小时前
IDEA DEBUG调试时如何获取 MyBatis-Plus 动态拼接的 SQL?
java·数据库·spring boot·sql·mybatis·debug·mybatis-plus
天硕国产存储技术站21 小时前
国产固态硬盘推荐:天硕工业级SSDDRAM缓存与HMB技术详解
缓存·固态硬盘·国产ssd·国产ssd品牌
Zhangzy@21 小时前
Rust 内存对齐与缓存友好设计
spring·缓存·rust