jdbctemplate 开启事务_SpringBoot 系列教程之事务隔离级别知识点小结

55602a9078cb0c1c1fb72a2bfe1f1d54.png

上一篇博文介绍了声明式事务@Transactional的简单使用姿势,最文章的最后给出了这个注解的多个属性,本文将着重放在事务隔离级别的知识点上,并通过实例演示不同的事务隔离级别下,脏读、不可重复读、幻读的具体场景

I. 基础知识

在进入正文之前,先介绍一下事务隔离级别的一些基础知识点,详细内容,推荐参考博文

mysql 之锁与事务[1]

1. 基本概念

以下基本概念源于个人理解之后,通过简单的 case 进行描述,如有问题,欢迎拍砖

更新丢失

简单来讲,两个事务 A,B 分别更新一条记录的 filedA, filedB 字段,其中事务 B 异常,导致回滚,将这条记录的恢复为修改之前的状态,导致事务 A 的修改丢失了,这就是更新丢失

脏读

读取到另外一个事务未提交的修改,所以当另外一个事务是失败导致回滚的时候,这个读取的数据其实是不准确的,这就是脏读

不可重复读

简单来讲,就是一个事务内,多次查询同一个数据,返回的结果居然不一样,这就是不可重复度(重复读取的结果不一样)

幻读

同样是多次查询,但是后面查询时,发现多了或者少了一些记录

比如:查询 id 在[1,10]之间的记录,第一次返回了 1,2,3 三条记录;但是另外一个事务新增了一个 id 为 4 的记录,导致再次查询时,返回了 1,2,3,4 四条记录,第二次查询时多了一条记录,这就是幻读

幻读和不可重复读的主要区别在于:

  • 幻读针对的是查询结果为多个的场景,出现了数据的增加 or 减少
  • 不可重复读对的是某些特定的记录,这些记录的数据与之前不一致

2. 隔离级别

后面测试的数据库为 mysql,引擎为 innodb,对应有四个隔离级别

隔离级别说明fixnot fixRU(read uncommitted)未授权读,读事务允许其他读写事务;未提交写事务禁止其他写事务(读事务 ok)更新丢失脏读,不可重复读,幻读RC(read committed)授权读,读事务允许其他读写事务;未提交写事务,禁止其他读写事务更新丢失,脏读不可重复读,幻读RR(repeatable read)可重复度,读事务禁止其他写事务;未提交写事务,禁止其他读写事务更新丢失,脏读,不可重复度幻读serializable序列化读,所有事务依次执行更新丢失,脏读,不可重复度,幻读-

说明,下面纯为个人观点,不代表权威,谨慎理解和引用

  • 我个人的观点,rr 级别在 mysql 的 innodb 引擎上,配合 mvvc + gap 锁,已经解决了幻读问题
  • 下面这个 case 是幻读问题么?
    • 从锁的角度来看,步骤 1、2 虽然开启事务,但是属于快照读;而 9 属于当前读;他们读取的源不同,应该不算在幻读定义中的同一查询条件中
a44f35b088729dd8c695026ae7691f46.png

II. 配置

接下来进入实例演示环节,首先需要准备环境,创建测试项目

创建一个 SpringBoot 项目,版本为2.2.1.RELEASE,使用 mysql 作为目标数据库,存储引擎选择Innodb,事务隔离级别为 RR

1. 项目配置

在项目pom.xml文件中,加上spring-boot-starter-jdbc,会注入一个DataSourceTransactionManager的 bean,提供了事务支持

mysql    mysql-connector-javaorg.springframework.boot    spring-boot-starter-jdbc

2. 数据库配置

进入 spring 配置文件application.properties,设置一下 db 相关的信息

## DataSourcespring.datasource.url=jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=falsespring.datasource.username=rootspring.datasource.password=

3. 数据库

新建一个简单的表结构,用于测试

CREATETABLE`money` (  `id`int(11) unsignedNOTNULL AUTO_INCREMENT,  `name`varchar(20) NOTNULLDEFAULT''COMMENT'用户名',  `money`int(26) NOTNULLDEFAULT'0'COMMENT'钱',  `is_deleted`tinyint(1) NOTNULLDEFAULT'0',  `create_at`timestampNOTNULLDEFAULTCURRENT_TIMESTAMPCOMMENT'创建时间',  `update_at`timestampNOTNULLDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMPCOMMENT'更新时间',  PRIMARY KEY (`id`),  KEY`name` (`name`)) ENGINE=InnoDB AUTO_INCREMENT=1DEFAULTCHARSET=utf8mb4;

III. 实例演示

1. 初始化数据

准备一些用于后续操作的数据

@Componentpublicclass DetailDemo {    @Autowired    private JdbcTemplate jdbcTemplate;    @PostConstruct    public void init() {        String sql = "replace into money (id, name, money) values (320, '初始化', 200)," + "(330, '初始化', 200)," +                "(340, '初始化', 200)," + "(350, '初始化', 200)";        jdbcTemplate.execute(sql);    }}

提供一些基本的查询和修改方法

private boolean updateName(int id) {    String sql = "update money set `name`='更新' where id=" + id;    jdbcTemplate.execute(sql);    returntrue;}public void query(String tag, int id) {    String sql = "select * from money where id=" + id;    Map map = jdbcTemplate.queryForMap(sql);    System.out.println(tag + " >>>> " + map);}private boolean updateMoney(int id) {    String sql = "update money set `money`= `money` + 10 where id=" + id;    jdbcTemplate.execute(sql);    returnfalse;}

2. RU 隔离级别

我们先来测试 RU 隔离级别,通过指定@Transactional注解的isolation属性来设置事务的隔离级别

通过前面的描述,我们知道 RU 会有脏读问题,接下来设计一个 case,进行演示

事务一,修改数据

/** * ru隔离级别的事务,可能出现脏读,不可避免不可重复读,幻读 * * @param id */@Transactional(isolation = Isolation.READ_UNCOMMITTED, rollbackFor = Exception.class)public boolean ruTransaction(int id) throws InterruptedException {    if (this.updateName(id)) {        this.query("ru: after updateMoney name", id);        Thread.sleep(2000);        if (this.updateMoney(id)) {            returntrue;        }    }    this.query("ru: after updateMoney money", id);    returnfalse;}

只读事务二(设置 readOnly 为 true,则事务为只读)多次读取相同的数据,我们希望在事务二的第一次读取中,能获取到事务一的中间修改结果(所以请注意两个方法中的 sleep 使用)

@Transactional(readOnly = true, isolation = Isolation.READ_UNCOMMITTED, rollbackFor = Exception.class)public boolean readRuTransaction(int id) throws InterruptedException {    this.query("ru read only", id);    Thread.sleep(1000);    this.query("ru read only", id);    returntrue;}

接下来属于测试的 case,用两个线程来调用只读事务,和读写事务

@Componentpublicclass DetailTransactionalSample {    @Autowired    private DetailDemo detailDemo;     /**     * ru 隔离级别     */    public void testRuIsolation() throws InterruptedException {        int id = 330;        new Thread(new Runnable() {            @Override            public void run() {                call("ru: 只读事务 - read", id, detailDemo::readRuTransaction);            }        }).start();        call("ru 读写事务", id, detailDemo::ruTransaction);    }}private void call(String tag, int id, CallFunc func) {    System.out.println("============ " + tag + " start ========== ");    try {        func.apply(id);    } catch (Exception e) {    }    System.out.println("============ " + tag + " end ========== ");}@FunctionalInterfacepublicinterface CallFunc {    R apply(T t) throws Exception;}

输出结果如下

============ ru 读写事务 start ====================== ru: 只读事务 - read start ==========ru read only >>>> {id=330, name=初始化, money=200, is_deleted=false, create_at=2020-01-20 11:37:51.0, update_at=2020-01-20 11:37:51.0}ru: after updateMoney name >>>> {id=330, name=更新, money=200, is_deleted=false, create_at=2020-01-20 11:37:51.0, update_at=2020-01-20 11:37:52.0}ru read only >>>> {id=330, name=更新, money=200, is_deleted=false, create_at=2020-01-20 11:37:51.0, update_at=2020-01-20 11:37:52.0}============ ru: 只读事务 - read end ==========ru: after updateMoney money >>>> {id=330, name=更新, money=210, is_deleted=false, create_at=2020-01-20 11:37:51.0, update_at=2020-01-20 11:37:54.0}============ ru 读写事务 end ==========

关注一下上面结果中ru read only >>>>开头的记录,首先两次输出结果不一致,所以不可重复读问题是存在的

其次,第二次读取的数据与读写事务中的中间结果一致,即读取到了未提交的结果,即为脏读

3. RC 事务隔离级别

rc 隔离级别,可以解决脏读,但是不可重复读问题无法避免,所以我们需要设计一个 case,看一下是否可以读取另外一个事务提交后的结果

在前面的测试 case 上,稍微改一改

// ---------- rc 事物隔离级别// 测试不可重复读,一个事务内,两次读取的结果不一样@Transactional(readOnly = true, isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class)public boolean readRcTransaction(int id) throws InterruptedException {    this.query("rc read only", id);    Thread.sleep(1000);    this.query("rc read only", id);    Thread.sleep(3000);    this.query("rc read only", id);    returntrue;}/** * rc隔离级别事务,未提交的写事务,会挂起其他的读写事务;可避免脏读,更新丢失;但不能防止不可重复读、幻读 * * @param id * @return */@Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class)public boolean rcTranaction(int id) throws InterruptedException {    if (this.updateName(id)) {        this.query("rc: after updateMoney name", id);        Thread.sleep(2000);        if (this.updateMoney(id)) {            returntrue;        }    }    returnfalse;}

测试用例

/** * rc 隔离级别 */private void testRcIsolation() throws InterruptedException {    int id = 340;    new Thread(new Runnable() {        @Override        public void run() {            call("rc: 只读事务 - read", id, detailDemo::readRcTransaction);        }    }).start();    Thread.sleep(1000);    call("rc 读写事务 - read", id, detailDemo::rcTranaction);}

输出结果如下

============ rc: 只读事务 - read start ==========rc read only >>>> {id=340, name=初始化, money=200, is_deleted=false, create_at=2020-01-20 11:46:17.0, update_at=2020-01-20 11:46:17.0}============ rc 读写事务 - read start ==========rc: after updateMoney name >>>> {id=340, name=更新, money=200, is_deleted=false, create_at=2020-01-20 11:46:17.0, update_at=2020-01-20 11:46:23.0}rc read only >>>> {id=340, name=初始化, money=200, is_deleted=false, create_at=2020-01-20 11:46:17.0, update_at=2020-01-20 11:46:17.0}============ rc 读写事务 - read end ==========rc read only >>>> {id=340, name=更新, money=210, is_deleted=false, create_at=2020-01-20 11:46:17.0, update_at=2020-01-20 11:46:25.0}============ rc: 只读事务 - read end ==========

从上面的输出中,在只读事务,前面两次查询,结果一致,虽然第二次查询时,读写事务修改了这个记录,但是并没有读取到这个中间记录状态,所以这里没有脏读问题;

当读写事务完毕之后,只读事务的第三次查询中,返回的是读写事务提交之后的结果,导致了不可重复读

4. RR 事务隔离级别

针对 rr,我们主要测试一下不可重复读的解决情况,设计 case 相对简单

/** * 只读事务,主要目的是为了隔离其他事务的修改,对本次操作的影响; * * 比如在某些耗时的涉及多次表的读取操作中,为了保证数据一致性,这个就有用了;开启只读事务之后,不支持修改数据 */@Transactional(readOnly = true, isolation = Isolation.REPEATABLE_READ, rollbackFor = Exception.class)public boolean readRrTransaction(int id) throws InterruptedException {    this.query("rr read only", id);    Thread.sleep(3000);    this.query("rr read only", id);    returntrue;}/** * rr隔离级别事务,读事务禁止其他的写事务,未提交写事务,会挂起其他读写事务;可避免脏读,不可重复读,(我个人认为,innodb引擎可通过mvvc+gap锁避免幻读) * * @param id * @return */@Transactional(isolation = Isolation.REPEATABLE_READ, rollbackFor = Exception.class)public boolean rrTransaction(int id) {    if (this.updateName(id)) {        this.query("rr: after updateMoney name", id);        if (this.updateMoney(id)) {            returntrue;        }    }    returnfalse;}

我们希望读写事务的执行周期在只读事务的两次查询之内,所有测试代码如下

/** * rr * 测试只读事务 */private void testReadOnlyCase() throws InterruptedException {    // 子线程开启只读事务,主线程执行修改    int id = 320;    new Thread(new Runnable() {        @Override        public void run() {            call("rr 只读事务 - read", id, detailDemo::readRrTransaction);        }    }).start();    Thread.sleep(1000);    call("rr 读写事务", id, detailDemo::rrTransaction);}

输出结果

============ rr 只读事务 - read start ==========rr read only >>>> {id=320, name=初始化, money=200, is_deleted=false, create_at=2020-01-20 11:46:17.0, update_at=2020-01-20 11:46:17.0}============ rr 读写事务 start ==========rr: after updateMoney name >>>> {id=320, name=更新, money=200, is_deleted=false, create_at=2020-01-20 11:46:17.0, update_at=2020-01-20 11:46:28.0}============ rr 读写事务 end ==========rr read only >>>> {id=320, name=初始化, money=200, is_deleted=false, create_at=2020-01-20 11:46:17.0, update_at=2020-01-20 11:46:17.0}============ rr 只读事务 - read end ==========

两次只读事务的输出一致,并没有出现上面的不可重复读问题

说明

  • @Transactional注解的默认隔离级别为Isolation#DEFAULT,也就是采用数据源的隔离级别,mysql innodb 引擎默认隔离级别为 RR(所有不额外指定时,相当于 RR)

5. SERIALIZABLE 事务隔离级别

串行事务隔离级别,所有的事务串行执行,实际的业务场景中,我没用过... 也不太能想像,什么场景下需要这种

@Transactional(readOnly = true, isolation = Isolation.SERIALIZABLE, rollbackFor = Exception.class)public boolean readSerializeTransaction(int id) throws InterruptedException {    this.query("serialize read only", id);    Thread.sleep(3000);    this.query("serialize read only", id);    returntrue;}/** * serialize,事务串行执行,fix所有问题,但是性能低 * * @param id * @return */@Transactional(isolation = Isolation.SERIALIZABLE, rollbackFor = Exception.class)public boolean serializeTransaction(int id) {    if (this.updateName(id)) {        this.query("serialize: after updateMoney name", id);        if (this.updateMoney(id)) {            returntrue;        }    }    returnfalse;}

测试 case

/** * Serialize 隔离级别 */private void testSerializeIsolation() throws InterruptedException {    int id = 350;    new Thread(new Runnable() {        @Override        public void run() {            call("Serialize: 只读事务 - read", id, detailDemo::readSerializeTransaction);        }    }).start();    Thread.sleep(1000);    call("Serialize 读写事务 - read", id, detailDemo::serializeTransaction);}

输出结果如下

============ Serialize: 只读事务 - read start ==========serialize read only >>>> {id=350, name=初始化, money=200, is_deleted=false, create_at=2020-01-20 12:10:23.0, update_at=2020-01-20 12:10:23.0}============ Serialize 读写事务 - read start ==========serialize read only >>>> {id=350, name=初始化, money=200, is_deleted=false, create_at=2020-01-20 12:10:23.0, update_at=2020-01-20 12:10:23.0}============ Serialize: 只读事务 - read end ==========serialize: after updateMoney name >>>> {id=350, name=更新, money=200, is_deleted=false, create_at=2020-01-20 12:10:23.0, update_at=2020-01-20 12:10:39.0}============ Serialize 读写事务 - read end ==========

只读事务的查询输出之后,才输出读写事务的日志,简单来讲就是读写事务中的操作被 delay 了

6. 小结

本文主要介绍了事务的几种隔离级别,已经不同干的隔离级别对应的场景,可能出现的问题;

隔离级别说明

级别fixnot fixRU更新丢失脏读,不可重复读,幻读RC更新丢失 脏读不可重复读,幻读RR更新丢、脏读,不可重复读,幻读-serialze更新丢失、 脏读,不可重复读,幻读-

使用说明

  • mysql innodb 引擎默认为 RR 隔离级别;@Transactinoal注解使用数据库的隔离级别,即 RR
  • 通过指定Transactional#isolation来设置事务的事务级别

IV. 其他

0. 系列博文&源码

源码

  • 工程:https://github.com/liuyueyi/spring-boot-demo[8]
  • 实例源码: https://github.com/liuyueyi/spring-boot-demo/blob/master/spring-boot/101-jdbctemplate-transaction[9]

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

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

相关文章

东风小康为什么是dfsk_助力地摊经济瑞驰纯电动物流车和东风小康微型货车厚积薄发...

核心提示:小康集团旗下瑞驰纯电动物流车及东风小康微型货车深耕多年,其中瑞驰纯电动物流车2015年上市,连续多年销量位列中国行业第一。近日,地摊经济成为热门,疫情常态化下,地摊经济、小店经济对于快速恢复…

链表中删除选定结点的优雅操作!

一般我们在进行单向链表链表的结点删除操作时,都是通过相应的结构体指针进行链表的遍历,然后找 到需要删除的节点,为了完成删除操作,我们需要在寻找该节点时,不断地记录下这个节点前面的节点 (prev),来保证当特定结点被删除后,我们还可以将断开的链表重新连起来,下面给出一段…

python抢货程序_Python自动化xpath实现自动抢票抢货代码示例

本篇文章小编给大家分享一下Python自动化xpath实现自动抢票抢货代码示例,文章代码介绍的很详细,小编觉得挺不错的,现在分享给大家供大家参考,有需要的小伙伴们可以来看看。 总代码: for i in range(51,56): driver.imp…

Ubuntu安装pycharm并且激活

下载pycharm: https://www.jetbrains.com/pycharm/download/#sectionlinux 选择专业版下载,然后提取,也就是解压 进入解压后目录,再进入bin目录,打开终端执行命令 ./pycharm.sh pycharm就启动了 激活Pycharm: 编辑hosts文件&am…

二阶矩阵转置怎么求_矩阵求导术(下)

点击上方“Datawhale”,选择“星标”公众号第一时间获取价值内容本文承接上篇 https://zhuanlan.zhihu.com/p/24709748,来讲矩阵对矩阵的求导术。使用小写字母x表示标量,粗体小写字母表示列向量,大写字母X表示矩阵。矩阵对矩阵的求…

洛谷-DFS-1019-单词接龙-个人AC题解和公共AC题解笔记

学习内容: 预处理万能头文件string的使用 话不多说,直奔主题 本人AC代码 #include<iostream> #include<cstdio> #include<cstring> using namespace std; #define MAXN 21 #define MAXLENGTH 21 int n; int length,max_length; char words[MAXN][MAXLEN…

c语言中x的n次方怎么表示_线性代数的本质及其在人工智能中的应用

线性代数是 AI 专家必须掌握的知识&#xff0c;这已不再是个秘密。如果不掌握应用数学这个领域&#xff0c;你永远就只能是「门外汉」。当然&#xff0c;学习线性代数道阻且长。数学&#xff0c;尤其是线性代数常与枯燥、复杂和毫无意义的事物联系起来。不过你还可以另辟蹊径。…

js之箭头函数

原文 ES6标准新增了一种新的函数&#xff1a;Arrow Function&#xff08;箭头函数&#xff09;。 为什么叫Arrow Function&#xff1f;因为它的定义用的就是一个箭头&#xff1a; x > x * x 上面的箭头函数相当于&#xff1a; function (x) {return x * x; } 箭头函数相当于…

android开发蓝牙是否可见开关_如何从后台开启android蓝牙的可见性以及始终保持可见性...

最近工作中遇到一个特殊的需求&#xff0c;要求代码能够从后台开机android手机蓝牙的可见性。而framework提供了一种打开可见性的操作&#xff0c;就是通过向用户弹出一个提示框&#xff0c;来询问是否允许开启可见性。而且限制了最长时间为300秒&#xff0c;代码如下&#xff…

洛谷-DFS-1101-单词方阵-个人AC题解及公共题解的笔记

先上自己AC代码(博主这个代码修改过多次,只因代码长度过长) #include<bits/stdc.h> using namespace std; #define MAXN 102 int flag; int n; int next1[8][2]{{1,0},{0,1},{-1,0},{0,-1},{1,1},{1,-1},{-1,1},{-1,-1}}; char map1[MAXN][MAXN],map2[MAXN][MAXN]; cons…

django 实现电子支付功能

思路&#xff1a;调用第三方支付 API 接口实现支付功能。本来想用支付宝来实现第三方网站的支付功能的&#xff0c;但是在实际操作中发现支付宝没有 Python 接口&#xff0c;网上虽然有他人二次封装的的 Python 接口&#xff0c;但是对我这个小白白来说上手还是有点难度&#x…

android中怎么保存checkbox中的checked属性_第二十四天HTML中的form表单

form表单用于收集用户信息&#xff0c;如&#xff1a;登录、注册等场景&#xff1b;所有要提交的数据都必须放在form标签中action&#xff1a;提交地址、动作&#xff0c;与input标签中typy标签的submit属性相关联。 &#xff0c;提交地址是action的地址method:提交方法&#x…

Python中曲率与弯曲的转换_1000R曲率更具沉浸感!三星T55曲面显示器评测

在曲面屏的设计上&#xff0c;三星一直在突破极限&#xff0c;比如在2017年推出的49英寸超宽带鱼屏C49HG90&#xff0c;引来众人围观&#xff0c;非常震撼。而在曲率方面&#xff0c;我们常见的有1800R和1500R&#xff0c;但是三星并不满足&#xff0c;于日前推出了一款曲率达到…

树的遍历-Preorde Traversal,Inorder Traversal,Postoder Traversal

上代码(创建树,先序,中序,后序) #include<bits/stdc.h> using namespace std; typedef struct TreeNode *BinTree; struct TreeNode {BinTree left,right;int value; }; BinTree CreateTree() {BinTree T;int value;cin>>value;if(value0) //如果输入0,则创…

opencv resize_opencv-python库基础操作(一)

点赞再看&#xff0c;养成习惯&#xff01;点赞再看&#xff0c;养成习惯&#xff01;点赞再看&#xff0c;养成习惯&#xff01;opencv-python库基础操作0.安装opencv-pythonpip install opencv-python进行下载并安装不过在python中导入opencv库的时候需要"import cv2&qu…

Page object设计模式

网上看了很多文章&#xff0c;充斥了大量代码。理解起来不容易&#xff0c;在此就我的理解谈谈PageObject设计模式到底是什么东西。 所谓的Page object模式&#xff0c;主要是编写不同层级的脚本&#xff0c;然后一层一层的继承来完成对web测试过程进行分解。 首先&#xff1a;…

百旺智能编码_【百旺】票字版开票软件操作指南已为您备好,请查阅!

为确保小规模纳税人继续享受税收优惠政策&#xff0c;请广大用户及时对开票软件进行升级~按照国家税务总局要求&#xff0c;从增值税发票税控开票软件五月补丁开始&#xff0c;将停止对增值税发票税控开票软件(税控盘版)“税”字版(以下简称“税”字版)的软件升级更新服务&…

洛谷-图的遍历-P2661-信息传递

#include <iostream> #include <cstdio> using namespace std; const int N 200010; int n, fa[N], ans 0x3f3f3f3f; int get (int x, int &cnt) { //cnt记录环的长度 cnt ;if (fa[x] x) return x;else return get(fa[x], cnt); } int main () {scanf("…

mysql binlog 备份_做好mysql运维,必须熟练掌握备份和恢复,实战一次不行多来几次...

原文&#xff1a;https://www.toutiao.com/i6855460778359816715/平台&#xff1a;头条作者&#xff1a;程序员不就是0和1一、 备份恢复策略进行备份或恢复操作时需要考虑一些因素&#xff1a;1、确定要备份的表的存储引擎是事务型还是非事务型&#xff0c;两种不同的存储引擎备…

在过程中要正式批准可交付成果_干货!软考高项项目管理知识体系5大过程组47个过程...

现在应该很多小伙伴都在紧张的复习软考中&#xff0c;为了让大家更加高效的复习&#xff0c;今天给大家分享软考高级信息系统项目管理师的考试重点&#xff0c;项目管理知识体系的5大过程组47个过程。考高项的朋友都知道&#xff0c;47个过程是非常重要的&#xff0c;必须要理解…