Mybatis-Plus“读-批量写-读”数据不一致的问题分享

news2025/1/2 4:07:37

在日常开发过程中,时常会遇到一个如下场景:

  1. 根据条件x,读取表A,得到多行数据;
  2. 遍历读取到的数据,对条件x以外的字段进行修改,并进行保存;(重点)
  3. 修改后,调用通用方法,传入条件x,重新读取表A的数据,并进行MQ广播;

流程如下图

业务代码MybatisMQ开启事务第一次查询S1条件x查询返回查询结果R1更新修改查询结果R1批量更新更新成功第二次查询S2条件x查询返回查询结果R2发送MQ结果R2发送MQ提交事务业务代码MybatisMQ

以上业务流程,是一个很普通的流程,一眼看去没什么问题,以下是我们期望的流程结果:

  • 第一次条件x查询,得到结果R1;
  • 修改R1后执行更新;
  • 第二次条件x查询,得到结果R2为更新后在当前事务中的最新数据;

但是在我们实际测试中,却发现,对于相同的条件x的前后两次查询S1和S2,得到的居然是一样的结果数据。若按照以上的业务流程,第二次查询后发送MQ,如果R2不是最新值,那么可能导致MQ消费者数据不一致的情况。

下面来看一下伪代码演示。

示例演示

以下伪代码中,为了方便演示,不进行MQ操作,直接使用打印日志代替

可以看到,在执行Mybatis-Plus提供的批量更新方法updateBatchById后,重新读取数据,居然还是更新前的数据,那这到底是怎么回事呢,我们往下分析与追踪。

分析与追踪

在这个案例中,只有查询-更新-查询的代码,数据库使用的是MySQL,ORM框架使用的是Mybatis-Plus,所以可以从以下两方面进行排查

  1. MySQL事务;
  2. MyBatis-Plus;

mysql事务

知识点A:在MySQL中,Innodb基于MVCC思想实现快照读。核心思想是利用事务的ReadView与undo log版本链进行匹配,从而判断当前事务能读取到哪个版本的数据。即,在Innodb的默认隔离级别为可重复读时,对于同一事务下,写操作的结果对下一个读操作是可见的。

回到上面的案例代码,可见,当前代码的“读-写-读”操作,是被包含在同一个事务中,事务管理完全交给Spring事务管理器,且并未对事务传播行为进行控制,所以按照MySQL的知识点,可以确定,第二次“读”操作,如果是正常从MySQL数据库进行读数据,那么得到的数据必然是“写”操作所产生的数据。

但是事实却恰恰相反,第二次“读”操作,无法得到前一次“写”操作产生的数据。由此,我们有理由怀疑,第二次”读“操作,可能存在没有执行MySQL读取操作的情况,为此,我们开启SQL执行日志打印功能,打印日志如下:

从打印的日志中,可以很明显看到,代码是执行”读-写-读“的操作,但是实际上,却只执行的”读-写“的语句,第二次的”读“操作,在代码中并没有报错,且能成功返回数据。

由于我们使用的ORM是Mybatis-Plus,那么我们可以做出以下判断

  1. 第二次”读“操作,Mybatis-Plus或Mybatis没有对MySQL执行select语句;
  2. 第二次”读“操作,Mybatis-Plus或Mybatis从”某个地方“,读取了缓存并返回数据。

接下来我们从Mybatis-Plus来分析。

MyBatis-Plus

知识点B:

  1. Mybatis无论是读操作,还是写操作的,底层都是通过SqlSession接口提供的方法来执行的,并且在默认情况下,同一事务中的多个读写操作,是共享同一个SqlSession实例对象。
  2. Mybatis默认是开启一级缓存的,一级缓存是SqlSession级别的,也就是说,在同一个SqlSession下,连续执行相同的读操作,Mybatis并不会每次都将查询语句发送到MySQL,而是将第一次查询的结果缓存下来,在下一次执行相同的读操作时,会直接查询当前SqlSession下的缓存,若存在,那么直接返回结果的,若不存在,则再与MySQL进行查询交互。
  3. 若两次相同的查询中间存在任何update、commit、rollback操作时,Mybatis会在操作之前,先清除当前SqlSession中的缓存数据。

”读“操作源码

”读“操作源码

”写“操作源码

基于以上Mybatis的”读“、”写“操作的源码,现在回过头看下我们的业务伪代码,我们可以对”读-写-读“操作,进行简单分析:

  1. ”读“操作,当前SqlSession缓存中,没有数据,需要与数据库进行”读“交互,读取完成后进行SqlSession级别缓存。
  2. ”写“操作,对当前SqlSession中的缓存进行清空操作,再与数据库进行”写“交互。
  3. ”读“操作,当前SqlSession中的缓存数据已经被前面”写“操作清空,此时进行”读“操作,需要需要与数据库进行”读“交互,得到”写“操作后的最新数据。

What??这样分析下来,还是与我们伪代码得到的结果不一致,敢情是分析了个寂寞??

别着急,我们重新看一下我们的伪代码。

在这段代码中,”读-写-读“操作,分别调用了demoUserRepository.listByIds()、demoUserRepository.updateBatchById()这两个方法,而这两个方法,并不是Mybatis框架提供的方法,而是MyBatis-Plus框架提供的,接下来分别看一下这两个方法。

listByIds()

可以看出listByIds()方法中,实际是直接调用了Mapper代理对象的方法,与常规的直接调用Mapper代理对象的方法并无区别。

updateBatchById()

但是在updateBatchById()方法中,却调用了sqlSessionBatch()的方法,该方法得到一个SqlSession实例对象,而后直接for循环调用该SqlSession对象的update()方法。

还记得前面知识点中提到过, ”Mybatis在默认情况下,同一事务中的多个读写操作,是共享同一个SqlSession实例对象“ 这句话吗??那么在此处出现的sqlSessionBatch()方法,并且得到一个SqlSession实例对象,并进行使用,又是为什么呢??我们继续跟进sqlSessionBatch()方法。

最终,我们跟进到了openSessionFromDataSource()方法,可以看到,updateBatchById()方法,为了执行批量更新的操作,重新构建了一个执行器类型为ExecutorType.BATCH的SqlSession实例对象。

也就是说,尽管前面的”读“操作使用的是SqlSession实例对象A,但是在updateBatchById()”写“操作时,却重新构建了一个SqlSession实例对象B,并使用该对象直接执行update语句,此时,”Mybatis在默认情况下,同一事务中的多个读写操作,是共享同一个SqlSession实例对象“ 这个默认情况,由于Mybatis-Plus的封装,直接被打破,导致将同时存在两个SqlSession实例对象A和B。

为了验证以上的结论,我们进行以下断点调试:

首先是第一次”读“操作,如下:

此时的SqlSession实例对象引用为DefaultSqlSession@10338。

接下来是”写“操作,如下:

此时的SqlSession实例对象引用为DefaultSqlSession@10344。

通过以上的断点调试,我们可以证明,在实例的伪代码中,”读-写“操作,确实产生了两个不同的SqlSession实例对象。

那么,以上的证明,又与我们讨论的第二次”读“操作得到数据不一致,有什么关系呢??

别急,我们接着看第二次”读“操作的断点调试结果:

从断点调试结果可以看到,第二次”读“操作,使用的SqlSession实例对象,与第一次”读“操作使用的SqlSession实例对象,是同一个对象,对象引用为DefaultSqlSession@10338。

到此,结合前面《知识点B》中第2点,我们即可瞬间推断出,为何第二次”读“操作,没有与MySQL进行读交互,却能获取到数据,并且得到的数据,并不是前面”写“操作所产生的数据,即

在”读-写-读“顺序中的,”写“操作使用Mybatis-Plus封装的updateBatchById()方法,使用了与前后”读“操作不同SqlSession对象实例,”写“操作无法清除第一次”读“操作所属的SqlSession中的缓存,而第二次”读“操作,却又使用了与第一次”读“操作相同的SqlSession,导致第二次”读“操作,直接从SqlSession缓存中直接获取第一次”读“操作得到的数据。

知识点:关于为何”Mybatis在默认情况下,同一事务中的多个读写操作,共享同一个SqlSession实例对象“的问题,是因为Mybatis在对Mapper接口方法生成动态代理对象的时候,将SqlSession作为构造参数传递进代理对象。详见org.apache.ibatis.binding.MapperProxy类。

结论与方案

SqlSession

从以上演示案例与源码简单分析可以得知,造成案例中第二次”读“操作数据有误问题的原因,主要在于”写“操作使用的是MyBatis-Plus框架封装的”批量写“操作,导致出现不同SqlSession的问题,那么在SqlSession层面,我们有以下的解决方案:

  1. 不使用Mybatis-Plus封装的updateBatchById()方法,使用循环调用Mybatis提供update方法,使”读-写“操作使用同一个SqlSession。
  2. 当使用Mybatis-Plus封装的updateBatchById()方法前,对之前”读“操作的SqlSession进行commit操作,使其清空缓存中的数据,这样在下次”读“操作时,将会直接从数据库读取数据。

事务

以上的演示案例,除了从SqlSession层面解决,也可以从事务层面来解决这个问题。针对以上案例中的第二次”读“操作,我们可以将该操作,与当前事务分离,在当前事务提交后,再进行”读“操作,此时的读操作,将单独生成一个新的SqlSession,并直接从数据库读取数据。代码如下:

该方案做法是在当前事务中注入一个同步事务回调事件,在当前事务执行完commit后,再重新从数据库读取数据。

*注意,该方案没有返回值,若对第二次“读”操作的结果需要进一步处理,需要将处理过程包含进afterCommit()方法内

跟进

按理来说,Mybatis-Plus这样成熟与活跃的框架,本不应该出现本次案例updateBatchById()方法的“Bug”。由于我们当前案例使用的Mybatis-Plus版本为3.1.0,最新的Mybatis-Plus版本已更新到3.5.1,为此,我们直接翻一下Mybatis-Plus 3.5.1版本的源码看一下是否解决了该问题。

由于Mybatis-Plus 3.5.1使用了大量Java的新语法,源码存在部分差异,我们直接看核心源码:

可以看到,Mybatis-Plus 3.5.1版本已经修复了该“Bug”,且带上了注释,改修复方案,与上面我们提出的SqlSession方案也是一致的。具体实现,请自行翻阅源码,此处不再赘述。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/146677.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基础算法(七)——离散化

离散化 介绍 这里的离散化,特指整数的、保序的离散化 有些题目可能需要以数据作为下标来操作,但题目给出的数据的值比较大,但是数据个数比较小。此时就需要将数据映射到和数据个数数量级相同的区间,这就是离散化,即…

基于imx6ull第一个Linux驱动

在编译第一个驱动之前,需要把基本的环境准备好,可以参照这两篇文章:https://wlink.blog.csdn.net/article/details/128590747https://wlink.blog.csdn.net/article/details/128591216我们之前写过一个基于ubuntu最基本的字符设备驱动&#xf…

关于固态硬盘冷数据掉速问题解决方案

20230107 By wdhuag 前言: 我有一个西数蓝盘500G固态,系统盘,一年没开机,这个月开机后发现系统很卡,持续读取假死严重。测试没有坏块,网上说的是冷数据掉速问题。 参考: 如何看待西数/闪迪多…

排序算法:插入、希尔、选择、冒泡

目录 一.插入排序 1.算法描述: 2.实现思路: 3.时间复杂度: 代码如下: 二.希尔排序 (插入排序的优化升级) 1.算法描述: 2.实现思路: 3.时间复杂度: 代码如下&a…

【算法笔记】最近公共祖先(LCA)问题求解——倍增算法

0. 前言 最近公共祖先简称 LCA(Lowest Common Ancestor)。两个节点的最近公共祖先,就是这两个点的公共祖先里面,离根最远的那个。 这种算法应用很广泛,可以很容易解决树上最短路等问题。 为了方便,我们记…

星光不负赶路人|2022年终总结

时间真快,转眼又是年末。整理一篇文章来给自己好好做一次年终盘点,本着陈述事实,提炼精华,总结不足的思路,给自己这一年的工作、生活、成长画个句号。 工作 🏢 从经海路到中关村 去年换了工作&#xff0c…

Java设计模式中的创建者模式/单例模式是啥?单例模式其中的饿汉式与懒汉式又是啥?又可以用在哪些地方

继续整理记录这段时间来的收获,详细代码可在我的Gitee仓库SpringBoot克隆下载学习使用! 4. 创建者模式 4.1 特点 使用者不需要知道对象的创建细节 4.2 单例模式 4.2.1使用场景 单例类:且仅能创建一个实例类访问类:使用单例类…

七、Gtk4-Defining a final class

1 定义一个最终类 1.1 一个非常简单的编辑器 在上一节中,我们创建了一个非常简单的文件查看器。现在我们继续重写它,并将其转换为非常简单的编辑器。它的源文件是tfe目录下的tfe1.c(文本文件编辑器1)。 GtkTextView是一个多行编辑器。因此&#xff0c…

java学习day71(乐友商城)购物车实现

今日目标&#xff1a; 1.实现未登录状态的购物车 2.实现登陆状态下的购物车 1.搭建购物车服务 1.1.创建module 1.2.pom依赖 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi&…

软件测试~测试分类

目录 1.按照是否查看代码划分 ① 黑盒测试(Black-box Testing) ② 白盒测试(White-box Testing) ③ 灰盒测试(Gray-Box Testing) 2.按照开发阶段划分 ① 单元测试(Unit Testing) ② 集成测试(Integration Testing) ③ 系统测试(System Testing) ④ 验收测试(Acceptance…

kNN分类

一、 概述 kNN(k nearest neighbor,k近邻)是一种基础分类算法&#xff0c;基于“物以类聚”的思想&#xff0c;将一个样本的类别归于它的邻近样本。 ![在这里插入图片描述] 二、算法描述 1.基本原理 给定训练数据集 T{(x1,y1),(x2,y2),...,(xN,yN)}T\left\{ \left( x_1,y_1 …

17. XML

文章目录一、XML概念二、XML语法1、基础语法2、快速入门3、组成部分4、约束1. 约束概述2. 分类3. DTD4. Schema三、XML解析1、操作xml文档2、 解析xml的方式1. DOM2. SAX3. xml常见的解析器&#xff08;工具包&#xff09;4. Jsoup&#xff08;1&#xff09;快速入门&#xff0…

VUE3 学习笔记(一):环境配置、项目创建

一、首先需要安装node.jsnodejs官网&#xff1a;Node.js (nodejs.org)下载安装包&#xff1a;下载稳定版本即可&#xff0c;目前&#xff08;2023-01-07&#xff09;是18.13.0版本c. 检查当前版本&#xff08;CMD&#xff09;&#xff1a;至此&#xff0c;nodejs已经安装成功&a…

电力系统机组组合(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️❤️&#x1f4a5;&#x1f4a5;&#x1f4a5;&#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清…

EM算法学习总结

序 这个和我的硕士毕业论文的题目就有一定关系&#xff0c;我的导师让我按时向她汇报学习进度。然而我还在进行实习&#xff0c;还要准备自己明年的秋招&#xff0c;只能想办法游走于三者之间。 EM算法是一个常用的数据挖掘算法&#xff0c;想必从事数据挖掘的相关工作的同学…

机器人操作系统ROS/ROS2(1)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录前言一、ROS和ROS2是什么&#xff1f;二、ROS2安装步骤1.Ubuntu虚拟机安装2.ROS2系统安装3.验证ROS2安装成功4.小海龟仿真示例总结前言 提示&#xff1a;这里可以添…

LeetCode刷题模版:21 - 30

目录 简介21. 合并两个有序链表22. 括号生成23. 合并K个升序链表24. 两两交换链表中的节点25. K 个一组翻转链表26. 删除有序数组中的重复项27. 移除元素28. 找出字符串中第一个匹配项的下标29. 两数相除【未理解】30. 串联所有单词的子串【未理解】结语简介 Hello! 非常感谢您…

devops 是什么东东了

DevOps&#xff0c;字面意思是Development &Operations的缩写。 DevOps是从实践中逐步总结提炼出的方法论理念。近而创造了DevOps这个词。 DevOps概念的萌芽阶段&#xff1a; 2008年敏捷大会上&#xff0c;来自Patrick Debois发表了题为 《Agile Infrastructure & Op…

基于.NET技术的动漫管理系统,给各位二次元老司机提供的动漫管理平台,基于.NET的前后端框架Blazor,含安装教程及使用说明

介绍 老司机驾驶舱——给各位二次元老司机提供的动漫管理平台。 下载地址&#xff1a;基于.NET技术的动漫管理系统 编写本应用的目的主要是实践巨硬&#xff08;Microsoft&#xff09;的新一代前端框架Blazor与EntityFramework&#xff0c;本应用也给想要学习这套框架的童鞋提…

Ubuntu20.04 hyperledger fabric2.4基于Docker搭建blockchain-explorer

准备 启动fabric测试网络。   这里默认已经完成了Fabric测试网络搭建以及运行。   后续会出Fabric安装&#xff0c;现在不会的就先去看别的博客 配置 1.在test-network 文件夹下面建立explorer文件夹&#xff1a; mkdir explorer2. 配置文件 2.1下载配置文件 先进文件…