当前位置: 首页 > news >正文

记一次事务中更新与查询数据不一致的问题分析

前言

在开发过程中,事务的使用是保障数据一致性的核心机制。然而,事务的隔离级别、传播行为以及框架的实现细节可能会导致一些“反直觉”的问题。

一、问题背景

1.1 业务场景

在某个基于 Spring Boot 和 MyBatis Plus 的项目中,开发人员需要实现以下功能:

  1. 更新某条记录的 score 字段;
  2. 查询该记录中 scoreNULL 的记录数量。

代码大致如下:

@Service
public class TocTopicApplyUserService {@Autowiredprivate TocTopicApplyUserMapper tocTopicApplyUserMapper;@Transactionalpublic void updateAndCount(Long topicApplyId) {// 更新操作TocTopicApplyUser user = new TocTopicApplyUser();user.setId(1L);user.setScore(85);tocTopicApplyUserMapper.updateById(user);// 查询操作long count = tocTopicApplyUserMapper.selectCount(new LambdaQueryWrapper<TocTopicApplyUser>().eq(TocTopicApplyUser::getTopicApplyId, topicApplyId).isNull(TocTopicApplyUser::getScore));}
}

1.2 问题现象

  • 更新操作执行后,score 字段确实被赋值为 85;
  • 但后续的 count 查询结果为 0,而直接在数据库客户端中执行相同的 SQL 语句却返回 1。

二、根本原因分析

2.1 事务的隔离级别

数据库事务的隔离级别决定了事务内部操作对其他事务或自身后续操作的可见性。MySQL 的默认隔离级别是 REPEATABLE READ,其特性如下:

  • 保证同一个事务内多次读取的结果一致
  • 不会看到其他事务未提交的更改
  • 即使同一事务内的更新操作已完成,后续查询也可能看不到更新后的数据(取决于数据库实现)。

在上述代码中:

  • 更新操作和查询操作处于同一个事务中;
  • 由于事务未提交,score 字段的更新尚未持久化到数据库;
  • 查询操作可能基于事务的快照(MVCC 机制)读取数据,导致无法看到更新后的值。

2.2 Spring 的事务传播行为

Spring 的 @Transactional 注解默认使用 Propagation.REQUIRED,即:

  • 如果当前存在事务,则加入该事务;
  • 事务的提交由 Spring 容器统一管理,直到方法执行完毕才会提交。

因此,更新和查询操作共享同一个事务上下文,但数据库可能因隔离级别限制,未将更新结果对后续查询可见。

2.3 MyBatis Plus 的 count 方法

MyBatis Plus 的 selectCount 方法本质上是基于数据库的 COUNT 操作:

  • 如果数据库的统计信息未更新(如 Hive 中的元数据缓存),可能导致结果异常;
  • 但在 MySQL 等支持事务的数据库中,问题更多与事务的隔离级别相关。

三、解决方案

3.1 调整事务的隔离级别

将事务的隔离级别改为 READ COMMITTED,确保更新操作对后续查询可见:

@Transactional(isolation = Isolation.READ_COMMITTED)
public void updateAndCount(Long topicApplyId) {// 更新和查询逻辑
}
  • READ COMMITTED 的特性:
    • 每次读取操作都会看到已提交的更改;
    • 可能导致不可重复读(但能解决当前问题)。

3.2 手动提交事务

如果业务允许,可以在更新操作后手动提交事务,确保数据持久化:

@Autowired
private PlatformTransactionManager transactionManager;
private TransactionStatus transactionStatus;public void updateAndCount(Long topicApplyId) {// 开启事务transactionStatus = transactionManager.getTransaction(new DefaultTransactionDefinition());try {// 更新操作tocTopicApplyUserMapper.updateById(user);// 手动提交事务transactionManager.commit(transactionStatus);// 查询操作(此时在独立事务中)long count = tocTopicApplyUserMapper.selectCount(...);} catch (Exception e) {transactionManager.rollback(transactionStatus);throw e;}
}

3.3 使用 SELECT FOR UPDATE 锁定记录

在查询时加锁,强制数据库读取最新数据:

long count = tocTopicApplyUserMapper.selectCount(new LambdaQueryWrapper<TocTopicApplyUser>().eq(TocTopicApplyUser::getTopicApplyId, topicApplyId).isNull(TocTopicApplyUser::getScore).last("FOR UPDATE")
);
  • 注意FOR UPDATE 会锁定记录,需谨慎使用以避免死锁。

3.4 验证 SQL 日志

通过开启 MyBatis Plus 的 SQL 日志,确认实际执行的 SQL 与预期一致:

mybatis-plus:configuration:log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

四、相关知识点总结

4.1 事务的 ACID 特性

  • 原子性(Atomicity):事务中的操作要么全部成功,要么全部失败;
  • 一致性(Consistency):事务执行前后,数据保持一致;
  • 隔离性(Isolation):多个事务并发执行时,彼此隔离;
  • 持久性(Durability):事务提交后,数据持久化到磁盘。

4.2 事务的隔离级别

隔离级别脏读不可重复读幻读适用场景
READ UNCOMMITTED无要求的高并发读写场景
READ COMMITTED大多数 OLTP 业务
REPEATABLE READ默认隔离级别(MySQL)
SERIALIZABLE严格一致性要求的场景(如金融)

4.3 Spring 事务传播行为

传播行为含义
REQUIRED如果存在事务则加入,否则新建事务(默认)
REQUIRES_NEW总是新建事务,与当前事务无关
NEVER不允许存在事务,否则抛异常
MANDATORY必须存在事务,否则抛异常

4.4 MyBatis Plus 的事务管理

  • MyBatis Plus 本身不直接管理事务,依赖 Spring 的事务机制;
  • count 方法的执行结果受数据库隔离级别和事务可见性影响;
  • 使用 @TableField 注解时,需确保字段映射正确,避免查询条件错误。

五、注意事项与最佳实践

  1. 事务隔离级别的选择

    • 根据业务需求选择合适的隔离级别,避免过度牺牲性能;
    • 在需要读取最新数据的场景中,优先使用 READ COMMITTED
  2. 事务的粒度控制

    • 避免大事务,减少锁竞争和资源占用;
    • 对于需要独立提交的逻辑,使用 REQUIRES_NEW 传播行为。
  3. SQL 日志的调试

    • 开启 SQL 日志,验证框架生成的 SQL 是否符合预期;
    • 直接在数据库客户端中执行 SQL,确认数据状态。
  4. MyBatis Plus 的字段映射

    • 使用 @TableField 明确字段与数据库列的映射关系;
    • 避免因字段名不一致导致查询条件失效。

你的问题涉及到 事务隔离级别MVCC(多版本并发控制) 的核心机制。以下是详细分析和解决方案:


问题核心:事务内更新后,查询为何看不到更新后的数据?

1.1 事务隔离级别与 MVCC 的作用
  • MySQL 默认隔离级别REPEATABLE READ
  • MVCC 机制:在 REPEATABLE READ 下,事务第一次查询时会生成一个 ReadView,后续查询复用该 ReadView。即使事务内更新了数据,只要事务未提交,新版本的数据对后续查询 不可见
1.2 为什么代码中 count 返回 0?
  • 事务内更新:你执行了 updateById(user),但事务未提交。
  • MVCC 快照:事务内的 count 查询基于事务开始时的 ReadView,无法看到事务内更新的新版本数据。
  • 数据库实际状态:事务未提交,数据仍为 NULL,但代码逻辑中因 MVCC 快照,count 查询误判为 0

关键知识点

1 事务隔离级别与 MVCC
隔离级别特性
READ COMMITTED每次查询生成新的 ReadView,能看到其他事务已提交的更改。
REPEATABLE READ第一次查询生成 ReadView,后续查询复用该视图,避免不可重复读。
2 MVCC 的 ReadView 机制
  • ReadView 包含当前活跃事务的 ID 列表。
  • 可见性规则:只有事务 ID 小于当前 ReadView 的记录才可见。
  • 事务内更新:新版本的事务 ID 会大于当前 ReadView,导致后续查询不可见。
3 Spring 事务传播行为
传播行为含义
REQUIRED加入当前事务(默认)。
REQUIRES_NEW新建事务,独立提交。
http://www.dtcms.com/a/264605.html

相关文章:

  • HTTP 协议深入理解
  • Git 分支与远程仓库基础教学总结
  • sudo本地提权漏洞(CVE-2025-32462)
  • S7-1200 PN与G120变频器控制起停及调速PROFINET实现详解
  • 微信小程序能不能获取物联网的上的设备数据
  • 在 proteus8或者proteus 9 中查看 micropython 的 print 输出
  • Redis搭建集群模式
  • 【WEB】Polar靶场 笔记
  • C++主流编辑器特点比较
  • 【HDMI CEC Menu Tunneling (菜单穿越) 功能详解
  • Stereolabs ZED系列与ZED X立体相机系列对比:如何根据项目需求选择?
  • AI大模型如何重塑软件开发流程?从自动化革命到人机共生范式
  • 小架构step系列01:小架构初衷
  • SQLMesh中的SQL模型:从基础定义到高级应用
  • 【网工|知识升华版|实验】1 登录华为设备并配置
  • 【Maven】Maven深度避坑指南:依赖冲突全维度解决方案与工业级实战(超万字解析)
  • 移动conda虚拟环境的安装目录
  • 超低功耗语音芯片有哪些?
  • 构建下一代云原生大模型多租户平台:架构设计与关键挑战
  • Django全栈开发:架构解析与性能优化实战
  • AWS CloudFormation部署双可用区VPC网络架构 - 完整指南
  • Chrome 下载文件时总是提示“已阻止不安全的下载”的解决方案
  • 力扣 hot100 Day32
  • 鸿蒙UI框架深度解析:对比Android/iOS的布局适配与组件设计
  • ElementUI 表格el-table自适应高度随浏览器窗口变化
  • 量子算法:微算法科技用于定位未知哈希图的量子算法,网络安全中的哈希映射突破
  • 在设计提示词(Prompt)时,关于信息位置的安排z怎么 结合模型特性和任务目标
  • 容器基础5-Helm 与 K8s 的关系
  • Lua 安装使用教程
  • 第二章AIGC入门:打开人工智能生成内容的新世界大门(3/36)