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查询订单表。

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

相关推荐
changshuaihua0012 小时前
Redis 详解:Redis 与 MySQL 的区别、数据类型与缓存三大问题
数据库·redis·缓存
梅孔立16 小时前
两种免费的翻译API调用方式详解 - Edge官方API与个人缓存服务
前端·缓存·edge
gwf21616 小时前
SSD读写速度深度解析:顺序读写vs随机读写、IOPS、延迟,你的硬盘性能到底怎么看?
git·嵌入式硬件·缓存·github·智能硬件
番茄炒鸡蛋加糖18 小时前
缓存三大问题
缓存
江晓鱼未暖1 天前
十七、Redis 核心原理与架构详解
大数据·数据库·数据仓库·redis·缓存·架构
小罗水1 天前
第13章 Redis 缓存、幂等锁与任务状态
数据库·redis·缓存
热心市民lcj1 天前
Spring Boot 整合 Caffeine 本地缓存实战
spring boot·后端·缓存
zx1154501 天前
【热点 Key 探测与加长缓存 TTL 实战】
缓存
阿维的博客日记1 天前
Java事务传播级别REQUIRES_NEW,REQUIRED_NEW区别
java·数据库·事务
momo2 天前
Java+WebAI黑马知识点
java·spring boot·mybatis