【父子线程传值TransmittableThreadLocal使用踩坑-及相关知识拓展】

文章目录

    • 一.业务背景
    • 二.TransmittableThreadLocal是什么?
    • 三.问题复现
      • 1.定义注解@DigitalAngel
      • 2.定义切面
      • 3.TransmittableThreadLocal相关
      • 4.线程池配置信息
      • 5.Controller
      • 6.Service
      • 7.测试结果
      • 8.问题分析
      • 9 解决办法及代码改造
      • 10.最终测试:
    • 四.与 ThreadLocal 和 InheritableThreadLocal 的对比
    • 五.TransmittableThreadLocal典型应用场景
    • 六.小结

一.业务背景

  • 这版本我有一个需求是这样的:要针对项目里面总计归纳有6个接口,要去判断有没有天使用户角色:“DIGITAL_ANGEL”,来做特殊化查询逻辑。如果登录用户有这天使用户角色,那么看到的数据范围更大。否则就只能看到登录用户自己权限范围内的数据。不知道我描述的这业务逻辑是否清楚了0.0,小伙伴们能get到这意思吗…
  • 这6个接口是其他同事做的(已上线),此次需求交给我来做了,因为是已上线接口,我尽量做到业务侵入最小化,植入我此次需求最小代码。

好的,然后我的设计思路是这样来做的:

  1. 定义注解@DigitalAngel,放到6个请求接口上
  2. 定义切面切注解,然后切面里面统一判断当前登录用户是否有"DIGITAL_ANGEL"角色,有则把这个布尔值存入TransmittableThreadLocal(因为有些接口里面开了子线程,涉及到父子线程传值)
  3. 在接口业务层面,拿到TransmittableThreadLocal里面的布尔值,如果为true ?执行我们的天使用户查询逻辑 :执行历史逻辑

一顿操作猛如虎,开始开发环境测试,刚开始都喜笑颜开,小小需求,随便拿捏。 后面等我多测几次发现不对劲了,出现了诡异的事情,切面里面判断某个人有这角色,布尔值为true,然后子线程里面里面竟然出现了false!!!,然后这问题还是偶发,有个时候又频率高有个时候又没问题,我都一脸懵逼了,课余赶紧补充知识。。。

二.TransmittableThreadLocal是什么?

  • ‌TransmittableThreadLocal(TTL)是阿里巴巴提供的一个工具包中的类,主要用于解决线程池场景下的变量传递问题。‌
    它继承自InheritableThreadLocal,提供了一种机制,使得在线程池中的线程能够传递和继承ThreadLocal变量的值‌。
  • TTL与普通的ThreadLocal不同,普通的ThreadLocal变量在线程之间是隔离的,每个线程只能访问自己的ThreadLocal变量,无法在线程切换时传递变量值。而TTL允许在线程切换时保留原始线程的变量值,并在新线程中恢复这些值,使得新线程能够继续使用原始线程的变量‌。
  • TTL的核心工作原理基于Java的ThreadLocal机制,并通过扩展InheritableThreadLocal来实现。在任务提交到线程池之前,会将当前线程的ThreadLocal变量值保存到一个中间结构中。当任务在子线程中执行时,会从这个中间结构中恢复这些变量值并设置到子线程的ThreadLocal副本中,从而实现跨线程传递‌

三.问题复现

因为我们是内网开发,不方便给大家直观展示,我把实现思路这里简单写一遍,一样的可以复现。

1.定义注解@DigitalAngel

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DigitalAngel {
    String memo();
}

2.定义切面

@Aspect
@Component
@Slf4j
public class RoleCheckAspect {

    @Around("@annotation(com.tuling.tulingmall.annotation.DigitalAngel)")
    public Object checkRole(ProceedingJoinPoint joinPoint) throws Throwable {

        // 模拟角色检查逻辑,这里检查是否拥有 "DIGITAL_ANGEL" 角色
        boolean hasRole = Math.random() > 0.5;
        log.info("Aspect: 检查角色,是否拥有DIGITAL_ANGEL角色:{} " , hasRole);
        RoleContext.setHasRole(hasRole);
        try {
            return joinPoint.proceed();
        } finally {
            // 清理 ThreadLocal,避免内存泄漏
            RoleContext.clear();
        }
    }
}

3.TransmittableThreadLocal相关

public class RoleContext {

    private static final TransmittableThreadLocal<Boolean> hasDigitalAngelRole = new TransmittableThreadLocal<>();

    public static void setHasRole(Boolean hasRole) {
        hasDigitalAngelRole.set(hasRole);
    }

    public static Boolean getHasRole() {
        return hasDigitalAngelRole.get();
    }

    public static void clear() {
        hasDigitalAngelRole.remove();
    }
}

4.线程池配置信息

为了让问题更容易复现:

  1. 使用线程池中的线程复用问题:可以增加线程池的复用频率和负载,减少线程重新创建的机会。通过设置线程池的核心线程数较小,同时增加任务提交量,让线程池更频繁地复用同一个线程,这样在上下文未正确传播时更容易复现问题。
  2. 控制 TransmittableThreadLocal 的传递与清理:在一些关键位置手动调用 RoleContext.clear(),或者模拟在子线程中上下文值被修改或清理的场景。
  3. 延迟任务执行:可以通过引入一定的延迟或模拟复杂任务,使得线程的生命周期较长,以增加上下文丢失的几率
  4. 多个异步任务并行运行:提交大量异步任务给线程池,增加并发量,让上下文丢失更容易出现

我把核心线程数变小,同时提交10个异步任务,然后给任务引入一定的延迟来增大问题复现概率

@Configuration
public class ThreadPoolConfiguration {

    @Bean("commonPool")
    public ExecutorService commonThreadPoolExecutor(){
        return new TulingMallThreadPoolExecutor("测试用例专用线程池",2,5).getLhrmsThreadPoolExecutor();
    }
}

5.Controller

@ApiOperation("测试-TransmittableThreadLocal")
@DigitalAngel(memo = "标注该接口需要校验是否存在DIGITAL_ANGEL角色")
@RequestMapping(value = "/testTransmittableThreadLocal", method = RequestMethod.POST)
public CommonResult<String> testTransmittableThreadLocal() {
    testCaseService.testTransmittableThreadLocal();
    return CommonResult.success("成功");
}

6.Service

	@Autowired
    @Qualifier("commonPool")
    private ExecutorService tulingThreadPoolExecutor;
 
   @Override
    public void testTransmittableThreadLocal() {
        // 主线程获取角色信息
        Boolean hasRole = RoleContext.getHasRole();
        log.info("主线程: 获取角色信息,是否拥有 'DIGITAL_ANGEL' 角色: " + hasRole);

        // 提交多个任务到自定义线程池
        for (int i = 0; i < 10; i++) {
            CompletableFuture.runAsync(() -> {
                try {
                    // 增加任务的执行时间,模拟长时间运行的任务
                    TimeUnit.MILLISECONDS.sleep((long) (Math.random() * 1000));
                    // 在子线程中获取角色信息
                    Boolean childThreadHasRole = RoleContext.getHasRole();
                    log.info("子线程: 获取角色信息,是否拥有 'DIGITAL_ANGEL' 角色: " + childThreadHasRole);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, tulingThreadPoolExecutor);
        }
    }

7.测试结果

正常我们如果没有仔细了解过TransmittableThreadLocal,是不是认为都使用TransmittableThreadLocal了,在切面里面(主线程)都打印有这个DIGITAL_ANGEL角色编码了,那么子线程拿到的肯定是true对吧。但是上面代码经测试会出现下面的异常情况:切面和主线程判断是有这个角色编码的,可是我们的子线程获取的都是false,认为它没有天使用户角色!从而造成我们的业务逻辑查的有问题!!!
在这里插入图片描述

8.问题分析

然后去查资料,总结了精辟的点。遇到的问题与 线程池复用 和 TransmittableThreadLocal 的传播机制 有关。
TransmittableThreadLocal 可以在父子线程之间传递值,但需要满足一些前提条件,比如子线程是在父线程创建后立即启动的。如果业务逻辑涉及到线程池或者异步执行任务,可能会出现以下两种情况:

  1. 线程池线程复用问题
    在线程池中,线程是被复用的,因此当某个线程完成任务后会被放回线程池,而不会立即销毁。接下来的任务可能会复用这个线程。在这种情况下,如果你没有正确清理 TransmittableThreadLocal,线程复用后可能获取到之前任务的值,或者无法获取到期望的值

解决方案:手动清理 TransmittableThreadLocal 的值。确保每次在线程池中使用线程执行任务时,清理上一次任务残留的数据。

try {
    // 在子线程中执行任务,并获取 TTL 中的布尔值
} finally {
    // 任务执行结束,清理 TransmittableThreadLocal
    TransmittableThreadLocal.clear();
}
  1. 子线程启动时机问题
    TransmittableThreadLocal 的值只能在子线程创建时从父线程中拷贝。如果子线程是在父线程存储值之前启动的,那么子线程可能无法获取父线程的 ThreadLocal 值。如果有异步任务,尤其是使用 @Async 或其他异步编程模型的情况下,线程的启动顺序可能会影响值的传递。

解决方案:确保在启动子线程之前,父线程已经将值存入 TransmittableThreadLocal。可以通过控制异步任务的触发时机来避免这一问题。

// 在主线程中存入 TransmittableThreadLocal 的值
boolean roleExists = checkRole();
TransmittableThreadLocal.set(roleExists);

// 确保子线程在设置完值后启动
executorService.submit(() -> {
    // 子线程中获取值
    Boolean value = TransmittableThreadLocal.get();
    // 执行业务逻辑
});
  1. 异步框架或线程池管理问题
    如果你使用了异步框架(比如 Spring 的 @Async)或自定义的线程池管理逻辑,确保这些线程池或任务管理器支持 TransmittableThreadLocal 的上下文传播。默认的线程池可能不支持自动传播上下文数据,导致无法在子线程中获取到父线程的数据。

解决方案:确保线程池或任务管理框架使用了 TTL 的增强版线程池(TTLExecutorService 等),这可以保证 TransmittableThreadLocal 的值在线程池中正确传播。

// 使用 TTL 版本的线程池来保证父子线程之间的值传递
ExecutorService ttlExecutorService = TtlExecutors.getTtlExecutorService(executorService);

ttlExecutorService.submit(() -> {
    Boolean value = TransmittableThreadLocal.get();
    // 子线程中业务逻辑
});

很明显,我们上面的代码问题符合上面“异步框架或线程池管理问题”。我们的线程池没有使用TTL 的增强版线程池!!!

  1. 总结如下3点:
  • 线程池复用 导致的值残留或丢失。
  • 子线程启动时机 导致的值未传递。
  • 线程池的上下文传播 未正确设置。
    可以从这几方面排查和修正你的问题,确保 TransmittableThreadLocal 在父子线程之间正确传播值。

9 解决办法及代码改造

因为我们项目里面的线程池是放在了Spring容器中,然后有其他场景也在使用,为了影响最小化。直接在开异步任务的时候,CompletableFuture传入 TTL 的增强版线程池(TTLExecutorService )。

  @Autowired
  @Qualifier("commonPool")
  private ExecutorService tulingThreadPoolExecutor;
    
  @Override
    public void testTransmittableThreadLocal() {
        // 主线程获取角色信息
        Boolean hasRole = RoleContext.getHasRole();
        log.info("主线程: 获取角色信息,是否拥有 'DIGITAL_ANGEL' 角色: " + hasRole);

        // 提交多个任务到自定义线程池
        for (int i = 0; i < 10; i++) {
            CompletableFuture.runAsync(() -> {
                try {
                    // 增加任务的执行时间,模拟长时间运行的任务
                    TimeUnit.MILLISECONDS.sleep((long) (Math.random() * 1000));
                    // 在子线程中获取角色信息
                    Boolean childThreadHasRole = RoleContext.getHasRole();
                    log.info("子线程: 获取角色信息,是否拥有 'DIGITAL_ANGEL' 角色: " + childThreadHasRole);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, TtlExecutors.getTtlExecutorService(tulingThreadPoolExecutor));
        }
    }

10.最终测试:

请求了10次,每次结果都是主线程认为没有角色权限则子线程也没有角色权限,主线程认为有角色权限则子线程也有角色权限。
在这里插入图片描述

四.与 ThreadLocal 和 InheritableThreadLocal 的对比

在这里插入图片描述

  1. 传统 ThreadLocal 的局限性
    ThreadLocal 是 Java 提供的线程本地存储工具,用于每个线程存储和访问自己的独立变量副本。但是在多线程环境下,尤其是线程池场景中,ThreadLocal 有以下局限性:
  • 线程隔离:每个线程有自己独立的 ThreadLocal 值,子线程不能继承父线程中的 ThreadLocal 值。
  • 线程复用问题:在线程池中,线程会被复用,导致某些上下文信息(例如 ThreadLocal值)在不同任务之间泄漏,或未能正确传递到子线程中。
  1. InheritableThreadLocal 的不足
    InheritableThreadLocal 是 ThreadLocal 的一个子类,它允许子线程继承父线程的 ThreadLocal 值,但它有以下几个问题:
  • 线程池场景不适用:InheritableThreadLocal 只能在父线程创建子线程时传递 ThreadLocal值,对于线程池中的线程复用场景无效,因为线程池中的线程在父线程运行之前就已创建。
  • 线程复用导致的值污染:线程池中的线程在执行完一个任务后会被重复利用,如果 InheritableThreadLocal的值没有被清理干净,可能导致数据污染。
  1. TransmittableThreadLocal 的原理
    TransmittableThreadLocal 解决了 ThreadLocal 和 InheritableThreadLocal 在多线程和线程池环境中的局限性,能够在父线程和子线程(包括线程池中的线程)之间传递 ThreadLocal 的值。
    其核心思想是通过任务提交的时机,在任务进入线程池执行前,主动捕获当前线程的 ThreadLocal 值并在子线程中恢复,从而确保上下文的传递。
    核心技术原理:
    1.拦截任务提交:在任务提交给线程池时,TransmittableThreadLocal 会拦截任务并记录当前父线程中的 ThreadLocal 值。这是通过对 ExecutorService、Runnable、Callable 等任务接口的增强来实现的。
    2.线程上下文的传递:当任务在子线程中执行时,TransmittableThreadLocal 将会把父线程的 ThreadLocal 上下文传递到子线程,并在子线程执行任务时还原这些上下文。
    3.任务执行完成后的清理:任务执行结束后,TransmittableThreadLocal 会清理子线程中的上下文,避免这些上下文在线程复用时污染其他任务。

  2. 工作流程
    以下是 TransmittableThreadLocal 在多线程场景下的典型工作流程:
    1.父线程设置 ThreadLocal 值:在父线程中使用 TransmittableThreadLocal 设置一些上下文信息,例如用户信息、请求 ID 等。
    2.任务提交到线程池:父线程将任务提交给线程池执行,此时 TransmittableThreadLocal 会通过 capture() 方法捕获当前线程的 ThreadLocal 值。
    3.子线程执行任务:在线程池中的某个子线程执行任务之前,TransmittableThreadLocal 的 replay() 方法会在子线程中恢复父线程的 ThreadLocal 值。
    4.任务执行完成后清理上下文:任务执行结束后,TransmittableThreadLocal 的 restore() 方法会清理子线程中的 ThreadLocal 上下文,防止上下文污染。

五.TransmittableThreadLocal典型应用场景

TransmittableThreadLocal 特别适用于以下场景:

  • 分布式追踪:在分布式系统中,传递请求上下文信息(如 Trace ID)到不同线程,保证日志或追踪信息的一致性。
  • 异步任务处理:在异步任务执行中,需要传递用户会话信息或安全上下文到不同线程。 线程池环境下的上下文传递:解决线程池复用带来的ThreadLocal 上下文传递问题。

六.小结

  • TransmittableThreadLocal 是对 ThreadLocal 和 InheritableThreadLocal的增强,解决了线程池复用和父子线程上下文传递问题。它在异步编程和多线程环境中,尤其是线程池场景下,有很大的应用价值,适用于需要传递线程上下文信息的各种场景,如分布式追踪、会话管理、日志追踪等。
  • 使用异步编程的时候,我们肯定会接触到父子线程传值问题,如果不使用TransmittableThreadLocal就得自己手动设置到每个子线程里面去,很是麻烦。如果使用TransmittableThreadLocal需要注意线程池复用、子线程启动时机、线程池的上下文传播以及清理ThreadLocal避免内存泄漏哦!

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

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

相关文章

Web集群服务-代理和负载均衡

1. 概述 1. 用户----->代理--->Web节点,后面只有一个节点,一般使用的是nginx代理功能即可 2. 后面如果是集群需要使用nginx负载均衡功能 2. 代理分类 代理分类方向应用正向代理用户(服务器)-->代理--->外部(某网站)服务器通过代理实现共享上网/访问公网反向代理用…

数据结构~AVL树

文章目录 一、AVL树的概念二、AVL树的定义三、AVL树的插入四、AVL树的平衡五、AVL树的验证六、AVL树的删除七、完整代码八、总结 一、AVL树的概念 AVL树是最先发明的自平衡二叉查找树&#xff0c;AVL是⼀颗空树&#xff0c;或者具备下列性质的二叉搜索树&#xff1a;它的左右子…

《微软飞行模拟2024》在飞行中可能占用高达180 Mb/s的互联网带宽

《微软飞行模拟2024》是一款要求相当高的游戏。 从理想的系统规格所需的高性能系统来看&#xff0c;该游戏目前在用户飞行和地形加载时使用的网络带宽高达 180 Mb/s。 这相当于每小时耗费高达 81 GB 的网络数据&#xff0c;对于有数据上限的用户来说简直就是噩梦。 数据上限通…

[Python学习日记-47] Python 中的系统调用模块—— os 与 sys

[Python学习日记-47] Python 中的系统调用模块 简介 os sys 简介 os 模块和 sys 模块提供了很多允许你的程序与操作系统直接交互的功能。下面将进行逐一介绍。 os 一、os.getcwd() 得到当前工作目录&#xff0c;即当前 Python 脚本工作的目录路径&#xff08;绝对路径&#…

芝法酱学习笔记(0.7)——harbor与SpringBoot容器化docker部署

前言 之前我们主要讲的jar包部署。使用jar包部署可能导致不同服务互相争抢资源&#xff08;隔离性&#xff09;&#xff0c;不同服务可能需要不同的jdk环境&#xff0c;有时也会造成困扰。故在微服务时代&#xff0c;我们通常使用docker部署 一、docker安装 docke相关的知识…

sherpa-ncnn 语言模型简单对比

在昨天把系统搞崩溃前&#xff0c;对sherpa-ncnn的中文模型做了一个简单的对比。这次使用的分别是sherpa-ncnn-streaming-zipformer-bilingual-zh-en-2023-02-13&#xff08;以下简称bilingual-zh-en-2023-02-13&#xff09;和sherpa-ncnn-streaming-zipformer-small-bilingual…

WPF自定义控件实现的几种方法

Windows Presentation Foundation (WPF) 是微软提供的一种用于构建 Windows 应用程序的开发框架。它以其强大的数据绑定、资源管理和可视化效果处理能力而闻名。在WPF中&#xff0c;自定义控件的实现是一个非常重要的方面&#xff0c;几乎所有的应用程序都会或多或少地需要自定…

哪款宠物空气净化器性价比高?希喂、米家和范罗士哪款更好?

这次我真的不是很想抱怨&#xff0c;是我男朋友真的很过分&#xff01;真的很过分&#xff0c;差点让我们两个分道扬镳。先听我说&#xff0c;这不是我和他都嫌家里太安静了吗&#xff0c;每天下班后两个人吃完饭就各玩各的手机&#xff0c;生活太无趣了&#xff0c;加上这几年…

【云从】五、负载均衡CLB

文章目录 1、负载均衡2、云负载均衡CLB3、CLB的组成4、CLB的应用场景 1、负载均衡 互联网发展早期&#xff0c;应用服务单机部署就足以负载所有用户的访问需求 如此&#xff0c;部署和运维都简单&#xff0c;但随着用户和访问量的提高&#xff0c;单台服务器的硬件性能是有上限…

【GESP】C++一级练习BCQM3044,字符形状输出

回到一级知识点&#xff0c;用给定字符按指定形状输出。 题目题解详见&#xff1a;https://www.coderli.com/gesp-1-bcqm3044/ 【GESP】C一级练习BCQM3044&#xff0c;字符形状输出 | OneCoder回到一级知识点&#xff0c;用给定字符按指定形状输出。https://www.coderli.com/…

鸿蒙开发 四十五 鸿蒙状态管理(嵌套对象界面更新)

当运行时的状态变量变化&#xff0c;UI重新渲染&#xff0c;在ArkUI中称为状态管理机制&#xff0c;前提是变量必须被装饰器修饰。不是状态变量的所有更改都会引起刷新&#xff0c;只有可以被框架观测到的更改才会引起UI刷新。其中boolen、string、number类型&#xff0c;可观察…

【项目安全设计】软件系统安全设计规范和标准(doc原件)

1.1安全建设原则 1.2 安全管理体系 1.3 安全管理规范 1.4 数据安全保障措施 1.4.1 数据库安全保障 1.4.2 操作系统安全保障 1.4.3 病毒防治 1.5安全保障措施 1.5.1实名认证保障 1.5.2 接口安全保障 1.5.3 加密传输保障 1.5.4终端安全保障 资料获取&#xff1a;私信或者进主页。…

如何从模块内部运行 Pytest

在 Python 中&#xff0c;pytest 是一个强大的测试框架&#xff0c;用于编写和运行测试用例。通常我们会在命令行中运行 pytest&#xff0c;但是有时你可能希望从模块或脚本的内部运行 pytest&#xff0c;比如为了自动化测试或集成到某个工作流程中。 1、问题背景 当你从模块…

Luatools太难了?保姆级教程来啦!

作为由合宙所提供的调试工具&#xff0c;Luatools支持最新固件获取、固件打包、trace打印、单机烧录等功能 此工具适用于合宙所有 4G 模组和 4G GNSS 模组。 一、下载并安装 &#xff08;一&#xff09;运行环境要求 此工具运行于win7及以上系统;不支持 Mac和 Linux。 &…

三亚旅游微信小程序的设计与实现

详细视频演示 请联系我获取更详细的演示视频 项目运行截图 技术框架 后端采用SpringBoot框架 Spring Boot 是一个用于快速开发基于 Spring 框架的应用程序的开源框架。它采用约定大于配置的理念&#xff0c;提供了一套默认的配置&#xff0c;让开发者可以更专注于业务逻辑而不…

vulnhub(15):lemonsqueezy(hydra爆破、计划任务提权)

端口 nmap -Pn -p- 192.168.72.173 ​ PORT STATE SERVICE 80/tcp open http MAC Address: 00:0C:29:B8:2D:FC (VMware) 打点 80端口 主页面是apache2的默认页面&#xff0c;没有robots.txt&#xff0c;我们直接扫描目录 gobuster dir -u http://192.168.72.173/ -w /usr/…

SHELL脚本之输出语句的使用

shell脚本能够给用户显示一些信息&#xff0c;就需要输出语句的使用。 1.echo语句 如上图所示&#xff0c;中英文都可以&#xff0c; 如上图所示&#xff0c;在shell脚本中对于转义符的使用应该加上-e的选项&#xff0c;\n表示换行&#xff0c;\t表示电脑键盘上使用tab键隔开的…

24/10/12 算法笔记 AlexNet

AlexNet采用深度网络结构&#xff0c;由8层组成&#xff0c;包括5个卷积和3个全连接层&#xff0c;这种深度结构使得网络能够学习到更复杂的特征表示 1.ReLU激活函数&#xff1a; 首次成功的在较深的网络中使用ReLU激活函数&#xff0c;解决了梯度消失问题&#xff0c;加快了…

华为国际云:全球领先的云服务解决方案

近年来&#xff0c;随着云计算技术的迅猛发展&#xff0c;越来越多的企业开始拥抱云计算&#xff0c;以实现业务的数字化转型和创新。在众多云服务提供商中&#xff0c;华为国际云凭借其强大的技术实力和全球布局&#xff0c;成为了备受瞩目的明星。那么&#xff0c;华为国际云…

Python连接Oracle

Python连接Oracle 可以使用Oracle提供的官方Python驱动程序cx_Oracle。&#xff1a; cx_Oracle官方驱动程序 安装cx_Oracle驱动程序&#xff1a;首先&#xff0c;确保你已经安装了Python解释器。然后&#xff0c;使用pip命令安装cx_Oracle驱动程序。可以在命令行中运行以下…