并发修改导致MVCC脏写问题

并发修改导致MVCC脏写问题

一、概要

1.1 业务场景

数据库表结构设计: 一个主档数据,通过一个字段,逗号分隔的方式去关联其他明细信息的id。

如主档数据A,有3条明细数据与A关联,其id分别是1,2,3,那么其存储在关联字段的值为1,2,3。

操作场景:

接口设计操作:①根据id查询主档数据 ②获取主档数据关联的明细id ③更新主档明细id

入参:主档id、明细id(这里不传列表,是因为相应参数需要返回相应的信息,因此就涉及成这样了)

在实际应用中,可能需要涉及到批量新增的操作,因此前端会出现某一时间点,同时调用多次该接口,导致MVCC脏写问题。

1.2 DEMO 初始化

MySQL中创建demo表

# 建表
CREATE TABLE `test` (
`id` INT(11) NOT NULL COMMENT '主档id',
`ids` VARCHAR(255) DEFAULT NULL COMMENT '关联ids',
PRIMARY KEY (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;

建立相应的web三层架构:controller、service、mapper

controller

@RestController
@RequestMapping("/demo")
public class democontroller {@Resourceprivate demoService demoservice;@GetMapping("/test")public void test(Long id, String relId) {demoservice.test(id, relId);}}

service

public interface demoService {void test(Long id, String relId);
}

service-实现类

@Slf4j
@Service
public class demoServiceImpl extends ServiceImpl<DemoMapper, Demo> implements demoService {@Override@Transactional(rollbackFor = Exception.class)public void test(Long id, String relId) {// 获取主档对象Demo demo = getById(id);// 打印当前线程获取到的ids列表log.info("Thread Name:{}, before demo:{}",Thread.currentThread().getName(), demo);// 获取当前关联明细idList<String> idsList =  Arrays.stream(demo.getIds().split(",")).collect(Collectors.toList());// 添加新的元素(去重操作省略)idsList.add(relId);// 更新关联字段demo.setIds(idsList.stream().collect(Collectors.joining(",")));// 打印当前线程获取到的ids列表log.info("Thread Name:{}, after demo:{}",Thread.currentThread().getName(), demo);updateById(demo);}
}

mapper

@Mapper
public interface DemoMapper extends BaseMapper<Demo> {
}

实体类

@Data
@EqualsAndHashCode(callSuper = true)
@TableName("test")
public class Demo extends Model<Demo> {@TableId(value = "id", type= IdType.ASSIGN_ID)private Integer id;private String ids;}

1.3 问题复现

数据初始化

INSERT INTO `study_test`.`test`(`id`, `ids`) VALUES (1, '1');

在这里插入图片描述

通过apifox开三个线程并发调用“/demo”接口,入参如下所示:id固定为1,relId为一个整数值(1-999随机数)

在这里插入图片描述

调用结果如下所示
在这里插入图片描述

在这里插入图片描述

问题到此复现。

二、 原因分析

原因已经在标题中打出来了,即MVCC的脏写问题,当我们使用@Transactional(rollbackFor = Exception.class)注解后,Spring 会为当前事务设置 MySQL 会话的事务隔离级别为MySQL默认隔离级别即RR(可重复读)。

我们在打个断点后,通过apifox发起并发访问

在这里插入图片描述

在MySQL中通过select trx_state, trx_started, trx_mysql_thread_id, trx_query, trx_isolation_level from information_schema.innodb_trx;命令查看当前执行中且未提交的事务,可以看到此时有三条事务正在运行中,并且他们的隔离级别是可重复读(RR)

在这里插入图片描述

由于在RR隔离级别下,访问数据(即getById操作)访问的数据是通过MVCC实现的快照读,此时在并发访问的情况下,可能三个事务查询到的结果都是一样的,所以导致最后在更新的时候只出现了新增成功一个值。

三、解决

3.1 将隔离级别改成串行(无效)

一开始是这个想法,因为串行级别下都是会加锁的,因此通过改变@Transactional参数:@Transactional(rollbackFor = Exception.class, isolation = Isolation.SERIALIZABLE)将事物隔离级别改成串行也许可以解决问题。

当我们并发调用该接口后,发生报错,即在尝试加锁的时候,检测到了死锁

2024-12-16 23:59:48.615 ERROR 45800 --- [nio-6666-exec-3] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.dao.DeadlockLoserDataAccessException: 
### Error updating database.  Cause: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
### The error may exist in com/example/mapper/DemoMapper.java (best guess)
### The error may involve com.example.mapper.DemoMapper.updateById-Inline
### The error occurred while setting parameters
### SQL: UPDATE test  SET ids=?  WHERE id=?
### Cause: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
; Deadlock found when trying to get lock; try restarting transaction; nested exception is com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction] with root cause

打个断点,将其中两个事物停在查询查找后更新操作前,将其中一个操作停在更新操作后

在这里插入图片描述

通过命令select trx_state, trx_started, trx_mysql_thread_id, trx_query, trx_isolation_level from information_schema.innodb_trx;查看当前未提交的任务

可以看到有三个未提交事务其中一个在进行更新操作的时候发生了锁等待

在这里插入图片描述

通过命令 SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;查看一下当前加锁情况

结果就是两个事务给数据加了S锁,一个事务需要给更改记录加X锁,同时前两个事物在后面也需要给数据加X锁,造成了死锁。

在这里插入图片描述

注:关于多线程并发访问打断点需要进行如下设置

在这里插入图片描述

3.2 开批量新增接口(无效)

开批量新增接口,将需要关联的id通过一个接口传入,而不是同时调用三次接口分别传入来解决该问题,但是如果在并发场景下还是会出关联id丢失的问题。

3.3 乐观锁 (无效)

相关乐观锁实现可以参考mybatis-plus官网的文档:https://baomidou.com/plugins/optimistic-locker/。(乐观锁在我理解上就是做版本控制,每次更新操作都要对版本进行验证)

这里直接采用mybatis-plus提供的配置方式进行测试

配置文件

@Configuration
public class MybatisPlusConfig {@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());return interceptor;}
}

实体类:

@Data
@EqualsAndHashCode(callSuper = true)
@TableName("test")
public class Demo extends Model<Demo> {@TableId(value = "id", type= IdType.ASSIGN_ID)private Integer id;private String ids;@Versionprivate Integer version;}

在数据表test中新增一个字段version,并设置其值为版本为2,再次并发调用接口,查看当前版本都为2

在这里插入图片描述

3.4 悲观锁(有效)

采用JUC、分布式锁等进行加锁,此时只有获取到锁的线程才可以对这一条数据进行操作,就不会出现并发问题。

修改代码,这里使用JUC下的ReentrantLock

@Slf4j
@Service
public class demoServiceImpl extends ServiceImpl<DemoMapper, Demo> implements demoService {private Lock lock = new ReentrantLock();@Override@Transactional(rollbackFor = Exception.class)public void test(Long id, String relId) {lock.lock();// 获取主档对象Demo demo = getById(id);// 打印当前线程获取到的ids列表log.info("Thread Name:{}, before demo:{}",Thread.currentThread().getName(), demo);// 获取当前关联明细idList<String> idsList =  Arrays.stream(demo.getIds().split(",")).collect(Collectors.toList());// 添加新的元素(去重操作省略)idsList.add(relId);// 更新关联字段demo.setIds(idsList.stream().collect(Collectors.joining(",")));// 打印当前线程获取到的ids列表log.info("Thread Name:{}, after demo:{}",Thread.currentThread().getName(), demo);updateById(demo);lock.unlock();}
}

结果成功

在这里插入图片描述

3.5 其他方法

如果有其他更好的方法,欢迎讨论!

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

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

相关文章

[创业之路-198]:华为的成立发展与新中国的建立与发展路径的相似性比较

目录 一、公司比较 1、创业初期的艰难与挑战 2、坚持自主创新与研发 3、市场拓展与国际化战略 4、企业文化与社会责任 5、面临的挑战与应对策略 二、任正非管理企业的思想大量借鉴了毛泽东建国的思想 1、矛盾论与企业管理 2、群众路线与企业文化 3、战略思维与长远发…

深入解析与示例:ROS中的catkin_make构建过程

深入解析与示例&#xff1a;ROS中的catkin_make构建过程 catkin_make 是用于构建ROS&#xff08;Robot Operating System&#xff09;中的catkin软件包的命令行工具。它的主要功能是编译工作空间中所有catkin软件包&#xff0c;并确保按照依赖关系正确构建每个软件包。下面详细…

PugiXML,一个高效且简单的 C++ XML 解析库!

嗨&#xff0c;大家好&#xff01;我是一行。今天要给大家介绍 PugiXML&#xff0c;这可是 C 里处理 XML 数据的得力助手。它能轻松地读取、修改和写入 XML 文件&#xff0c;就像一个专业的 XML 小管家&#xff0c;不管是解析配置文件&#xff0c;还是处理网页数据&#xff0c;…

SSE(Server-Sent Events)主动推送消息

说明 使用Java开发web应用&#xff0c;大多数时候我们提供的接口返回数据都是一次性完整返回。有些时候&#xff0c;我们也需要提供流式接口持续写出数据&#xff0c;以下提供一种简单的方式。 SSE&#xff08;Server-Sent Events&#xff09; SSE 是一种允许服务器单向发送事…

Java四大内部类之局部内部类、匿名内部类

目录 一、局部内部类 二、匿名内部类 基于接口的匿名内部类 基于类的匿名内部类 三、匿名内部类的实践 类的五大成员&#xff1a;属性、方法、构造器、代码块、内部类 内部类的分类 定义在外部类局部位置上&#xff08;比如说方法内&#xff09;&#xff1a; 局部内部类&…

请求go web后端接口 java安卓端播放视频

前端代码 添加gradle依赖 implementation com.squareup.retrofit2:retrofit:2.9.0 implementation com.squareup.retrofit2:converter-gson:2.9.0 添加访问网络权限 <uses-permission android:name"android.permission.INTERNET" />允许http 请求请求 andro…

ARMS 用户体验监控正式发布原生鸿蒙应用 SDK

作者&#xff1a;羿莉 背景 对企业数据进行敏感数据扫描和保护可以提升企业或组织的数据安全。一方面敏感数据可能包括个人身份信息、财务记录、医疗记录等&#xff0c;定期扫描这些数据可以防止未经授权的访问和泄露。 另一方面&#xff0c;许多国家和地区都有关于数据保护的…

CSS学习记录14

CSS不透明度 opacity属性指定元素的不透明度/透明度。opacity属性的取值范围为0.0 ~ 1.0。 值越低&#xff0c;越透明&#xff1a; img {opacity: 0.5; } opacity属性通常与:hover选择器一同使用&#xff0c;这样就可以在鼠标悬停时更改不透明度&#xff1a; img {opacity: 0…

【YashanDB知识库】如何处理yasql输入交互模式下单行字符总量超过限制4000字节

现象 在yasql执行sql语句后报错&#xff1a;YASQL-00021 input line overflow (>4000 byte at line 4) 原因 yasql在交互模式模式下单行字符总量限制4000字节&#xff0c;超出该限制即报错。 交互式模式下&#xff0c;yasql会显示一个提示符&#xff0c;通常是 SQL>…

为何ZLG致远电子要推出LGA嵌入式核心板?

LGA嵌入式核心板究竟有什么好&#xff1f;能让ZLG致远电子陆续推出了5个系列12个型号的LGA嵌入式核心板。 到目前为止&#xff0c;ZLG致远电子一共推出了M1106/M1107、M1126、M6Y2C、 A6Y2C、MR6450等5个系列&#xff0c;共12个型号的LGA形态的嵌入式核心板&#xff0c;涵盖了A…

开源实时多模态AI Agent,搭载Gemini多模态API(在线体验)

今天发现一个惊艳的开源项目&#xff0c;利用多模态大模型API进行多智能体交互。支持RAG、搜索等。 TEN Agent 是一款由 TEN 提供支持的对​​话式 AI&#xff0c;集成了 Gemini 2.0 Multimodal Live API、OpenAI Realtime API、RTC 等。它提供实时的看、听和说功能&#xff0…

C++小白实习日记——Pollnet,Efvi,UDP,数据类型转换(下)

内容太多了&#xff0c;这篇记录UDP接收端 一&#xff0c;UDP接收端接收数据 有了pollnet这个开源项目的支持&#xff0c;接收端的步骤为&#xff1a;1&#xff09;初始化硬编码的参数&#xff1a;接口&#xff0c;IP和端口 2&#xff09;创建接收文件.csv 3&#xff09;读…

【ROS2】坐标TF入门

1、简介 1)定义: TF(TransForm)是用于坐标系管理的工具,它提供了快速、高效的坐标变换和时间同步功能。 2)坐标系 坐标系:在机器人系统中,存在多个坐标系,如基坐标系(Base Frame)、世界坐标系(World Frame)、工具坐标系、工件坐标系等。这些坐标系之间的关系在机…

[创业之路-197]:华为的成立发展与新中国的建立与发展路径的相似性比较

目录 一、公司比较 1、创业初期的艰难与挑战 2、坚持自主创新与研发 3、市场拓展与国际化战略 4、企业文化与社会责任 5、面临的挑战与应对策略 二、任正非管理企业的思想大量借鉴了毛泽东建国的思想 1、矛盾论与企业管理 2、群众路线与企业文化 3、战略思维与长远发…

Loadsh源码分析-filter,find,findLast,reject,partition

lodash源码研读之filter,find,findLast,reject,partition 一、源码地址 GitHub 地址: GitHub - lodash/lodash: A modern JavaScript utility library delivering modularity, performance, & extras.官方文档地址: Lodash 官方文档 二、结构分析 结构框图省略。 三、函…

全国青少年信息学奥林匹克竞赛(信奥赛)备考实战之字符类型

要进行计算机程序设计&#xff0c;不仅仅进行科学数值类型计算&#xff0c;还需要处理一些文本信息&#xff0c;这就需要字符类型。在C中字符可以表示字母、符号等&#xff0c;在编程的时候&#xff0c;通常使用一对单引号括起来&#xff0c;例如&#xff1a;A,a,B,b,0等。字符…

Spring Boot 2.1.7 数据源自动加载过程详解

在 Spring Boot 中&#xff0c;数据源的自动配置是框架中一个关键功能&#xff0c;本文将以 Spring Boot 2.1.7 版本为例&#xff0c;详细讲解在单数据源情况下数据源是如何自动加载的。我们通过源码分析&#xff0c;追踪整个加载流程。 1. 自动配置类的发现 Spring Boot 使用…

Node.js第三方模块

1.npm 简介 npm:node package manager 是一个nodejs的包管理和开发工具。 2.npm作用 (提供第三方的安装模块) 1. 从npm服务器下载别人写的第三方js库包到本地使用。 2. 从npm服务器下载并安装第三方命令行程序到本地使用。 3. 将自己写的包或命令行程序上传到n…

剑指Offer 03比特位计数

只是记录 题目链接 题目链接 自己想出来的 第一种解法 思路简述 遍历[0,n]之间的数字&#xff0c;对于每一个数字按照二进制的方式展开&#xff0c;判断最低位置是否为1&#xff0c;若为1则1&#xff0c;反之不加&#xff0c;直到该数字等于0就停止。 public static int[] …

C# 23种设计模式(4)访问者模式(Visitor Pattern)

一、访问者模式介绍 访问者模式&#xff08;Visitor Pattern&#xff09;是一种行为设计模式&#xff0c;它允许你以一种新的方式来增加作用于一组对象的操作&#xff0c;而无需修改这些对象类的代码。访问者模式将数据操作与数据结构分离&#xff0c;适用于数据结构相对稳定但…