并发修改导致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 其他方法
如果有其他更好的方法,欢迎讨论!
相关文章:

并发修改导致MVCC脏写问题
并发修改导致MVCC脏写问题 一、概要 1.1 业务场景 数据库表结构设计: 一个主档数据,通过一个字段,逗号分隔的方式去关联其他明细信息的id。 如主档数据A,有3条明细数据与A关联,其id分别是1,2,3,那么其存…...

跌倒数据集,5345张图片, 使用yolo,coco json,voc xml格式进行标注,平均识别率99.5%以上
跌倒数据集,5345张图片, 使用yolo,coco json,voc xml格式进行标注,平均识别率99.5%以上 ,可用于某些场景下识别人是否跌倒或摔倒并进行告警。 数据集分割 训练组99% 5313图片 有效集0&am…...
Java转C之CMake
对于一位从 Java 转到 C 或 C 的工程师,理解 CMake 和其指令非常重要,因为 CMake 是目前 C/C 项目中最常用的构建工具。CMake 本质上是一个跨平台的自动化构建系统,它通过 CMakeLists.txt 文件来管理和配置项目的构建过程。在学习 CMake 的过…...
如何自己创建database.js文件来初始化本地sqlite数据库
如何自己创建database.js文件来初始化本地sqlite数据库!下面是一个案例展示,帮助大家,快速的视线,本地sqlite数据库信息初始化。 为了使用 database.js 文件初始化 SQLite 数据库并存储解签内容,你需要按以下步骤操作。…...

【汇编语言】内中断(三) —— 中断探险:从do0到特殊响应的奇妙旅程
文章目录 前言1. do01.1 do0程序1.2 存放字符串,得到完整的程序1.3 分析初步完成的程序1.4 正确的完整程序1.5 分析正确的完整程序 2. 设置中断向量3. 单步中断3.1 什么是单步中断?3.2 CPU为什么要提供单步中断3.2.1 思考一下Debug功能3.2.2 Debug是如何…...

0006.基于SpringBoot+element付费问答系统
适合初学同学练手项目,部署简单,代码简洁清晰; 愿世界和平再无bug 一、系统架构 前端:vue| elementui 后端:springboot | mybatis-plus 环境:jdk1.8 | mysql | maven 二、登录角色 1.管理员 2.用户 …...
SpringBoot feign基于HttpStatus重试
场景 基于springboot开发的项目,对接第三方,第三方的接口有限流策略,某个时间段内有调用频率限制,返回的状态码HttpStatus不是200,而HttpStatus是429。现基于HttpStatus我们发起的重试。 技术点 springbootfeign fe…...

【记录49】vue2 vue-office在线预览 docx、pdf、excel文档
vue2 在线预览 docx、pdf、excel文档 docx npm install vue-office/docx vue-demi0.14.6 指定版本 npm install vue-office/docx vue-demi <template><VueOfficeDocx :src"pdf" style"height: 100vh;" rendere"rendereHandler" error&…...
正则表达式中^的用法
正则表达式中^的用法 1.用法一: 限定开头 文档上给出了解释是匹配输入的开始,如果多行标示被设置成了true,同时会匹配后面紧跟的字符 比如 /^A/会匹配"An e"中的A,但是不会匹配"ab A"中的A 比如(\s|^)表示空字符串或字…...
WPF 关于界面UI菜单权限(或者任意控件的显示权限)的简单管理--只是简单简单简单简单
1.定义你的User类 public class User{public User(){ID ObjectId.NewObjectId().ToString();}public string? ID { get; set; }public string? Account { get; set; }public string? Password { get; set; }public string? PasswordMD5 { get; set; }public AccountType?…...

Https身份鉴权(小迪网络安全笔记~
附:完整笔记目录~ ps:本人小白,笔记均在个人理解基础上整理,若有错误欢迎指正! 5.2 Https&身份鉴权 引子:上一篇主要对Http数据包结构、内容做了介绍,本篇则聊聊Https、身份鉴权等技术。 …...
AngularJS 输入验证
AngularJS 输入验证 AngularJS 是一个强大的 JavaScript 框架,它允许开发者构建动态的、高性能的 Web 应用程序。在处理用户输入时,确保数据的准确性和完整性至关重要。AngularJS 提供了一套内置的输入验证机制,可以帮助开发者轻松地实现这一目标。 为什么需要输入验证? …...
【网络安全】WIFI WPA/WPA2协议:深入解析与实践
WIFI WPA/WPA2协议:深入解析与实践 1. WPA/WPA2 协议 1.1 监听 Wi-Fi 流量 解析 WPA/WPA2 的第一步是监听 Wi-Fi 流量,捕获设备与接入点之间的 4 次握手数据。然而,设备通常不会频繁连接或重新连接,为了加速过程,攻…...

前端使用xlsx-js-style导出Excel,带样式,并处理合并单元格边框显示不全和动态插入表头解决
一、在学习之前,先给出一些学习/下载地址: xlsx-js-style下载地址 https://github.com/gitbrent/xlsx-js-style 或者 https://www.npmjs.com/package/xlsx-js-style SheetJS中文教程: https://xlsx.nodejs.cn/docs/csf/cell 二、先看样…...
自动化工具ansible部署和实践
1 介绍和部署 1.1 介绍 ansible的功能 我爱你在当今的IT自动化领域,Ansible无疑是一个无法被忽视的重要角色。其便利性和高效性受到了广大开发者和系统管理员的一致好评,成为了配置管理和应用部署的首选工具。然而,对于一些初学者来说&#…...

无人机推流直播平台EasyDSS视频技术如何助力冬季森林防火
冬季天干物燥,大风天气频繁,是森林火灾的高发期。相比传统的人力巡查,无人机具有更高的灵敏度和准确性,尤其在夜间或浓雾天气中,依然能有效地监测潜在火源。 无人机可以提供高空视角和实时图像传输,帮助巡…...
React Fiber
React Fiber 是 React 16 引入的全新重写的协调(Reconciliation)算法的实现,旨在改善 React 的更新机制和性能,尤其是在复杂应用和大量更新的场景下。它使得 React 更加灵活、可调度,能够实现优先级控制和中断更新等特…...

【前端】JavaScript 中的 map() 方法:高级解析与应用
博客主页: [小ᶻ☡꙳ᵃⁱᵍᶜ꙳] 本文专栏: 前端 文章目录 💯前言💯map() 方法的定义与核心特性1.1 方法定义1.2 主要特性 💯map() 方法的语法与高级用法2.1 基本语法2.2 简化写法与箭头函数2.3 结合链式操作 💯ma…...
《智能体开发实战(高阶)》四、系统化的日志周报智能体开发计划
智能体扩展与完善规划 为了将前几个章节的智能体逐步扩展为支持整个公司团队使用的高效工具,以下是分阶段的完善与扩写规划。每个阶段旨在提升功能覆盖范围、处理能力和用户体验,并为企业提供实际价值。 阶段一:基础功能完善 目标:巩固现有功能,提升健壮性和适用性。 支…...

什么是Apache日志?为什么Apache日志分析很重要?
Apache是全球最受欢迎的Web服务器软件,支持约30.2%的所有活跃网站。凭借其可靠性、灵活性和强大的功能,Apache数十年来一直是互联网的中坚力量。 一、Apache Web服务器的工作原理 Apache Web服务器的工作原理如下: 接收HTTP请求࿱…...
web vue 项目 Docker化部署
Web 项目 Docker 化部署详细教程 目录 Web 项目 Docker 化部署概述Dockerfile 详解 构建阶段生产阶段 构建和运行 Docker 镜像 1. Web 项目 Docker 化部署概述 Docker 化部署的主要步骤分为以下几个阶段: 构建阶段(Build Stage):…...

基于FPGA的PID算法学习———实现PID比例控制算法
基于FPGA的PID算法学习 前言一、PID算法分析二、PID仿真分析1. PID代码2.PI代码3.P代码4.顶层5.测试文件6.仿真波形 总结 前言 学习内容:参考网站: PID算法控制 PID即:Proportional(比例)、Integral(积分&…...

突破不可导策略的训练难题:零阶优化与强化学习的深度嵌合
强化学习(Reinforcement Learning, RL)是工业领域智能控制的重要方法。它的基本原理是将最优控制问题建模为马尔可夫决策过程,然后使用强化学习的Actor-Critic机制(中文译作“知行互动”机制),逐步迭代求解…...
ssc377d修改flash分区大小
1、flash的分区默认分配16M、 / # df -h Filesystem Size Used Available Use% Mounted on /dev/root 1.9M 1.9M 0 100% / /dev/mtdblock4 3.0M...

前端导出带有合并单元格的列表
// 导出async function exportExcel(fileName "共识调整.xlsx") {// 所有数据const exportData await getAllMainData();// 表头内容let fitstTitleList [];const secondTitleList [];allColumns.value.forEach(column > {if (!column.children) {fitstTitleL…...
c++ 面试题(1)-----深度优先搜索(DFS)实现
操作系统:ubuntu22.04 IDE:Visual Studio Code 编程语言:C11 题目描述 地上有一个 m 行 n 列的方格,从坐标 [0,0] 起始。一个机器人可以从某一格移动到上下左右四个格子,但不能进入行坐标和列坐标的数位之和大于 k 的格子。 例…...

【项目实战】通过多模态+LangGraph实现PPT生成助手
PPT自动生成系统 基于LangGraph的PPT自动生成系统,可以将Markdown文档自动转换为PPT演示文稿。 功能特点 Markdown解析:自动解析Markdown文档结构PPT模板分析:分析PPT模板的布局和风格智能布局决策:匹配内容与合适的PPT布局自动…...

2025盘古石杯决赛【手机取证】
前言 第三届盘古石杯国际电子数据取证大赛决赛 最后一题没有解出来,实在找不到,希望有大佬教一下我。 还有就会议时间,我感觉不是图片时间,因为在电脑看到是其他时间用老会议系统开的会。 手机取证 1、分析鸿蒙手机检材&#x…...

STM32HAL库USART源代码解析及应用
STM32HAL库USART源代码解析 前言STM32CubeIDE配置串口USART和UART的选择使用模式参数设置GPIO配置DMA配置中断配置硬件流控制使能生成代码解析和使用方法串口初始化__UART_HandleTypeDef结构体浅析HAL库代码实际使用方法使用轮询方式发送使用轮询方式接收使用中断方式发送使用中…...
【学习笔记】erase 删除顺序迭代器后迭代器失效的解决方案
目录 使用 erase 返回值继续迭代使用索引进行遍历 我们知道类似 vector 的顺序迭代器被删除后,迭代器会失效,因为顺序迭代器在内存中是连续存储的,元素删除后,后续元素会前移。 但一些场景中,我们又需要在执行删除操作…...