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. 不同事务方法之间的互相影响一般情况下不需要太过考虑。(真需要可以考虑用乐观锁)

五、底层原理

未完待续~

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

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

相关文章

基于ssm乐购游戏商城系统论文

摘 要 随着社会的发展&#xff0c;游戏品种越来越多&#xff0c;计算机的优势和普及使得乐购游戏商城系统的开发成为必需。乐购游戏商城系统主要是借助计算机&#xff0c;通过对信息进行管理。减少管理员的工作&#xff0c;同时也方便广大用户对个人所需信息的及时查询以及管理…

IO流【 文件字符输入、出流;带缓冲区的字符输入、出流;对象流】

day36 IO流 字符流继承图 字符流 继day35 应用场景&#xff1a;操作纯文本数据 注意&#xff1a;字符流 字节流编译器 编译器&#xff1a;可以识别中文字符和非中文字符&#xff0c;非中文字符获取1个字节&#xff08;一个字节一个字符&#xff09;&#xff0c;编译器会根据…

Electron打包vue+java+nginx 踩坑记录

记录下遇到的问题&#xff1a; ⚠注意&#xff1a;64位系统和32位系统的配置不太一样 1、运行npm run packager失败 原因&#xff1a;在package.json没有对应命令 解决&#xff1a;在package.json 中添加对应命令&#xff0c;其中testApp是你想要的输入的项目名称&#xff0…

langchain 使用本地通义千问

langchian 使用已经下载到本地的模型&#xff0c;我们使用通义千问 显存&#xff1a;24G 模型&#xff1a;qwen1.5-7B-Chat&#xff0c;qwen-7B-Chat 先使用 qwen-7B-Chat&#xff0c;会报错用不了&#xff1a; 看了下是不支持这中模型&#xff0c;但看列表中有一个 Qwen 字样…

Asterisk语音卡驱动DAHDI 3.2版本对于TDM410P的支持

目录 DAHDITDM410base.c什么是电话语音卡 资本掌控下的Asterisk虽然继续履行开源社区的承诺&#xff0c;但实际上小手还是会四处乱摸&#xff0c;比如对于Asterisk硬件驱动DAHDI&#xff0c;就做了些隐蔽的小动作。 DAHDI DAHDI 全称是 Digium Asterisk Hardware Device Inter…

xss.pwnfunction-Ugandan Knuckles

这个是把<>过滤掉了所以只能用js的事件 ?weya"onfocus"alert(1337)" autofocus"

不允许在constexpr函数中进行声明

这是我用pycharm在windows系统下复现sfm深度学习网络(Deep Two-View Structure-from-Motion Revisited&#xff09;遇见的问题&#xff0c;复现时有段代码pytorch扩展cuda/c&#xff0c;pycharm中出现C标准相关的报错如下&#xff1a; 在网上查找很久无果&#xff0c;后面通过…

java国产化云HIS基层医院系统源码 SaaS模式

目录 ​ 云HIS开发环境 功能模块介绍&#xff1a; 1、门诊模块 2、住院模块 3、药房、药库模块 ​编辑 4、电子病历模块 5、统计报表模块 6、系统管理模块 系统优势 云his之电子病历子系统功能 云 his 系统是运用云计算、大数据、物联网等新兴信息技术&#xff0c;按…

python爬虫———激发学习兴趣的案列(第十三天)

&#x1f388;&#x1f388;作者主页&#xff1a; 喔的嘛呀&#x1f388;&#x1f388; &#x1f388;&#x1f388;所属专栏&#xff1a;python爬虫学习&#x1f388;&#x1f388; ✨✨谢谢大家捧场&#xff0c;祝屏幕前的小伙伴们每天都有好运相伴左右&#xff0c;一定要天天…

linux内核驱动-在内核代码里添加设备结点

linux中&#xff0c;一切皆文件 我们在用户层用一些系统函数&#xff08;如&#xff1a;fopen等等&#xff09;时&#xff0c;会进入内核&#xff0c;内核会在字符注册了的设备号链表中查找。如果找到就运行我们写的设备文件的&#xff08;驱动&#xff09;函数 我们在前面已经…

[方案实操|数据技术]数据要素十大创新模式(1):基于区块链的多模态数据交易服务平台

“ 区块链以其公开共享、去中心化、不可篡改、可追溯和不可抵赖等优势&#xff0c;吸引了包括金融业、医疗业和政府部门等众多利益相关方的极大兴趣&#xff0c;被认为是解决数据安全交换问题的合适方案。” 武汉东湖大数据科技股份有限公司凭借基于区块链的多模态数据交易服务…

C语言---顺序表(一)

文章目录 前言1.数据结构1.1.什么是数据结构1.2.为什么需要数据结构&#xff1f; 2.顺序表2.1.顺序表的概念2.2.顺序表分类 3.动态顺序表的实现 前言 在前面20多篇博客中&#xff0c;我们已经学习完了C语言知识&#xff0c;下面我们开启新的一章—数据结构。 1.数据结构 1.1.…

消息队列介绍

MQ&#xff08;Message Queue&#xff09;&#xff1a;消息队列。通过典型的生产者和消费者模型&#xff0c;生产者不断向消息队列中生产消息&#xff0c;消费者不断的从队列中获取消息。因为消息的生产和消费都是异步的&#xff0c;而且只关心消息的发送和接收&#xff0c;没有…

106. 跑步锻炼(结果填空)

public class Main { public static void main(String[] args) { int startYear 2000; int startMonth 1; int startDay 1; // 周六 int endYear 2020; int endMonth 10; int endDay 1; // 周四 int totalDistance 0; // 计算开始日期到结束日期之间的每一天 …

分布式文件系统

引言&#xff1a; GFS是一个可扩展的分布式文件系统&#xff0c;用于大型的、分布式的、对大量数据进行访问的应用。它运行于廉价的普通硬件上&#xff0c;并提供容错功能。它可以给大量的用户提供总体性能较高的服务。 一、 GlusterFS 概述 1.1 GlusterFS简介 GlusterFS 是…

7B超越百亿级,北大开源aiXcoder-7B最强代码大模型,企业部署最佳选择

ChatGPT狂飙160天&#xff0c;世界已经不是之前的样子。 新建了免费的人工智能中文站https://ai.weoknow.com 新建了收费的人工智能中文站https://ai.hzytsoft.cn/ 更多资源欢迎关注 ​ 对代码大模型而言&#xff0c;比能做编程题更重要的&#xff0c;是看是能不能适用于企业…

LMDoply部署实战

使用LMDeoply部署各类开源大模型&#xff0c;进行推理实践。 一. 环境准备 1. 创建Conda环境 studio-conda -t lmdeploy -o pytorch-2.1.2 2. 安装LMDeploy 激活刚刚创建的虚拟环境。 conda activate lmdeploy 安装0.3.0版本的lmdeploy。 pip install lmdeploy[all]0.3.…

day77 JSPServlet

知识点&#xff1a; 1Web工程 2JSP是什么&#xff1f;JSP页面包含哪些内容&#xff1f;JSP页面执行原理 3JSP九大内置对象&#xff0c;及四个作用域 4什么是SERVLET&#xff1f;及servlet相关API 5MVC模型 6EL表达式及JSTL标签库的使用 7在JSP页面实现分页和多条件查询 …

Harmony鸿蒙南向驱动开发-Watchdog

看门狗&#xff08;Watchdog&#xff09;&#xff0c;又称看门狗计时器&#xff08;Watchdog timer&#xff09;&#xff0c;是一种硬件计时设备。一般有一个输入、一个输出&#xff0c;输入叫做喂狗&#xff0c;输出连接到系统的复位端。当系统主程序发生错误导致未及时清除看…

vue实现从本地上传头像功能

上传头像&#xff1a; <template><div><el-card class"box-card"><div slot"header" class"clearfix"><span>更换头像</span></div><div><!-- 图片、用来展示用户选择的头像 --><img…