在日常开发过程中,时常会遇到一个如下场景:
-
根据条件x,读取表A,得到多行数据;
-
遍历读取到的数据,对条件x以外的字段进行修改,并进行保存;(重点)
-
修改后,调用通用方法,传入条件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,所以可以从以下两方面进行排查
-
MySQL事务;
-
MyBatis-Plus;
mysql事务
知识点A:在MySQL中,Innodb基于MVCC思想实现快照读。核心思想是利用事务的ReadView与undo log版本链进行匹配,从而判断当前事务能读取到哪个版本的数据。即,在Innodb的默认隔离级别为可重复读时,对于同一事务下,写操作的结果对下一个读操作是可见的。
详见大佬博客:
blog.csdn.net/SIESTA030/a…
回到上面的案例代码,可见,当前代码的“读-写-读”操作,是被包含在同一个事务中,事务管理完全交给Spring事务管理器,且并未对事务传播行为进行控制,所以按照MySQL的知识点,可以确定,第二次“读”操作,如果是正常从MySQL数据库进行读数据,那么得到的数据必然是“写”操作所产生的数据。
但是事实却恰恰相反,第二次“读”操作,无法得到前一次“写”操作产生的数据。由此,我们有理由怀疑,第二次”读“操作,可能存在没有执行MySQL读取操作的情况,为此,我们开启SQL执行日志打印功能,打印日志如下:
从打印的日志中,可以很明显看到,代码是执行”读-写-读“的操作,但是实际上,却只执行的”读-写“的语句,第二次的”读“操作,在代码中并没有报错,且能成功返回数据。
由于我们使用的ORM是Mybatis-Plus,那么我们可以做出以下判断
-
第二次”读“操作,Mybatis-Plus或Mybatis没有对MySQL执行select语句;
-
第二次”读“操作,Mybatis-Plus或Mybatis从”某个地方“,读取了缓存并返回数据。
接下来我们从Mybatis-Plus来分析。
MyBatis-Plus
知识点B:
-
Mybatis无论是读操作,还是写操作的,底层都是通过SqlSession接口提供的方法来执行的,并且在默认情况下,同一事务中的多个读写操作,是共享同一个SqlSession实例对象。
-
Mybatis默认是开启一级缓存的,一级缓存是SqlSession级别的,也就是说,在同一个SqlSession下,连续执行相同的读操作,Mybatis并不会每次都将查询语句发送到MySQL,而是将第一次查询的结果缓存下来,在下一次执行相同的读操作时,会直接查询当前SqlSession下的缓存,若存在,那么直接返回结果的,若不存在,则再与MySQL进行查询交互。
-
若两次相同的查询中间存在任何update、commit、rollback操作时,Mybatis会在操作之前,先清除当前SqlSession中的缓存数据。
”读“操作源码
”读“操作源码
”写“操作源码
基于以上Mybatis的”读“、”写“操作的源码,现在回过头看下我们的业务伪代码,我们可以对”读-写-读“操作,进行简单分析:
-
”读“操作,当前SqlSession缓存中,没有数据,需要与数据库进行”读“交互,读取完成后进行SqlSession级别缓存。
-
”写“操作,对当前SqlSession中的缓存进行清空操作,再与数据库进行”写“交互。
-
”读“操作,当前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层面,我们有以下的解决方案:
-
不使用Mybatis-Plus封装的updateBatchById()方法,使用循环调用Mybatis提供update方法,使”读-写“操作使用同一个SqlSession。
-
当使用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方案也是一致的。具体实现,请自行翻阅源码,此处不再赘述。
知其然,必须知其所以然。
文章目录
问题
描述
问题
原因解决办法方案一方案二(推荐)
问题
描述
今天在公司项目中修改id的生成策略为
mybatis-plus
自带的IdWorker策略时,发现返回给前台的id竟然和
数据
库不
一致
。费解得很呐。
package net.mshome.twisted.tmall.entity;
import com....
Redundant boxing inside ‘Integer.valueOf(xx)’
解决办法:
改为Integer.parseInt(xx)即可。因为Integer.valueOf内部调用了parseInt,会提示多余的拆箱操作
‘xxx == null ? false : yyy’ can be simplified to 'xxx!=null && yyy
解决办法:
按照提示修改即可。简化
写
法,原来
写
法比较啰嗦
‘OptionalInt.getAsInt()’ witho.
关于集合属性,是前端界面一起打包过来的,一般是通过js追加的,加载到后台怎麽处理,要遍历,没有相关实例,博主随便
写
一点(要遍历一般是在Service层当中,不要到controller层当中):
@requestMappering(value="")
public String example(HttpRequestServlet request,Map<String,Object>,C...
mapper 文件@Repository
public interface UserMapper {
List<String> queryNamesByIds(Map<String, Object> userIdsParam);
}@Named
public class UserServiceImpl implements UserService { @Override
在
MyBatis-Plus
中,可以使用 `saveBatch` 方法来实现
批量
插入
数据
。
首先,确保你已经正确引入了
MyBatis-Plus
的依赖。接下来,在你的
数据
访问层(如 Mapper 或 Service 类)中,使用 `saveBatch` 方法进行
批量
插入操作。
示例代码如下:
```java
List<Entity> entityList = new ArrayList<>(); // 要插入的实体列表
// 添加要插入的实体
数据
到列表中
entityList.add(new Entity("data1"));
entityList.add(new Entity("data2"));
entityList.add(new Entity("data3"));
// 调用 saveBatch 方法执行
批量
插入操作
boolean success = entityMapper.saveBatch(entityList);
if (success) {
System.out.println("
批量
插入成功!");
} else {
System.out.println("
批量
插入失败!");
在上述示例中,我们创建了一个实体列表 `entityList`,然后将要插入的实体
数据
添加到列表中。最后,通过调用 `saveBatch` 方法执行
批量
插入操作。如果插入成功,返回值为 true,否则为 false。
需要注意的是,`saveBatch` 方法会自动使用
MyBatis-Plus
的
批量
插入功能,提高插入效率。同时,也可以在调用 `saveBatch` 方法时传入一个
批量
插入的大小参数,以控制每次
批量
插入的数量。
希望能帮到你!如有更多
问题
,请继续提问。