百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

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

bigegpt 2025-02-24 15:09 8 浏览

背景

技术选型与版本: SpringBoot 2.7.6、Mybatis-Plus 3.1.0

技术名词: 快照读、ReadView、Undo log、SqlSession、一级缓存

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

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

流程如下图



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

  • 第一次条件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:

Mybatis无论是读操作,还是写操作的,底层都是通过SqlSession接口提供的方法来执行的,并且在默认情况下,同一事务中的多个读写操作,是共享同一个SqlSession实例对象。Mybatis默认是开启一级缓存的,一级缓存是SqlSession级别的,也就是说,在同一个SqlSession下,连续执行相同的读操作,Mybatis并不会每次都将查询语句发送到MySQL,而是将第一次查询的结果缓存下来,在下一次执行相同的读操作时,会直接查询当前SqlSession下的缓存,若存在,那么直接返回结果的,若不存在,则再与MySQL进行查询交互。若两次相同的查询中间存在任何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方案也是一致的。具体实现,请自行翻阅源码,此处不再赘述。

相关推荐

Linux 系统启动完整流程

一、启动系统流程简介如上图,简述系统启动的大概流程:1:硬件引导UEFi或BIOS初始化,运行POST开机自检2:grub2引导阶段系统固件会从MBR中读取启动加载器,然后将控制权交给启动加载器GRU...

超专业解析!10分钟带你搞懂Linux中直接I/O原理

我们先看一张图:这张图大体上描述了Linux系统上,应用程序对磁盘上的文件进行读写时,从上到下经历了哪些事情。这篇文章就以这张图为基础,介绍Linux在I/O上做了哪些事情。文件系统什么是...

linux入门系列12--磁盘管理之分区、格式化与挂载

前面系列文章讲解了VI编辑器、常用命令、防火墙及网络服务管理,本篇将讲解磁盘管理相关知识。本文将会介绍大量的Linux命令,其中有一部分在“linux入门系列5--新手必会的linux命令”一文中已经...

Linux环境下如何设置多个交叉编译工具链?

常见的Linux操作系统都可以通过包管理器安装交叉编译工具链,比如Ubuntu环境下使用如下命令安装gcc交叉编译器:sudoapt-getinstallgcc-arm-linux-gnueab...

可算是有文章,把Linux零拷贝技术讲透彻了

阅读本文大概需要6.0分钟。作者:卡巴拉的树链接:https://dwz.cn/BaQWWtmh本文探讨Linux中主要的几种零拷贝技术以及零拷贝技术适用的场景。为了迅速建立起零拷贝的概念...

linux软链接的创建、删除和更新

大家都知道,有的时候,我们为了省下空间,都会使用链接的方式来进行引用操作。同样的,在系统级别也有。在Windows系列中,我们称其为快捷方式,在Linux中我们称其为链接(基本上都差不多了,其中可能...

Linux 中最容易被黑客动手脚的关键目录

在Linux系统中,黑客攻击后常会针对关键目录和文件进行修改以实现持久化、提权或隐藏恶意活动。本文介绍下黑客最常修改的目录及其手法。一、/etc目录关键文件有:/etc/passwd和/et...

linux之间传文件命令之Rsync傻瓜式教程

1.前言linux之间传文件命令用什么命令?本文介绍一种最常用,也是功能强大的文件同步和传输工具Rsync,本文提供详细傻瓜式教程。在本教程中,我们将通过实际使用案例和最常见的rsync选项的详细说...

Linux下删除目录符号链接的方法

技术背景在Linux系统中,符号链接(symlink)是一种特殊的文件,它指向另一个文件或目录。有时候,我们可能需要删除符号链接,但保留其指向的目标目录。然而,在删除符号链接时可能会遇到一些问题,例如...

阿里云国际站注册教程:aa云服务器怎么远程链接?

在全球化的今天,互联网带给我们无以计数的便利,而云服务器则是其中的重要基础设施之一。这篇文章将围绕阿里云国际站注册、aa云服务器如何远程链接,以及服务器安全防护如Ddos防火墙、网站应用防护waf防火...

Linux 5.16 网络子系统大范围升级 多个新适配器驱动加入

Linux在数据中心中占主导地位,因此每个内核升级周期的网络子系统变化仍然相当活跃。Linux5.16也不例外,周一最新与网络相关的更新加入了大量的驱动和新规范的支持。一个较新硬件的驱动是Realt...

搭建局域网文件共享服务(Samba),手机电脑都能看喜欢的影视剧

作为一名影视爱好者,为了方便地观看自己喜欢的影视作品,在家里搞一个专门用来存放电影的服务器是有必要的。蚁哥选则用一台Ubuntu系统的电脑做为服务器,共享影音文件,其他同一个局域网内的电脑或手机可以...

分享一个实用脚本—centos7系统巡检

概述这周闲得慌,就根据需求写了差不多20个脚本(部分是之前分享过的做了一些改进),今天主要分享一个给平时运维人员用的centos7系统巡检的脚本,或者排查问题检查系统情况也可以用..实用脚本#!/bi...

Linux 中创建符号链接的方法

技术背景在Linux系统里,符号链接(SymbolicLink),也被叫做软链接(SoftLink),是一种特殊的文件,它指向另一个文件或者目录。符号链接为文件和目录的管理带来了极大的便利,比...

一文掌握 Linux 符号链接

符号链接(SymbolicLink),通常被称为“软链接”,是Linux文件系统中一种强大而灵活的工具。它允许用户创建指向文件或目录的“快捷方式”,不仅简化了文件管理,还在系统配置、软件开发和日...