seata 如何开启tcc事物_分布式事务Seata-TCC源码分析

为了更好理解分布式事务,首先提出一个问题:

假设数据库中有两个表ta,tb,我们要分别更改ta表中的ra记录和tb表中的rb记录,但要求ra和rb记录都修改成功,才认为此次操作时成功,或者需要失败回滚。针对这种情况处理方式很简单,只需要使用个事务就好了。

但假如ta和tb不在一个数据库中或者不在一个数据库实例上,此时数据库的事务这两个表也是无法同时管理的,针对这种情况要如何解决了?如何保证对ta和tb操作的一致性?

此时可以通过TCC来解决上述问题,TCC通过实现两阶段协议,将服务流程抽象为Try-Confirm-Cancel 三个操作:

第一阶段:try,主要用于对资源的预留

第二阶段:comfirm/cancel,comfirm用于对预留资源的使用,对业务进行提交,cancel是对预留资源的释放,对业务进行回滚操作

下面从三个方面介绍TCC

seata中TCC的源码实现

写好TCC实现的注意点

seata中TCC模式如何做到高可用的

1. seata中TCC的实现

seata主要由三个模块组成

TC (Transaction Coordinator) - 事务协调者维护全局和分支事务的状态,驱动全局事务提交或回滚。

TM (Transaction Manager) - 事务管理器定义全局事务的范围:开始全局事务、提交或回滚全局事务。

RM (Resource Manager) - 资源管理器管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

public interface TccActionOne {

@TwoPhaseBusinessAction(name = "DubboTccActionOne" , commitMethod = "commit", rollbackMethod = "rollback")

public boolean prepare(BusinessActionContext actionContext, int a);

public boolean commit(BusinessActionContext actionContext);

public boolean rollback(BusinessActionContext actionContext);

}

public interface TccActionTwo {

@TwoPhaseBusinessAction(name = "DubboTccActionTwo" , commitMethod = "commit", rollbackMethod = "rollback")

public boolean prepare(BusinessActionContext actionContext, String b);

public boolean commit(BusinessActionContext actionContext);

public boolean rollback(BusinessActionContext actionContext);

}

同时在provider和consumer端都要引入具体的GlobalTransactionScanner,该类会对TM和RM进行初始化和注册:

具体调用如图:

8bad530a15f7

image.jpeg

以上是简单的使用方式,有了上面基本使用流程的介绍后,现在开始分析下具体的代码实现,首先重点关注下以下的类和注解:

GlobalTransactionScanner,用于扫描是否开启了分布式事务,并对加了分布式事务注解的方法注入代理,如TwoPhaseBusinessAction和GlobalTransactional

注解TwoPhaseBusinessAction,表示该方法使用的TCC模式,并同时制定commit和cancel方法

注解GlobalTransactional,用于表示被修饰的方法会开启分布式事务来进行处理

GlobalTransactionScanner通过AbstractAutoProxyCreator类,来为被分布式相关注解修饰的方法添加动态代理,所以在服务启动时,会执行GlobalTransactionScanner类中相关方法,

主要涉及的方法有:

GlobalTransactionScanner#initClient,初始化TM和RM客户端

GlobalTransactionScanner#wrapIfNecessary,为添加了TwoPhaseBusinessAction和GlobalTransactional注解的方法添加代理,同时分别为修饰的方法注入TccActionInterceptor和GlobalTransactionalInterceptor代理类,同时会将本地服务作为RM客户端注册到TC服务端中

1.1 客户端初始化

所以GlobalTransactionScanner是Seata客户端的启动类,首先看下TM和RM客户端的初始化

TM和RM会分别初始化TmNettyRemotingClient和RmNettyRemotingClient,这个两个类的父类都是AbstractNettyRemotingClient,在该类的init方法中,会启动一个定时来检查TC的channel是否存活,同时会发送注册信息到TC中,最后会启动netty客户端

public void init() {

timerExecutor.scheduleAtFixedRate(new Runnable() {

@Override

public void run() {

//检测连接TC的channel是否存活,若不存在对应channel或者channel已关闭,则会重新连接到TC,同时发送注册信息到TC服务中

clientChannelManager.reconnect(getTransactionServiceGroup());

}

}, SCHEDULE_DELAY_MILLS, SCHEDULE_INTERVAL_MILLS, TimeUnit.MILLISECONDS);

if (NettyClientConfig.isEnableClientBatchSendRequest()) {

mergeSendExecutorService = new ThreadPoolExecutor(MAX_MERGE_SEND_THREAD,

MAX_MERGE_SEND_THREAD,

KEEP_ALIVE_TIME, TimeUnit.MILLISECONDS,

new LinkedBlockingQueue<>(),

new NamedThreadFactory(getThreadPrefix(), MAX_MERGE_SEND_THREAD));

mergeSendExecutorService.submit(new MergedSendRunnable());

}

super.init();

clientBootstrap.start();

}

在NettyClientChannelManager#reconnect方法中,会通过获取所有注册到注册中心的TC服务地址,然后判断当前缓存NettyClientChannelManager#channels中是否存在对应地址且存活状态的channel,若不存在,则会为该TC地址创建channel,同时向改地址发送注册信息,TmNettyRemotingClient和RmNettyRemotingClient注册信息分别为RegisterTMRequest和RegisterRMRequest,主要方法步骤是在netty.NettyClientChannelManager#doConnect中创建channel,然后在NettyPoolableFactory#makeObject方法中发送对应的注册消息

而在GlobalTransactionScanner#wrapIfNecessary方法中,会为TwoPhaseBusinessAction和GlobalTransactional修饰的方法添加代理实现

@Override

protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {

try {

synchronized (PROXYED_SET) {

if (PROXYED_SET.contains(beanName)) {

return bean;

}

interceptor = null;

//check TCC proxy

if (TCCBeanParserUtils.isTccAutoProxy(bean, beanName, applicationContext)) {

//TCC interceptor, proxy bean of sofa:reference/dubbo:reference, and LocalTCC

interceptor = new TccActionInterceptor(TCCBeanParserUtils.getRemotingDesc(beanName));

ConfigurationCache.addConfigListener(ConfigurationKeys.DISABLE_GLOBAL_TRANSACTION,

(ConfigurationChangeListener)interceptor);

} else {

Class> serviceInterface = SpringProxyUtils.findTargetClass(bean);

Class>[] interfacesIfJdk = SpringProxyUtils.findInterfaces(bean);

if (!existsAnnotation(new Class[]{serviceInterface})

&& !existsAnnotation(interfacesIfJdk)) {

return bean;

}

if (interceptor == null) {

if (globalTransactionalInterceptor == null) {

globalTransactionalInterceptor = new GlobalTransactionalInterceptor(failureHandlerHook);

ConfigurationCache.addConfigListener(

ConfigurationKeys.DISABLE_GLOBAL_TRANSACTION,

(ConfigurationChangeListener)globalTransactionalInterceptor);

}

interceptor = globalTransactionalInterceptor;

}

}

LOGGER.info("Bean[{}] with name [{}] would use interceptor [{}]", bean.getClass().getName(), beanName, interceptor.getClass().getName());

if (!AopUtils.isAopProxy(bean)) {

bean = super.wrapIfNecessary(bean, beanName, cacheKey);

} else {

AdvisedSupport advised = SpringProxyUtils.getAdvisedSupport(bean);

Advisor[] advisor = buildAdvisors(beanName, getAdvicesAndAdvisorsForBean(null, null, null));

for (Advisor avr : advisor) {

advised.addAdvisor(0, avr);

}

}

PROXYED_SET.add(beanName);

return bean;

}

} catch (Exception exx) {

throw new RuntimeException(exx);

}

}

方法中会为TwoPhaseBusinessAction注解修饰的方法生成TccActionInterceptor代理,为GlobalTransactional生成GlobalTransactionalInterceptor代理。

但在TCCBeanParserUtils#isTccAutoProxy方法中若存在TwoPhaseBusinessAction注解,会通过RmNettyRemotingClient#registerResource发送注册信息,具体方法在DefaultRemotingParser#parserRemotingServiceInfo中,个人觉得这个步骤可以去掉了有点冗余

1.2 服务端初始化

TC服务端启动类io.seata.server.Server#main,该方法中会初始化DefaultCoordinator类,这个类是所有消息的处理类,DefaultCoordinator主要属性如下

//各种定时任务,用来重试

private ScheduledThreadPoolExecutor retryRollbacking = new ScheduledThreadPoolExecutor(1,new NamedThreadFactory("RetryRollbacking", 1));

private ScheduledThreadPoolExecutor retryCommitting = new ScheduledThreadPoolExecutor(1,new NamedThreadFactory("RetryCommitting", 1));

private ScheduledThreadPoolExecutor asyncCommitting = new ScheduledThreadPoolExecutor(1,new NamedThreadFactory("AsyncCommitting", 1));

private ScheduledThreadPoolExecutor timeoutCheck = new ScheduledThreadPoolExecutor(1,new NamedThreadFactory("TxTimeoutCheck", 1));

private ScheduledThreadPoolExecutor undoLogDelete = new ScheduledThreadPoolExecutor(1,new NamedThreadFactory("UndoLogDelete", 1));

private RemotingServer remotingServer; //消息通信服务端

private DefaultCore core; //主要的事务处理

针对消息的处理流程,具体方法在NettyRemotingServer#registerProcessor:

private void registerProcessor() {

// 1. registry on request message processor

ServerOnRequestProcessor onRequestProcessor =

new ServerOnRequestProcessor(this, getHandler());

//处理事务提交回滚等消息

super.registerProcessor(MessageType.TYPE_BRANCH_REGISTER, onRequestProcessor, messageExecutor);

super.registerProcessor(MessageType.TYPE_BRANCH_STATUS_REPORT, onRequestProcessor, messageExecutor);

super.registerProcessor(MessageType.TYPE_GLOBAL_BEGIN, onRequestProcessor, messageExecutor);

super.registerProcessor(MessageType.TYPE_GLOBAL_COMMIT, onRequestProcessor, messageExecutor);

super.registerProcessor(MessageType.TYPE_GLOBAL_LOCK_QUERY, onRequestProcessor, messageExecutor);

super.registerProcessor(MessageType.TYPE_GLOBAL_REPORT, onRequestProcessor, messageExecutor);

super.registerProcessor(MessageType.TYPE_GLOBAL_ROLLBACK, onRequestProcessor, messageExecutor);

super.registerProcessor(MessageType.TYPE_GLOBAL_STATUS, onRequestProcessor, messageExecutor);

super.registerProcessor(MessageType.TYPE_SEATA_MERGE, onRequestProcessor, messageExecutor);

// 2. registry on response message processor,对分支事务提交和回滚响应结果的处理

ServerOnResponseProcessor onResponseProcessor =

new ServerOnResponseProcessor(getHandler(), getFutures());

super.registerProcessor(MessageType.TYPE_BRANCH_COMMIT_RESULT, onResponseProcessor, messageExecutor);

super.registerProcessor(MessageType.TYPE_BRANCH_ROLLBACK_RESULT, onResponseProcessor, messageExecutor);

// 3. registry rm message processor,处理RM客户端的注册消息

RegRmProcessor regRmProcessor = new RegRmProcessor(this);

super.registerProcessor(MessageType.TYPE_REG_RM, regRmProcessor, messageExecutor);

// 4. registry tm message processor,处理TM客户端的注册消息

RegTmProcessor regTmProcessor = new RegTmProcessor(this);

super.registerProcessor(MessageType.TYPE_REG_CLT, regTmProcessor, null);

// 5. registry heartbeat message processor,处理客户端的心跳消息

ServerHeartbeatProcessor heartbeatMessageProcessor = new ServerHeartbeatProcessor(this);

super.registerProcessor(MessageType.TYPE_HEARTBEAT_MSG, heartbeatMessageProcessor, null);

}

从上图可以看出注册消息是在RegRmProcessor和RegTmProcessor中进行处理。

1.3 TCC消息处理分析

首先还是看下一开始的示例代码:

@GlobalTransactional

public String doTransactionCommit(){

//第一个TCC 事务参与者

boolean result = tccActionOne.prepare(null, 1);

if(!result){

throw new RuntimeException("TccActionOne failed.");

}

List list = new ArrayList();

list.add("c1");

list.add("c2");

result = tccActionTwo.prepare(null, "two", list);

if(!result){

throw new RuntimeException("TccActionTwo failed.");

}

return RootContext.getXID();

}

在调用doTransactionCommit方法时,会进入到代理类GlobalTransactionalInterceptor中,最终会执行到TransactionalTemplate#execute方法,该方法的主要逻辑如下:

beginTransaction(txInfo, tx);//开始事务

rs = business.execute(); //执行业务代码,即执行doTransactionCommit方法

commitTransaction(tx); //提交事务

在开始事务beginTransaction方法中,会向TC服务发送GlobalBeginRequest消息,来获取事务xid,该消息最终会在服务端DefaultCore#begin方法中得到处理:

通过雪花算法产生一个随机数作为transactionId.根据transactionId生成xid,具体规则是ipAddress + ":" + port + ,":" + transactionId,ipAddress为本机ip,port为当前服务的端口

将全局事务记录写入global_table表中,同时返回xid,表中xid为主键,transactionId为索引

执行业务代码,业务代码会调用远端服务,如tccActionOne.prepare方法,由于该方法被TwoPhaseBusinessAction修饰,会执行代理类TccActionInterceptor,在TccActionInterceptor类中的invoke方法主要逻辑如下:

String branchId = doTccActionLogStore(method, arguments, businessAction, actionContext); //注册分支事务,同时获取分支事务id

.....

ret.put(Constants.TCC_METHOD_RESULT, targetCallback.execute()); #执行实际的分支事务的业务代码,如tccActionOne.prepare方法

其中在doTccActionLogStore方法中客户端通过发送BranchRegisterRequest消息,其中clientId和lockKeys都是null,resourceId为TwoPhaseBusinessAction中name名称该消息最终会在服务端AbstractCore#branchRegister:

通过随机算法生成branchId

生成一个分支记录,将记录插入到branch_table表中,其中branchId为主键

doTransactionCommit所有业务逻辑执行成功后,GlobalTransactionalInterceptor代理类会执行到commitTransaction方法,进行全局事务的提交,客户端会通过DefaultTransactionManager#commit方法发送GlobalCommitRequest事务提交消息,此时服务端接收到该消息后,会通过DefaultCore#doGlobalCommit方法进行全局事务的提交,该方法的主要逻辑如下:

从存储中获取全局事务xid下所有分支事务记录,为每个分支事务调用AbstractCore#branchCommitSend方法,发送BranchCommitRequest消息到对应的分支事务客户端,来进行分支事务的commit,客户端接收到消息后,会执行TwoPhaseBusinessAction注解中填写的commit方法来完成分支事务的提交

当有任何一个分支服务调用失败时,如tccActionOne.prepare调用失败,会回滚全局事务,然后TC服务端会回滚所有的分支事务

2. 写好TCC实现的注意点

写好一个完备的TCC的实现是有一定的要求,需要解决空回滚,幂等操作和悬挂问题。

空回滚

即全局事务回滚时,有可能分支事务try接口由于网络问题并没有被触发或者还在处于try阶段,此时TC已经触发了分支事务的cancel,此时需要分支事务服务需要返回成功,不然会有重试,即分支事务要支持空回滚

幂登性

由于网络抖动问题,分支事务中的try方法可能会被执行多次,所以要保证资源不会被重复消耗,解决办法可以通过为每一个请求维护一个唯一id,如分支事务id,来过滤重复的请求

悬挂问题

当全局事务回滚时,由于分支事务try方法执行了较长时间,导致分支事务执行cancel方法后,try方法才执行成功,这样导致被try锁定的资源得不到释放,解决办法是将每个分支事务的请求记录下来,所以当执行try方法后,发现已经存在cancel的执行记录后,则回滚当前的try操作

3. seata中TCC模式如何做到高可用的

要做到高可用,要做到服务的无状态,为了做到这点seata做了如下工作:

存储,TC中事务数据的存储避免使用本地存储,可以使用mysql等

服务发现与注册,

从上文实现分析中,我们可以看出TC服务会将本服务的ip注册到注册中心,如zk,etcd等,TM和RM客户端会拉取所有TC服务端的地址,同时将客户端服务的ip注册到所有TC服务中,这样保证了每个TC服务都有所有客户端的链接信息

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

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

相关文章

promise的三种状态_一.Promise中核心逻辑的实现

首先看一下Promise代码&#xff1a;let promise new Promise((resolve,reject)>{resolve(成功);//reject(失败); }) promise.then(val>{console.log(val); },reason>{console.log(reason); })我们根据以上的一个简单的用例&#xff0c;得到Promise类的最主要的核心逻辑…

mysql如何定位到数据_如何快速定位当前数据库消耗CPU最高的sql语句?

概述如果是Oracle数据库我们可以很容易通过sql来定位到当前数据库中哪些消耗CPU高的语句&#xff0c;而mysql数据库可以怎么定位呢&#xff1f;这里用一个简单例子说明下...主要是了解如何定位的思路&#xff0c;具体看官网介绍..参考&#xff1a;https://www.percona.com/blog…

当当elastic-job docker快速部署_[小Z课堂]-docker 快速部署 elasticsearch 和 kibana,一键部署...

各位小伙伴&#xff0c;小Z课堂来袭&#xff0c;每天只需看三分钟&#xff0c;你就能用docker 快速部署各种环境。今天就用docker 来部署 elasticsearch 和 kibana。docker的入门请上度娘学习&#xff0c;这里直接进入实战。拉镜像镜像版本&#xff1a;base image&#xff1a;U…

数字有维度, 质数可追寻

摘要 用数轴的点表示数, 实际是把数的几何意义单一 化, 把所有实数同等化. 在研究素数的问题上, 应该挖掘数的 更多几何意义, 就正自然数而言, 不同区间的数, 几何意义是 不相同的, 对应的点是不同空间的点, 具有多样化的. 寻找质 数, 就是设法把不同空间的 1 维数 (质数) 找出…

局域网聊天程序 java MySQL_课内资源 - 基于JAVA的局域网聊天软件的设计与实现(仿制QQ)...

一、系统分析1.1 问题描述客户端实现简易版的局域网聊天器实现富文本内容聊天智能聊天机器人群发消息传送文件等功能服务器端实现群发通知管理聊天线程1.2 系统功能分析客户端功能登陆注册发送表情消息发送文本消息截取图片图片处理震动效果发送文件群发消息设置聊天文本样式服…

北工大一拟录取女研究生在网络发不当言论,已被网友举报!

近日&#xff0c;北工大一拟录取女研究生在自己的社交平台发表不当的言论。随后&#xff0c;其言论引起网友的热议。>>>>对于网友的质疑&#xff0c;其通过微博发文称&#xff0c;要“开小号专门打拳”。当天夜里&#xff0c;有网友通过其微博的公开的考研信息&…

java divide 用法_java中BigDecimal加减乘除基本用法

Java在java.math包中提供的API类BigDecimal&#xff0c;用来对超过16位有效位的数进行精确的运算。双精度浮点型变量double可以处理16位有效数。在实际应用中&#xff0c;需要对更大或者更小的数进行运算和处理。float和double只能用来做科学计算或者是工程计算&#xff0c;在商…

如何反映两条曲线的拟合精度_你知道850加工中心定位精度的检测有哪些吗?

你知道850加工中心定位精度的检测有哪些吗&#xff1f;定位精度在机械制造上指零件或刀具等实际位置与标准位置&#xff08;理论位置/理想位置&#xff09;之间的差距&#xff0c;其差距越小&#xff0c;说明精度越高。定位精度是零件加工精度得以保证的前提。XFL-850加工中心的…

java ee最新_从此再无 JavaEE,现在叫 JakartaEE

各位小伙伴,你们都知道这个消息吗&#xff1f;Java EE 正式改名为 Jakarta EE 了。以后小伙伴们自我介绍的时候又多了一种方式。(〃▽〃) &#xff1a;你是做什么的&#xff1f;(&#xffe3;▽&#xffe3;)~* &#xff1a;你好我是做 JakartaEE&#xff01;Σ(っД;)っ&#…

十二月份找工作好找吗_小儿推拿师工作好找吗?工资高吗?

小儿推拿师工作好找吗&#xff1f;会不会学成之后找不到工作?作为现在比较热门的一个医学类职业&#xff0c;小儿推拿师还是比较容易找工作的&#xff0c;现在基本一条街上能有不低于十家小儿推拿店&#xff0c;而这些店里面小儿推拿师只有两三个&#xff0c;后面孩子还有很多…

java 单例 读写锁_终极锁实战:单JVM锁+分布式锁

目录1.前言2.单JVM锁3.分布式锁4.总结正文分割线1.前言锁就像一把钥匙&#xff0c;需要加锁的代码就像一个房间。出现互斥操作的典型场景&#xff1a;多人同时想进同一个房间争抢这个房间的钥匙(只有一把)&#xff0c;一人抢到钥匙&#xff0c;其他人都等待这个人出来归还钥匙&…

linux java socket编程_深入学习socket网络编程,以java语言为例

了解java的socket编程与Linux Socket API之间的关系一、java的网络编程1、socket原理socket通信就是通过IP和端口号将两台主机建立连接&#xff0c;提供通信。主机A的应用程序要能和服务器B进行通信&#xff0c;必须通过Socket建立连接&#xff0c;而建立Socket连接本质上就是依…

python去掉空白行_python去掉空白行的多种实现代码

这篇文章主要介绍了python去掉空白行实现代码,需要的朋友可以参考下 测试代码 php.txt1:www.php.cn 2:www.php.cn 3:www.php.cn 4:www.php.cn 5:www.php.cn 6:www.php.cn 7:www.php.cn 8:www.php.cn 9:www.php.cn 10:www.php.cn 11:www.php.cn 12:www.php.cn 13:www.php.cn 14:…

html get请求_99% 的人都理解错了 HTTP 中 GET 与 POST 的区别【面试必问】

先分析一波&#xff1a;1、GET和POST与数据如何传递没有关系&#xff1f;&#xff1f;GET和POST是由HTTP协议定义的。那么使用哪个方式与应用层的数据如何传输是没有相互关系的。从而&#xff0c;HTTP就没有要求&#xff0c;POST一定要放到请求体里面&#xff0c;GET就一定要放…

hive load data外部表报错_生产SparkSQL如何读写本地外部数据源及排错

https://spark-packages.org/里有很多third-party数据源的package&#xff0c;spark把包加载进来就可以使用了csv格式在spark2.0版本之后是内置的&#xff0c;2.0之前属于第三方数据源一、读取本地外部数据源1.直接读取一个json文件[hadoophadoop000 bin]$ ./spark-shell --mas…

ffmpeg命令_使用ffmpeg命令为多个短视频修改视频备注说明

今天主要给大家讲一下使用视频剪辑高手中的ffmpeg命令为多个短视频修改备注说明的详细步骤&#xff0c;有需要和感兴趣的宝贝们可以跟随小编一起来试试。收集视频将需要剪辑的短视频保存到同一文件夹上进入软件双击进入视频剪辑高手&#xff0c;选择“批量剪辑视频”功能添加视…

从事python需要掌握哪些知识和技能_零基础想转行从事Python?需要掌握如下技能...

零基础python能找到工作吗&#xff1f;需要掌握哪些技能&#xff1f;对于大部分零基础学编程半路出家的人来说&#xff0c;无非是想改变现状换一门新职业&#xff0c;所谓技术大牛不过是比小白们更早接触编程罢了&#xff0c;选择好自己有兴趣的职业技能&#xff0c;并为之学习…

java byte 判断相等_你真的了解Java中quot;==quot;和equals()的区别?

部分面试资料链接:https://pan.baidu.com/s/1qDb2YoCopCHoQXH15jiLhA密码:jsam想获得全部面试必看资料&#xff0c;关注公众号&#xff0c;大家可以在公众号后台回复“知乎”即可。“判断两个事物是否相等”&#xff0c;是编程中最常见的操作之一&#xff0c;在Java中&#xff…

数据通信原理_同网段主机通信原理

本篇文章介绍数据通信中最基础&#xff0c;最关键的原理之一&#xff0c;两台通网段的主机如何通信。获得更多技术资料和免费学习视频&#xff0c;加入讨论群&#xff1a;752160765适合两台普通电脑之间&#xff0c;两台服务器之间&#xff0c;两台手机之间&#xff0c;电脑和打…

java jdk 未知错误_解决JAVA JDK安装出错的最常见问题,帮你排除困扰

一般来说&#xff0c;安装JAVA JDK的整个流程是很简单的&#xff0c;只要按照提示进行操作即可&#xff0c;就不会出现问题。但是呢&#xff0c;有小伙伴反映说&#xff0c;之前安装了JAVA JDK&#xff0c;进行卸载重装的时候出现错误提示&#xff0c;“正在进行另一Java安装”…