MySQL innoDB存储引擎多事务场景下的事务执行情况

一、背景

在日常开发中,对不同事务之间的隔离情况等理解如果不够清晰,很容易导致代码的效果和预期不符。因而在这对一些存在疑问的场景进行模拟。

下面的例子全部基于innoDB存储引擎。

二、场景:

2.1、两个事务修改同一行记录

正常来说,两个事务修改相同的记录,肯定会相互阻塞,排队执行的。

一开始号码为13827622366的客户的名称为哈哈哈。A事务先进入事务,但未执行到变更号码为13827622366的客户记录的操作(睡眠实现),B事务开启事务执行变更号码为13827622366的客户记录。

代码

	@ApiOperation(value = "transaction1", notes = "")@GetMapping(value = "/transaction1")@Transactional(rollbackFor = Exception.class)public Result<?> transaction1() throws InterruptedException {System.out.println("事务1开始");Thread.sleep(8000);//其他业务LambdaQueryWrapper<SdSchoolCustomer> queryWrapper=new LambdaQueryWrapper<>();queryWrapper.eq(SdSchoolCustomer::getPhone,"13827622377");List<SdSchoolCustomer> list = sdSchoolCustomerService.list(queryWrapper);if(CollectionUtil.isNotEmpty(list)){System.out.println("事务1:"+list.get(0).getName());}LambdaUpdateWrapper<SdSchoolCustomer> updateWrapper=new LambdaUpdateWrapper<>();updateWrapper.eq(SdSchoolCustomer::getPhone,"13827622377").set(SdSchoolCustomer::getName,"事务1name");sdSchoolCustomerService.update(updateWrapper);System.out.println("事务1结束");return Result.ok();}@ApiOperation(value = "transaction2", notes = "")@GetMapping(value = "/transaction2")@Transactional(rollbackFor = Exception.class)public Result<?> transaction2() {System.out.println("事务2开始");LambdaQueryWrapper<SdSchoolCustomer> queryWrapper=new LambdaQueryWrapper<>();queryWrapper.eq(SdSchoolCustomer::getPhone,"13827622366");List<SdSchoolCustomer> list = sdSchoolCustomerService.list(queryWrapper);if(CollectionUtil.isNotEmpty(list)){System.out.println("事务2:"+list.get(0).getName());}LambdaUpdateWrapper<SdSchoolCustomer> updateWrapper=new LambdaUpdateWrapper<>();updateWrapper.eq(SdSchoolCustomer::getPhone,"13827622366").set(SdSchoolCustomer::getName,"事务2name");sdSchoolCustomerService.update(updateWrapper);System.out.println("事务2结束");return Result.ok();}

执行结果

最后该客户的name是“事务1name”。结合下图可以看到,事务1先开启了事务然后睡眠了,接着事务2开启事务,执行查询然后更新记录,接着事务1睡眠完毕,执行查询,查到了事务2提交之后的数据,然后更新记录。也就是说,开启事务之后,在还没有执行到更新操作之前,其他事务还是可以更新该数据并且不会被阻塞。

把睡眠放到update后面,再来验证一下。

代码

@ApiOperation(value = "transaction1", notes = "")@GetMapping(value = "/transaction1")@Transactional(rollbackFor = Exception.class)public Result<?> transaction1() throws InterruptedException {System.out.println("事务1开始");LambdaQueryWrapper<SdSchoolCustomer> queryWrapper=new LambdaQueryWrapper<>();queryWrapper.eq(SdSchoolCustomer::getPhone,"13827622366");List<SdSchoolCustomer> list = sdSchoolCustomerService.list(queryWrapper);if(CollectionUtil.isNotEmpty(list)){System.out.println("事务1:"+list.get(0).getName());}LambdaUpdateWrapper<SdSchoolCustomer> updateWrapper=new LambdaUpdateWrapper<>();updateWrapper.eq(SdSchoolCustomer::getPhone,"13827622366").set(SdSchoolCustomer::getName,"事务1name");sdSchoolCustomerService.update(updateWrapper);Thread.sleep(8000);//其他业务System.out.println("事务1结束");return Result.ok();}@ApiOperation(value = "transaction2", notes = "")@GetMapping(value = "/transaction2")@Transactional(rollbackFor = Exception.class)public Result<?> transaction2() {System.out.println("事务2开始");LambdaQueryWrapper<SdSchoolCustomer> queryWrapper=new LambdaQueryWrapper<>();queryWrapper.eq(SdSchoolCustomer::getPhone,"13827622366");List<SdSchoolCustomer> list = sdSchoolCustomerService.list(queryWrapper);if(CollectionUtil.isNotEmpty(list)){System.out.println("事务2:"+list.get(0).getName());}LambdaUpdateWrapper<SdSchoolCustomer> updateWrapper=new LambdaUpdateWrapper<>();updateWrapper.eq(SdSchoolCustomer::getPhone,"13827622366").set(SdSchoolCustomer::getName,"事务2name");sdSchoolCustomerService.update(updateWrapper);System.out.println("事务2结束");return Result.ok();}

执行结果

最后该客户的name是“事务2name”。结合下图,事务1开始执行查询,并执行更新数据的操作,然后进入睡眠。这个时候事务2开始执行,也查询(因为事务1还没提交,所以查到的也还是原来的值),尝试执行更新数据操作,但这次被阻塞了,一直到事务1提交了事务之后才能继续执行update语句后面的代码。

结论

不同事务更新同一条记录,假如A先执行到更新该行记录的事务,A会阻塞其他想要更新该记录的事务;假如B事务在(A事务执行了更新操作但未提交事务之前)也执行到更新该记录,B事务的代码会被阻塞,必须等A事务提交或回滚了之后,B事务的代码才能继续往下执行。

另外,因为在MySQL中,一个SQL也相当于一个事务,所以一个事务一个非事务修改同一行记录的执行结果和上面也是一样的。

2.2、两个事务修改同一个表的不同行记录

事务1开启事务,修改号码为13827622377的记录的名称,然后睡眠;事务2开启事务,修改号码为13827622366的记录,看看事务2是否还会被阻塞。

代码

	@ApiOperation(value = "transaction1", notes = "")@GetMapping(value = "/transaction1")@Transactional(rollbackFor = Exception.class)public Result<?> transaction1() throws InterruptedException {System.out.println("事务1开始");LambdaUpdateWrapper<SdSchoolCustomer> updateWrapper=new LambdaUpdateWrapper<>();updateWrapper.eq(SdSchoolCustomer::getPhone,"13827622366").set(SdSchoolCustomer::getName,"事务1name");sdSchoolCustomerService.update(updateWrapper);Thread.sleep(8000);//其他业务System.out.println("事务1结束");return Result.ok();}@ApiOperation(value = "transaction2", notes = "")@GetMapping(value = "/transaction2")public Result<?> transaction2() {System.out.println("事务2开始");LambdaUpdateWrapper<SdSchoolCustomer> updateWrapper=new LambdaUpdateWrapper<>();updateWrapper.eq(SdSchoolCustomer::getPhone,"13827622377").set(SdSchoolCustomer::getName,"事务2name");sdSchoolCustomerService.update(updateWrapper);System.out.println("事务2结束");return Result.ok();}

执行结果

两个事务都成功提交了,从下图结果来看,事务2并没有因为事务1还未提交而被阻塞,说明开启事务的时候修改不同的行记录不会互相影响。(这样事务执行的效率更高了)

2.3、上面几种场景得出的结论

从上面的几个例子可以看出,事务执行到更新记录操作之后,该行记录暂时不可被该事务之外的操作更改,无论是开启事务来变更记录还是直接变更记录,都会被阻塞。要等待事务1执行完毕提交或回滚事务之后才可以进行记录更新并继续往下执行。(阻塞的位置在更新记录的代码处)

2.4、A事务第一次查询数据,B事务更新数据,A事务再次查询数据

同一条记录,两次查询有什么区别?

innoDB的默认隔离级别是可重复读,这意味着从第一次查询数据开始,这条数据就被记录下来了,只要当前事务没有更改该记录,并且还在当前事务内,无论查询多少次,该条记录的值都是一样的,相当于后续查到的都是记录的一个快照。(这就是事务之间的数据隔离,自己事务更新的数据是可以看到更新之后的值的)

号码为13827622366的记录的name一开始的值是“哈哈哈4”。事务1先开启事务并进行第一次查询,然后睡眠;这时事务2开启事务,并更新该记录的name为“事务2name”;接着事务1睡眠完毕进行第二次查询。

代码

@ApiOperation(value = "transaction1", notes = "")@GetMapping(value = "/transaction1")@Transactional(rollbackFor = Exception.class)public Result<?> transaction1() throws InterruptedException {System.out.println("事务1开始");LambdaQueryWrapper<SdSchoolCustomer> queryWrapper=new LambdaQueryWrapper<>();queryWrapper.eq(SdSchoolCustomer::getPhone,"13827622366");List<SdSchoolCustomer> list = sdSchoolCustomerService.list(queryWrapper);if(CollectionUtil.isNotEmpty(list)){System.out.println("事务1第一次查询:"+list.get(0).getName());}Thread.sleep(8000);//其他业务list = sdSchoolCustomerService.list(queryWrapper);if(CollectionUtil.isNotEmpty(list)){System.out.println("事务1第二次查询:"+list.get(0).getName());}System.out.println("事务1结束");return Result.ok();}@ApiOperation(value = "transaction2", notes = "")@GetMapping(value = "/transaction2")public Result<?> transaction2() {System.out.println("事务2开始");LambdaUpdateWrapper<SdSchoolCustomer> updateWrapper=new LambdaUpdateWrapper<>();updateWrapper.eq(SdSchoolCustomer::getPhone,"13827622366").set(SdSchoolCustomer::getName,"事务2name");sdSchoolCustomerService.update(updateWrapper);System.out.println("事务2结束");return Result.ok();}

执行结果

在事务1还在睡眠的时候,在系统查询该记录,该记录的name已经更新为“事务2name”。但当事务1第二次查询的时候查询出的结果还是“哈哈哈4”,和第一次查询的结果保持一致,符合可重复读。

解析

innoDB的默认隔离级别是可重复读,要求在一个事务内多次读取同一条记录的结果保持一致。MySQL是通过快照读来实现的,在事务内第一次查询数据的时候,记录所有行记录当前最新的已提交的事务版本号,并形成一个视图。该事务内的后续查询都要和视图内的数据进行比对,只能查询出记录的事务版本号及以前版本的数据,从而实现行记录的快照读。(快照是整个表那一刻的快照,下两个例子验证)

2.5、A事务第一次查询数据,B事务插入数据,A事务再次查询数据

两次查询记录的数量有什么不同?记录的数量上也是实现了可重复读。

号码为13827622366的记录一开始只有一条。事务1开启事务,并第一次查询号码为1382762236的记录个数,然后睡眠;接着事务2开启事务,新插入一条号码为13827622366的记录;接着事务1睡眠结束,进行第二次查询号码为1382762236的记录个数。

代码

	@ApiOperation(value = "transaction1", notes = "")@GetMapping(value = "/transaction1")@Transactional(rollbackFor = Exception.class)public Result<?> transaction1() throws InterruptedException {System.out.println("事务1开始");LambdaQueryWrapper<SdSchoolCustomer> queryWrapper=new LambdaQueryWrapper<>();queryWrapper.eq(SdSchoolCustomer::getPhone,"13827622366");List<SdSchoolCustomer> list = sdSchoolCustomerService.list(queryWrapper);if(CollectionUtil.isNotEmpty(list)){System.out.println("事务1第一次查询数量:"+list.size());}Thread.sleep(8000);//其他业务list = sdSchoolCustomerService.list(queryWrapper);if(CollectionUtil.isNotEmpty(list)){System.out.println("事务1第二次查询数量:"+list.size());}System.out.println("事务1结束");return Result.ok();}@ApiOperation(value = "transaction2", notes = "")@GetMapping(value = "/transaction2")public Result<?> transaction2() {System.out.println("事务2开始");SdSchoolCustomer customer=new SdSchoolCustomer();customer.setCustomerNo(RandomUtil.randomString(10));customer.setPhone("13827622366");sdSchoolCustomerService.save(customer);System.out.println("事务2结束");return Result.ok();}

执行结果

在事务1还在睡眠的时候,在系统查询号码为1382762236的记录,能查到两条记录,说明事务2所插入的新数据已经生效了。但事务1第二次查到的数量却还是1,说明在事务内,数据在数量上也是存在快照读的。

  2.6、A事务查询甲记录,B事务修改乙记录,A事务接着查询乙记录

上述的甲记录和乙记录属于同一个表,看看A事务第一次查询所记录的快照是针对整个表还是仅针对查到的记录。

一开始号码为13827622377的记录的名称为“哈哈哈5”。事务1先开启事务,查询号码为13827622366的记录,接着睡眠;这时候事务2开启事务,更新号码是13827622377的记录的名称为“事务2name”;然后事务1睡眠结束,查询号码为13827622377的记录,看看查到的记录是事务2更新前还是更新后的数据。

代码

	@ApiOperation(value = "transaction1", notes = "")@GetMapping(value = "/transaction1")@Transactional(rollbackFor = Exception.class)public Result<?> transaction1() throws InterruptedException {System.out.println("事务1开始");LambdaQueryWrapper<SdSchoolCustomer> queryWrapper=new LambdaQueryWrapper<>();queryWrapper.eq(SdSchoolCustomer::getPhone,"13827622366");List<SdSchoolCustomer> list = sdSchoolCustomerService.list(queryWrapper);if(CollectionUtil.isNotEmpty(list)){System.out.println("事务1第一次查询:"+list.get(0).getName());}Thread.sleep(8000);//其他业务queryWrapper.clear();queryWrapper.eq(SdSchoolCustomer::getPhone,"13827622377");list = sdSchoolCustomerService.list(queryWrapper);if(CollectionUtil.isNotEmpty(list)){System.out.println("事务1第二次查询:"+list.get(0).getName());}System.out.println("事务1结束");return Result.ok();}@ApiOperation(value = "transaction2", notes = "")@GetMapping(value = "/transaction2")public Result<?> transaction2() {System.out.println("事务2开始");LambdaQueryWrapper<SdSchoolCustomer> queryWrapper=new LambdaQueryWrapper<>();queryWrapper.eq(SdSchoolCustomer::getPhone,"13827622377");List<SdSchoolCustomer> list = sdSchoolCustomerService.list(queryWrapper);if(CollectionUtil.isNotEmpty(list)){System.out.println("事务2第一次查询:"+list.get(0).getName());}LambdaUpdateWrapper<SdSchoolCustomer> updateWrapper=new LambdaUpdateWrapper<>();updateWrapper.eq(SdSchoolCustomer::getPhone,"13827622377").set(SdSchoolCustomer::getName,"事务2name");sdSchoolCustomerService.update(updateWrapper);list = sdSchoolCustomerService.list(queryWrapper);if(CollectionUtil.isNotEmpty(list)){System.out.println("事务2第二次查询:"+list.get(0).getName());}System.out.println("事务2结束");return Result.ok();}

执行结果

事务1还在睡眠的时候,在系统查询号码为13827622377的记录,该记录的name已经更新为“事务2name”。事务1第一次查询号码为13827622366的记录的名称并打印只是用来代表查到了该表的数据;接着事务2开启,更新号码为13827622377的记录的名称;事务1睡眠完毕,查询号码为13827622377的记录的名称,发现查询到的结果是事务2修改之前的结果。和从系统直接查询到的结果不一致,说明事务1在第一次查询的时候保存的快照是针对整个表的快照。

三、总结

  1. 事务之间的互相阻塞是在执行到更新操作代码并且更新到相同表的相同行记录情况下才会触发的。(相当于需要顺序执行)
  2. MySQL innoDB存储引擎 可重复读隔离级别下,事务在第一次查询表记录的时候记录的是整个表的快照,后续查询无论是数据上,还是数据的量上都是快照读。
  3. 可重复读隔离级别下,依旧存在幻读问题。可重复读的隔离级别要求事务内多次查询同一个表的数据和数据的量保持一致,这意味着事务内读取到的数据量和实际的数据量可能是不一致的,也就是可能读取到不存在的数据或者读取不到已插入的数据,从而出现幻读问题。

四、实际开发中使用事务的一些见解

  1. 一些业务如果需要同时用到锁和事务,一般锁加在事务外层。
  2. 不同事务方法之间的互相影响一般情况下不需要太过考虑。(真需要可以考虑用乐观锁)

五、底层原理

未完待续~

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

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

相关文章

自动化测试框架-senlenium(2)

目录 1.前言 2.鼠标点击 2.1click点击对象 2.2senk_keys在对象上模拟键盘输入 2.3清除对象输入的文本内容 2.4submit提交 2.5 text用于获取文本信息 ​编辑3.获取信息 3.1获取title 3.2获取url 1.前言 前面我们讲了如何定位元素,那么我们把元素定位到了以后,又如何…

【力扣】104. 二叉树的最大深度、111. 二叉树的最小深度

104. 二叉树的最大深度 题目描述 给定一个二叉树 root &#xff0c;返回其最大深度。 二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。 示例 1&#xff1a; 输入&#xff1a;root [3,9,20,null,null,15,7] 输出&#xff1a;3 示例 2&#xff1a; 输…

ENSP防火墙配置策略路由及ip-link探测

拓扑 配置目标 1.A区域走ISP1&#xff0c;B区域走ISP2 2. isp线路故障时及时切换到另一条线路 配置接口及安全区域 配置安全策略 配置nat 配置默认路由 配置ip-link 配置策略路由 cl-1 cl-2 验证配置成功 策略路由 A走ISP1 B走ISP2 验证线路故障 isp1 in g0/0/0 shoutdow…

Qt——示波器/图表 QCustomPlot

一、介绍 QCustomPlot是一个用于绘图和数据可视化的Qt C小部件。它没有进一步的依赖关系&#xff0c;提供友好的文档帮助。这个绘图库专注于制作好看的&#xff0c;出版质量的2D绘图&#xff0c;图形和图表&#xff0c;以及为实时可视化应用程序提供高性能。QCustomPlot可以导出…

HWOD:走方格的方案数

一、自己的解题思路 1、(0,m)和(n,0) (0,m)表示处在棋盘的左边线&#xff0c;此刻能回到原点的路线只有一个&#xff0c;就是一路向上 (n,0)表示处在棋盘的上边线&#xff0c;此刻能回到原点的路线只有一个&#xff0c;就是一路向左 2、(1,1) (1,1)表示只有一个方格&#…

02 Git 之IDEA 集成使用 GitHub(Git同时管理本地仓库和远程仓库)

2 .IDEA 集成使用 GitHub&#xff08;Git同时管理本地仓库和远程仓库&#xff09; 首先在 IDEA 的设置中绑定 GitHub 的账号 先创建一个 test1.txt 文件&#xff0c;内容为 aaa. 最上一栏 VCS&#xff0c; SHARE ON GitHub&#xff0c;然后选择要发送到远程仓库的文件即可。…

Vue实现防篡改水印的效果。删除元素无效!更改元素属性无效!支持图片、元素、视频等等。

1、演示 2、水印的目的 版权保护&#xff1a;水印可以在图片、文档或视频中嵌入作者、品牌或版权所有者的信息&#xff0c;以防止未经授权的复制、传播或使用。当其他人使用带有水印的内容时&#xff0c;可以追溯到原始作者或版权所有者&#xff0c;从而加强版权保护。 身份识…

跟TED演讲学英文:A new way to build AI, openly by Percy Liang

A new way to build AI, openly Link: https://www.ted.com/talks/percy_liang_a_new_way_to_build_ai_openly? Speaker: Percy Liang Date: October 2023 文章目录 A new way to build AI, openlyIntroductionVocabularyTranscriptSummary后记 Introduction Today’s AI …

vitepress/vite vue3 怎么实现vue模版字符串实时编译

如果是vue模版字符串的话&#xff0c;先解析成模版对象 另一篇文章里有vue模版字符串解析成vue模版对象-CSDN博客 //vue3写法&#xff08;vue2可以用new Vue.extend(vue模版对象)来实现&#xff09;import { createApp, defineComponent } from vue;// 定义一个简单的Vue组件c…

登陆qq,经常收到qq游戏中心的推送信息,关闭推送信息

手动关闭推送信息的步骤&#xff1a; 1.点开左侧游戏中心 2、在打开界面&#xff0c;点击左下角自己的头像 3、打开设置中心&#xff0c;关闭所有的推送 4、完成关闭&#xff0c;不会推送了

头歌-机器学习 第13次实验 特征工程——共享单车之租赁需求预估

第1关&#xff1a;数据探索与可视化 任务描述 本关任务&#xff1a;编写python代码&#xff0c;完成一天中不同时间段的平均租赁数量的可视化功能。 相关知识 为了完成本关任务&#xff0c;你需要掌握&#xff1a; 读取数据数据探索与可视化 读取数据 数据保存在./step1/…

vmware esxi6.0安装配置操作

系统安装及配置 在服务器上安装ESXI 6.0 提示是否继续安装 如果不想安装,按ESC后再按F11即可,稍后电脑会重启. 继续安装,则按回车键 按F11同意声明继续 选择将EXSI 安装到哪个硬盘上,我这里使用的是虚拟机,所以只有这一个选项 选择默认键盘布局,默认的美国键盘即可 设置root…

华为ensp中PPP(点对点协议)中的CHAP认证 原理和配置命令

作者主页&#xff1a;点击&#xff01; ENSP专栏&#xff1a;点击&#xff01; 创作时间&#xff1a;2024年4月11日6点00分 PPP协议&#xff08;Point-to-Point Protocol&#xff09;是点到点协议&#xff0c;是一种常用的串行链路层协议&#xff0c;用于在两个节点之间建立点…

Facial Micro-Expression Recognition Based on DeepLocal-Holistic Network 阅读笔记

中科院王老师团队的工作&#xff0c;用于做微表情识别。 摘要&#xff1a; Toimprove the efficiency of micro-expression feature extraction,inspired by the psychological studyof attentional resource allocation for micro-expression cognition,we propose a deep loc…

【网站项目】校园失物招领小程序

&#x1f64a;作者简介&#xff1a;拥有多年开发工作经验&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的项目或者毕业设计。 代码可以私聊博主获取。&#x1f339;赠送计算机毕业设计600个选题excel文件&#xff0c;帮助大学选题。赠送开题报告模板&#xff…

chromium 协议栈 cronet ios 踩坑案例

1、请求未携带 Accept-Language http header 出现图片加载失败 现象&#xff1a; 访问 https://www.huawei.com/cn/?ic_mediumdirect&ic_sourcesurlent 时出现图片加载失败的问题 预期结果&#xff1a; 原因&#xff1a; 网络库删除了添加 Accept-Language header 的逻…

搭建NFS服务器,部署k8s集群,并在k8s中使用NFS作为持久化储存

&#x1f407;明明跟你说过&#xff1a;个人主页 &#x1f3c5;个人专栏&#xff1a;《Kubernetes航线图&#xff1a;从船长到K8s掌舵者》 &#x1f3c5; &#x1f516;行路有良友&#xff0c;便是天堂&#x1f516; 目录 一、前言 1、k8s概述 2、NFS简介 二、NFS服务器…

分享 WebStorm 2024 激活的方案,支持JetBrains全家桶

大家好&#xff0c;欢迎来到金榜探云手&#xff01; WebStorm公司简介 JetBrains 是一家专注于开发工具的软件公司&#xff0c;总部位于捷克。他们以提供强大的集成开发环境&#xff08;IDE&#xff09;而闻名&#xff0c;如 IntelliJ IDEA、PyCharm、和 WebStorm等。这些工具…

SOCKS代理是如何增强网络隐私?

在数字化时代&#x1f310;&#xff0c;网络隐私的重要性日益凸显。个人和组织都在寻找有效的方法来保护自己的网络活动不受侵犯。SOCKS代理作为一种流行的网络协议&#xff0c;提供了一种有效的手段来增强网络隐私。本文将详细介绍SOCKS代理是如何工作的&#xff0c;以及它是如…

C++模板编程

模板是泛型编程的基础&#xff0c;先给出泛型编程的概念。 泛型编程&#xff1a;编写与类型无关的通用代码&#xff0c;是代码复用的一种手段。 应用场景&#xff1a;比如要实现一个通用的&#xff0c;进行两个变量互相交换的函数&#xff0c;此时可以通过函数重载的方式&…