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

news/2024/5/2 22:10:43/文章来源:https://blog.csdn.net/weixin_43975276/article/details/137543856

一、背景

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

下面的例子全部基于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.luyixian.cn/news_show_1045834.aspx

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

相关文章

Linux JDK修改不生效

原JDK8&#xff0c;现需要切换为JDK11&#xff0c;环境变量已经修改为11&#xff0c;但java -version还是显示8。 ln -s -f /home/jdk-11.0.19/bin/java

稀碎从零算法笔记Day45-LeetCode:电话号码的字母组合

题型&#xff1a;映射、回溯算法、递归 链接&#xff1a;17. 电话号码的字母组合 - 力扣&#xff08;LeetCode&#xff09; 来源&#xff1a;LeetCode 题目描述 给定一个仅包含数字 2-9 的字符串&#xff0c;返回所有它能表示的字母组合。答案可以按 任意顺序 返回。 给出…

AI大模型引领未来智慧科研暨ChatGPT自然科学高级应用

以ChatGPT、LLaMA、Gemini、DALLE、Midjourney、Stable Diffusion、星火大模型、文心一言、千问为代表AI大语言模型带来了新一波人工智能浪潮&#xff0c;可以面向科研选题、思维导图、数据清洗、统计分析、高级编程、代码调试、算法学习、论文检索、写作、翻译、润色、文献辅助…

基于Springboot中小企业设备管理系统设计与实现(论文+源码)_kaic

摘 要 随着信息技术和网络技术的飞速发展&#xff0c;人类已进入全新信息化时代&#xff0c;传统管理技术已无法高效&#xff0c;便捷地管理信息。为了迎合时代需求&#xff0c;优化管理效率&#xff0c;各种各样的管理系统应运而生&#xff0c;各行各业相继进入信息管理时代&a…

TensorFlow学习之:深度学习基础

神经网络基础 神经网络是深度学习的核心&#xff0c;它们受人脑的结构和功能启发&#xff0c;能够通过学习大量数据来识别模式和解决复杂问题。神经网络的基本工作原理包括前向传播和反向传播两个阶段。 前向传播&#xff08;Forward Propagation&#xff09; 前向传播是神经…

AI大模型之ChatGPT科普(深度好文)

目录 训练ChatGPT分几步&#xff1f; 如何炼成ChatGPT&#xff1f; 如何微调ChatGPT? 如何强化ChatGPT? 如何调教ChatGPT? AI思维链是什么&#xff1f; GPT背后的黑科技Transformer是什么&#xff1f; Transformer在计算机视觉上CV最佳作品&#xff1f; ChatGPT是人…

修复 Windows 上的 PyTorch 1.1 github 模型加载权限错误

问题: 在 Windows 计算机上执行示例 github 模型加载时,生成了 master.zip 文件的权限错误(请参阅下面的错误堆栈跟踪)。 错误堆栈跟踪: 在[4]中:en2de = torch.hub.load(pytorch/fairseq, transformer.wmt16.en-de, tokenizer=moses, bpe=subword_nmt) 下载:“https://…

【R基础】一组数据计算均值、方差与标准差方法及意义

【R基础】一组数据计算均值、方差与标准差方法及意义 均值、方差与标准差是用来描述数据分布情况 均值&#xff1a;用来衡量一组数据整体情况。 数据离散程度度量标准&#xff1a; 方差&#xff08;均方&#xff0c;s^2&#xff0c;总体参数&#xff0c;离均差平方和&#…

实用工具推荐:如何使用MechanicalSoup进行网页交互

在当今数字化时代&#xff0c;网页交互已经成为日常生活和工作中不可或缺的一部分。无论是自动填写表单、抓取网页数据还是进行网站测试&#xff0c;都需要一种高效而可靠的工具来实现网页交互。而在众多的选择中&#xff0c;MechanicalSoup作为一种简单、易用且功能强大的Pyth…

GitLab教程(一):安装Git、配置SSH公钥

文章目录 序一、Git安装与基本配置&#xff08;Windows&#xff09;下载卸载安装基本配置 二、SSH密钥配置 序 为什么要使用代码版本管理工具&#xff1a; 最近笔者确实因为未使用代码版本管理工具遇到了一些愚蠢的问题&#xff0c;笔者因此认为代码版本管理工具对于提高团队…

基于FPGA的以太网相关文章导航

首先需要了解以太网的一些接口协议标准&#xff0c;常见的MII、GMII、RGMII时序&#xff0c;便于后续开发。 【必读】从MII到RGMII&#xff0c;一文了解以太网PHY芯片不同传输接口信号时序&#xff01; 介绍一款比较老的以太网PHY芯片88E1518&#xff0c;具有RGMII接口&#xf…

防止狗上沙发,写一个浏览器实时识别目标检测功能

家里有一条狗&#x1f436;&#xff0c;很喜欢乘人不备睡沙发&#x1f6cb;️&#xff0c;恰好最近刚搬家 狗迎来了掉毛期 不想让沙发上很多毛。所以希望能识别到狗&#xff0c;然后播放“gun 下去”的音频&#x1f4e3;。 需求分析 需要一个摄像头&#x1f4f7; 利用 chrome…

openHarmony 如何从API9升级到API10

最近用从官方下载的DevEco Studio3.1开发小app, 需要用到第三方库&#xff0c;加载第三方库&#xff0c;并添加代码&#xff0c;编译时如下错误&#xff1a; hvigor Finished :entry:defaultGenerateMetadata… after 3 ms hvigor ERROR: Failed :entry:defaultMergeProfile… …

微信小程序页面交互综合练习 (重点:解决“setData of undefined”报错问题)

一、写一个注册表单&#xff0c;点击“注册”按钮将用户输入的数据带到服务器&#xff0c;并且能在控制台显示参数。 &#xff08;1&#xff09;首先&#xff0c;我需要在vscode里面创建一个简易的node.js服务器 //第一步:引入http模块 var http require(http); //第二步:创建…

算法刷题Day30 | 332.重新安排行程、51. N皇后、37. 解数独

目录 0 引言1 重新安排行程1.1 我的解题1.2 更好的解法 2 N皇后2.1 我的解题 3 解数独3.1 我的解题3.2 &#x1f64b;‍♂️ 作者&#xff1a;海码007&#x1f4dc; 专栏&#xff1a;算法专栏&#x1f4a5; 标题&#xff1a;算法刷题Day30 | 332.重新安排行程、51. N皇后、37. …

【图论】详解链式前向星存图法+遍历法

细说链式前向星存图法 首先要明白&#xff0c;链式前向星的原理是利用存边来进行模拟图。 推荐左神的视频–建图、链式前向星、拓扑排序 比方说有这样一张图&#xff0c;我们用链式前向星来进行模拟时&#xff0c;可以将每一条边都进行编号&#xff0c;其中&#xff0c;红色的…

刷题DAY49 | LeetCode 121-买卖股票的最佳时机 122-买卖股票的最佳时机II

121 买卖股票的最佳时机&#xff08;easy&#xff09; 给定一个数组 prices &#xff0c;它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。 你只能选择 某一天 买入这只股票&#xff0c;并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取…

【前端面试3+1】10 npm run dev 发生了什么、vue的自定义指令如何实现、js的数据类型有哪些及其不同、【最长公共前缀】

一、npm run dev发生了什么 运行npm run dev时&#xff0c;通常是在一个基于Node.js的项目中&#xff0c;用来启动开发服务器或者执行一些开发环境相关的任务。下面是一般情况下npm run dev会执行的步骤&#xff1a; 1. 查找package.json中的scripts字段&#xff1a; npm会在项…

双指针,滑动窗口

今天也是闲来无事&#xff0c;想去做一下&#xff0c;之前学过的某个题型&#xff0c;但是在中间突然发现了这个题&#xff0c;那时候年少无知&#xff0c;做不出来&#xff0c;今天也是很轻松的用双指针轻松拿捏&#xff0c;因此发帖。 传送门&#xff1a;逛画展 题解&#x…

VRRP虚拟路由实验(华为)

思科设备参考&#xff1a;VRRP虚拟路由实验&#xff08;思科&#xff09; 一&#xff0c;技术简介 VRRP&#xff08;Virtual Router Redundancy Protocol&#xff09;是一种网络协议&#xff0c;用于实现路由器冗余&#xff0c;提高网络可靠性和容错能力。VRRP允许多台路由器…