附1:多线程并发方案的不足——响应式Spring的道法术器

【响应式编程】Schedulers之线程池共用问题 本篇重点介绍下Reactor中线程池的共用问题。由于在响应式编程中线程数很有限,所以共用时一定要注意,而这个在以前命令式编程中是很少被关注的,需要引起注意。 阅读详情

本系列其他文章见:《响应式Spring的道法术器》。
本篇内容是响应式流的附录。

(以下接响应式流的1.2.1.1节,关于“CPU眼中的时间”的内容。请不要单独看这一篇内容,否则有些内容可能让你摸不着头脑 0…0)

多线程的方式有其不完美之处,而且有些难以驾驭——

一、耗时的上下文切换

CPU先生不太乐意切换进程,每次换进程的时候都需要一个小时,因为每次切换进程的时候,办公桌上所有的资料(进程上下文)都要重新换掉。但是每个进程都必须雨露均沾地照顾到,要不客户就不满意了。

操作系统部很贴心,进程任务里边又可以分成一批线程子任务,如果某个线程子任务在等待I/O和网络的数据,就先放一边换另一个线程子任务去做,而不是切换整个进程。切换线程任务是一种相对轻量级的操作,因为不同的线程任务的许多数据都是共享的,所以只需要更新线程相关的数据,不用完全重新整理办公桌。即便如此也需要将近半小时的切换时间,以便能够“熟悉”任务内容。

不过毕竟,CPU先生的工作逐渐充实起来了。如图:

注意到图中CPU的时间条中深褐色的为上下文切换的时间,可以想见,高并发情况下,线程数会非常多,那么上下文切换对资源的消耗也会变得明显起来。况且在切换过程中,CPU并未执行任何业务上的或有意义的计算逻辑。

这里我们没有关注线程的创建时间,因为应用通常会维护一个线程池。需要新的线程执行任务的时候,就从线程池里取,用完之后返还线程池。类似的,数据库连接池也是同样的道理。

通过这个图,我们可以得到估算线程池的大小的方法。为了让CPU能够恰好跑满,Java Web服务器的最佳工作线程数符合以下公式:

( 1 + I O 阻 塞 时 间 C P U 处 理 时 间 ) × C P U 个 数 {( 1 + {IO阻塞时间 \over CPU处理时间} )} \times CPU个数 (1+CPU处理时间IO阻塞时间​)×CPU个数

二、烦人的互斥锁

由于讨生活不易,CPU先生练就了“左右互搏术”,对外就说“超线程”,特有逼格。对于有些任务,工作效率几乎可以翻倍了,不过计算题多的时候,就不太行了,毕竟虽有两只手却只有一个脑子啊。而且今时不同往日,现在的CPU早就不再单打独斗了,比如CPU先生的办公室就还有3个CPU同事,4个CPU核心组成一个团队提供计算服务。

也正因为如此,涉及到线程间共享的数据方面,互相之间合作时不时出现冲突。

作为“贴身秘书”的一级缓存,每个CPU核心都会配置一个。CPU先生的秘书特别有眼力价,能随时准备好80%的数据给CPU先生使用,这些数据资料都是找内存组的同事要的。要的时候会让内存组的同事复印一份数据资料,拿来交给CPU先生。CPU先生算好之后的结果,它会再拿给内存组的同事。

有时候两个执行不同线程任务的CPU几乎同时让秘书来内存组要复印的数据资料,各自算好之后,再返回给内存组的同事更新,于是就会有一个结果会覆盖另一个结果,也就是说先算完的结果等于白算了。如下图“a)无锁”所示,线程1中的值已更新,但尚未通知到内存,也就是说线程2拿到的是过时的值,结果显然就不对了。所以,对于多线程频繁变化的共享数据,CPU先生会额外留个心眼,让秘书拿数据资料的时候,顺便告诉内存把这个数据先锁起来。直到算完的数据再拿回给内存的时候,才让它把锁解开。上锁期间,别人不能拿这个数据。其实不光是防别的CPU,CPU先生自己处理的多个线程之间都有可能出现这种冲突的情况,毕竟CPU先生虽然算题快,记性却非常差。

如此,下图“b)加锁”(怎么读起来感觉这么顺嘴0_0)这种方式就能够保证数据的一致性了。

这种方式叫做互斥锁。CPU先生粗略算了一下,每次加锁或解锁,大概会花费它1分钟左右的时间,但是可能被加锁的数据就是为了花几秒算个自增。这锁能起到作用还好,更糟心的是,许多时候,线程可能没有很多,撞到同一份数据资料的概率其实很低,但是以防万一,还不得不加锁,白白增加工作时间。

数据被加锁的时候让CPU先生很烦躁,谁知道它啥时候才会解锁,只能让“贴身秘书”时不时去看看了,锁解开之前这个线程就被阻塞住了。

更过分的是CPU先生遇到的一次死锁事件,至今令它心有余悸。那天它让“贴身秘书”找内存要数据,并让内存把数据锁起来。拿到手才发现数据有A、B两部分,它们只拿到了A。CPU先生让秘书去要B,问了好多次,一直都被锁着。后来才知道,另外一个CPU的秘书几乎同时拿到并锁了B,也一直在等A。WTF!

三、乐观不起来的乐观锁

CPU先生与其他众CPU一合计,不是频繁改动的数据就不加锁了,大家在往回更新数据的时候先看看有没有被人动过不就得了。如下图,

执行线程2的时候取走的是i==1,算完回来要更新的时候,发现i==2了,那刚才算的不作数,从新取值再算一遍。这种“比较并交换(Compare-and-Swap,CAS)”的指令是原子的,“现场检查现场更新”,不会给其他线程以可乘之机。

乐观情况下,如果线程不多,互相冲突的几率不大的话,很少导致阻塞情况的出现,既确保了数据一致性,又保证了性能。

但乐观锁也有其局限性,在高并发环境下,如果乐观锁所保护的计算逻辑执行时间稍微长一些,可能会陷入一直被别人更新的状态,往往性能还不如悲观锁。所以高并发且数据竞争激烈的情况下,乐观锁出场率并不高。

如果在高并发且某些变量容易被频繁改动的情况下,CAS比较失败并重新计算的概率就高了。我们不妨做个实验,扩展一个具有CAS算法的AtomicInteger:

MyAtomicInteger.java

public class MyAtomicInteger extends AtomicInteger {
    private AtomicLong failureCount = new AtomicLong(0);

    public long getFailureCount() {
        return failureCount.get();
    }

    /**
     * 从以下两个方法 inc 和 dec 可以看出 Atomic* 的原子性的实现原理:
     * 这是一种乐观锁,每次修改值都会【先比较再赋值】,这个操作在CPU层面是原子的,从而保证了其原子性。
     * 如果比较发现值已经被其他线程变了,那么就返回 false,然后重新尝试。
     */
    public void inc() {
        Integer value = get();  
        while (!compareAndSet(value, value + 1)) {  
            failureCount.getAndIncrement();  
            value = get();  
            // try {  
            //     TimeUnit.MICROSECONDS.sleep(2);  
            // } catch (InterruptedException e) {  
            //     e.printStackTrace();  
            // }  
        }  
    }

    public void dec() {
        Integer value = get();  
        while (!compareAndSet(value, value - 1)) {  
            failureCount.getAndIncrement();  
            value = get();  
            // try {  
            //     TimeUnit.MICROSECONDS.sleep(2);  
            // } catch (InterruptedException e) {  
            //     e.printStackTrace();  
            // }  
        }  
}

测试:

@Test
public void testCustomizeAtomic() throws InterruptedException {
    final MyAtomicInteger myAtomicInteger = new MyAtomicInteger();
    // 执行自增和自减操作的线程各10个,每个线程操作10000次
    Thread[] incs = new Thread[10];
    Thread[] decs = new Thread[10];
    for (int i = 0; i < incs.length; i++) {
        incs[i] = new Thread(() -> {
            for (int j = 0; j < 10000; j++) {
                myAtomicInteger.inc();
            }
        });
        incs[i].start();
        decs[i] = new Thread(() -> {
            for (int j = 0; j < 10000; j++) {
                myAtomicInteger.dec();
            }
        });
        decs[i].start();
    }

    for (int i = 0; i < 10; i++) {
        incs[i].join();
        decs[i].join();
    }

    System.out.println(myAtomicInteger.get() + " with " + myAtomicInteger.getFailureCount() + " failed tries.");
}

我电脑上跑出的结果是:

0 with 223501 failed tries.

总共20万次操作,失败了22万多次。

高并发情况下,如果计算时间比较长,那么就容易陷入总是被别人更新的状态,而导致性能急剧下降。比如上例,如果把MyAtomicInteger.java中sleep的注释打开,再次跑测试,就会出现久久都无法执行结束的情况。

四、莫名躺枪的指令重排

对于CPU先生来说,没有多线程的日子挺美好的。有了多线程之后,总是会莫名踩坑,比如一次接到的任务是有两个线程子任务:

// 一个线程执行:
a = 1;
x = b;

// 另一个线程执行:
b = 1;
y = a;

a, b, x, y 的初始值都是0。

CPU先生的注意力只在当前线程,而从来记不住切换过来之前的线程做了什么,做到哪了。客户也知道CPU先生记性不好,而且执行到半截可能会切到另一个线程去,对于可能出现的结果也有心理准备:

  1. x0 && y1:可能的执行顺序比如:a = 1; x = b; b = 1; y = a;
  2. x1 && y0:可能的执行顺序比如:b = 1; y = a; a = 1; x = b;
  3. x1 && y1:可能的执行顺序比如:a = 1; b = 1; x = b; y = a;

无外乎这三种结果嘛。结果CPU先生给算出了 x==0 && y==0!出现这种结果的原因只能是 x = b; y = a; 是在 a = 1; b = 1;这两句之前执行的。Buy why?

事实上CPU和编译器对于程序语句的执行顺序还有会做一些优化的。比如第一个线程的两句程序,相互之间并无任何依赖关系,对于当前线程来说,调整执行顺序并不会影响逻辑结果。比如执行第一句的时候a的结果CPU先生和“秘书”一级缓存都没有,这时候会跟二级缓存甚至内存要,但是CPU先生闲着难受,等待a的值的工夫就先把x = b执行了。第二个线程也有可能出现同样的情况,从而导致了第四种结果的出现。

对于没有依赖关系的执行语句,编译期和CPU会酌情进行指令重排,以便优化执行效率。这种优化在单线程下是没问题的,但是多线程下就需要在开发程序的时候采取一些措施来避免这种情况了。

CPU先生:

五、委屈的内存

多线程的处理方式对于解决客户的高并发需求确实很给力,虽然许多线程是处于等待数据状态,但总有一些线程能让CPU先生的工作饱和起来。客户对计算组CPU们吃苦耐劳的工作态度赞赏有加。

内存组则有些委屈了,多线程也有它们的很大功劳,CPU的“贴身秘书”只是保管一小部分临时数据,绝大多数的数据还是堆在内存组。多一个线程就得需要为这个线程准备一块儿“工作内存”,虽然划拨给内存组的空间越来越大,但是如果动不动就开成百上千个线程的话,面对堆积如山的数据还总是不太够。

六、多线程并非银弹

搞IT的如果不套用一句“XXX并非银弹”总是显得逼格不够,所以我也郑重其事地说一句“多线程并非银弹”。以上归纳下来:

  • 高并发环境下,多线程的切换会消耗CPU资源;
  • 应对高并发环境的多线程开发相对比较难,并且有些问题难以在测试环境发现或重现;
  • 高并发环境下,更多的线程意味着更多的内存占用。
响应式编程-基于Reactor模式WebFlux框架的Spring Gateway 响应式编程在一些java生态的框架中应用很多,如Spring Cloud Gateway、Spring AI等中,Spring Cloud Gateway依靠响应式编程框架WebFlux基于Netty实现了一个非阻塞的网关,解决传统网关请求量大时候的线程耗尽问题,另外对于日常AIGC生产一般的大模型流式数据返回也是使用到了响应式编程,这样可以第一时间拿到响应内容,提升用户体验。 阅读详情

相关推荐

滚雪球学Spring[7.1讲]:响应式编程(Reactive Programming)

在上一节【6.6 Spring Boot与容器化】中,我们探讨了如何通过 Docker 等容器化技术部署 Spring Boot 应用,使得微服务架构在分布式系统中的管理更加灵活和高效。随着应用规模和用户量的增长,系统需要处理更多的请求,传统的阻塞式编程模型显得力不从心,尤其在高并发、高延迟的场景下,性能瓶颈变得尤为明显。为了解决这些问题,响应式编程成为了一种新的编程范式。它通过非阻塞和事件驱动的方式处理数据流,大幅提升系统的吞吐量和响应速度。

**My Coding Family** 1030

Java处理异常的几个小知识

好程序员Java培训分享Java处理异常的几个小知识,如果 Java 方法不能按照正常的流程执行,那么可以通过另外一种途径退出:抛出一个封装了错误信息的对象,这个就是Java的异常;当发生异常时,后面的代码无法继续执行,而是由异常处理器继续执行。   一、异常的分类   Throwable是所有异常的超类,下一级可以分为Error和Exception :   1. Error   Error是指Java运行时系统内部的错误,或者说它代表了JVM本身的错误,通常都是比较严重的错误,比如内存溢出, 虚拟机错误等

qq_35970166的博客 228

响应式Spring编程:理论与实践相结合

响应式编程是一种以异步数据流为核心,通过使用声明式的函数来实现数据流的变换和交互。其关键特性包括:异步性:响应式系统不依赖于传统的同步阻塞调用,数据流可以异步处理,提高系统性能。事件驱动:数据流被抽象为事件流,系统通过事件驱动来处理数据和状态的变化。非阻塞:数据处理流程在遇到慢操作时不会阻塞当前线程,而是继续处理其他流或任务。

weixin_42598278的博客 1136

多线程并发与响应式编程

响应式编程:Actor模型 rxjava,scala,akka,reactor https://www.jianshu.com/p/d803e2a7de8e 并发编程:锁(synchronized ReentrantLock volitale,Atomic**)

红豆和绿豆的博客 458

Zephyr、FreeRTOS 和 RT-Thread

你是…更适合用嵌入式新手,刚接触 RTOSFreeRTOS(入门简单)IoT 项目开发,需要 BLE/WiFi/MQTTZephyr(协议全)想快速构建可部署的产品Zephyr(模块统一)只做单个任务或简单状态机控制FreeRTOS(轻量)做平台化、持续集成、自动测试Zephyr(CI 支持强)

is0815的博客 2243

库卡kuka仿真软件sim Pro 3.1简单实用教程

实验目的 建立机器人,自定义抓手,导入外部模型,掌握工具坐标、基坐标的使用,自动生成轨迹。 实验步骤 打开KUKA.Sim Pro 3.1.2仿真软件,依次点击KUKA Sim Library 3.1 → KUKA_ROBOTS→Small Robots(3kg-10kg)→Archiv→AGILUS-1 Series→sixx,双击KR 6 R700 sixx,加载机器臂到工作空间; 加载自定义抓手。双击Miscellaneous,再双击圆锥Cone设置上半径为0,下半径为40,高为100; 点击“捕

weixin_45168199的博客 4万+

响应式编程(反应式编程)的来龙去脉(同步编程、多线程编程、异步编程再到响应式编程)

本文将会讲述,响应式编程来龙去脉。我们从同步编程讲起,到多线程编程出现,再到为了解决阻塞线程导致浪费资源而出现的异步编程。最后为了更好编写异步的程序出现了的响应式编程

你好 2815

代数拓扑与AI架构:AI应用架构师深入探索数学研究如何分析复杂系统的结构特性

随着人工智能技术的飞速发展,AI系统变得越来越复杂。AI应用架构师需要有效的工具和方法来理解、设计和优化这些复杂系统。代数拓扑作为数学的一个分支,提供了独特的视角和技术来分析复杂系统的结构特性。本文旨在帮助AI应用架构师深入了解代数拓扑,以及如何将其应用于AI架构设计中,提升对复杂系统的分析能力。首先,我们会介绍代数拓扑和AI架构相关的核心概念及其联系,通过有趣的故事引入主题,让读者轻松理解复杂概念。接着,深入探讨代数拓扑的核心算法原理,结合具体的编程语言代码示例详细阐述。

大模型应用工坊 670

VisionAgent:5 分钟从零跑通自然语言视觉代码生成,新手也能写检测代码

深夜十一点,屏幕上是一张森林火灾的照片,明早的汇报要回答"图里有几个火点",而你才看完 OpenCV 教程的前五十页。这一步正是 vision-agent 解决的:一张图加一句话,VisionAgent 直接产出能跑的视觉检测代码。 ## 一句话说清:VisionAgent 是什么 VisionAgent 是 LandingAI 开源的视觉 AI 代理:你用一句话配上图片或视频,它从 60 多

gitblog_00697的博客 1123

语音算法面试复习系列3——语音识别基础 + CTC 详解

摘要:本文系统讲解了语音识别(ASR)的核心算法CTC。主要内容包括:1) ASR的核心挑战是输入音频帧与输出文字序列的对齐问题;2) CTC通过引入blank标记和动态规划算法,无需对齐标注即可训练;3) 详细解析CTC前向算法实现路径概率求和,以及贪心和Beam Search解码方法;4) 指出CTC的条件独立性假设导致无法建模输出依赖关系;5) 对比CTC与传统方法和Attention/Transducer的优缺点,分析实际应用中的subsampling技术。文章包含数学推导、代码实现和面试常见问题,

m0_60605453的博客 305

【数据集5】土地利用类型数量预测数据LUH2

简介土地利用类型数量预测数据LUH2

WW、forever的博客 4546

基于PCA9422与STM32的电源管理方案:动态调压与低功耗设计

电源管理是嵌入式系统稳定运行的基础,PMIC(电源管理集成电路)将多路电压转换、充电管理和状态监测集成于单芯片,通过I2C总线与主控通信,实现动态电压调节和系统级低功耗控制。相比分立LDO方案,PMIC具有更高转换效率、更小占板面积和更灵活的软件配置能力,广泛应用于电池供电设备、手持仪器和低功耗物联网节点。本文围绕PCA9422与STM32F103RC构建的完整电源链路,详细阐述硬件连接、寄存器配置、动态调压实现、充电管理状态机及多级低功耗模式设计,并结合实测功耗与效率数据,为嵌入式电源工程师提供从硬件到固

weixin_33863087的博客 323

基于 MATLAB 的精馏塔模拟仿真

基于 MATLAB 的精馏塔模拟仿真精馏塔是一种重要的化工设备,其中乙烯精馏塔和醋酸乙烯精馏塔是常见的两种精馏塔。本文基于 MATLAB 软件,给出了针对这两种精馏塔的模拟仿真代码。

与其临渊羡鱼,不如退而结网 903

响应式Spring的道法术器(Spring WebFlux 教程)

Spring WebFlux 2小时快速入门 Spring 5 之使用Spring WebFlux开发响应式应用。 lambda与函数式(15min) Reactor 3 响应式编程库(60min) Spring Webflux和Spring Data Reactive开发响应式应用(45min) 通过以上内容相信可以对Spring 5.0 推出的响应式开发有了初步的体会。如果希望有更加...

刘康的专栏 4万+

C++(Qt)软件调试---gdb调试入门用法(12)

学习GDB可以帮助程序员更好地调试程序,找到程序中的错误和问题,提高程序的质量和稳定性。 在开发大型软件时,程序中可能存在许多错误和问题,这些问题可能会导致程序崩溃或者出现不可预期的行为。 使用GDB可以帮助程序员更快地找到这些问题,并且提供一些调试工具来帮助程序员更好地理解程序的行为。

mahuifa的博客 5548

UDF学习(四)FLUENT UDF-DEFINE_PROFILE宏

利用DEFINE_PROFLILE可以指定以下边界或计算域信息:DEFINE_PROFILE(name,t,i)//既可以用。

weixin_44386973的博客 1806

(19)Reactor Processors——响应式Spring的道法术器

本系列其他文章见:《响应式Spring的道法术器》。 前情提要:响应式流 | Reactor3快速上手 | 深入理解响应式流规范 2.9 Processor Processor既是一种特别的发布者(Publisher)又是一种订阅者(Subscriber)。 所以你能够订阅一个Processor,也可以调用它们提供的方法来手动插入数据到序列,或终止序列。 前面一直在聊响应...

刘康的专栏 5856

WINCC7.3破解文件

WINCC7.3硬件够破解文件,有需要的下载!WINCC7.3硬件够破解文件,有需要的下载!

响应式Spring的道法术器(Spring WebFlux 快速上手 + 全面介绍)

https://blog.51cto.com/liukang/2090163 1. Spring WebFlux 2小时快速入门 Spring 5 之使用Spring WebFlux开发响应式应用。 lambda与函数式(15min) Reactor 3 响应式编程库(60min) Spring Webflux和Spring Data Reactive开发响应式应用(45min) 通过以上内容相信可以对Spring 5.0 推出的响应式开发有了初步的体会。如果希望有更加深入的了解,欢迎阅读下边的系列文章

qq120631157的博客 500

Devexpress16.2.6破解工具

Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具

win10 跳过U8环境检测的IE WebControls.rar

win10 安装u8的时候 提示IE WebControls 不通过,这个工具可以欺骗u8的检查。 跳过U8环境检测的IE WebControls。提示导入失败后,重新检测通过。

(2)响应式流——响应式Spring的道法术器

原文:https://blog.youkuaiyun.com/get_set/article/details/79466402 1.2 响应式流 上一节留了一个坑——为啥不用Java Stream来进行数据流的操作? 原因在于,若将其用于响应式编程中,是有局限性的。比如如下两个需要面对的问题: Web 应用具有I/O密集的特点,I/O阻塞会带来比较大的性能损失或资源浪费,我们需要一种异步非阻塞的响应式的库,而Java Stream是一种同步API。 假设我们要搭建从 数据层到前端的 一个变化传递管道,可能会遇到数据层

qq120631157的博客 532

lwip-2.0.3

lwip是瑞典计算机科学院(SICS)的Adam Dunkels 开发的一个小型开源的TCP/IP协议栈。实现的重点是在保持TCP协议主要功能的基础上减少对RAM 的占用。

Spring响应式高并发编程

reactor框架

2301_78960337的博客 1169

Spring Boot 多线程演进:从 2.0 到 4.0 的并发编程变革

摘要:本文系统分析了SpringBoot各版本在多线程和并发处理方面的演进。SpringBoot 2.x以传统线程池为主,3.x全面拥抱响应式编程并支持虚拟线程预览,4.x(预计)将正式集成虚拟线程和结构化并发。文章详细对比了各版本的线程实现方式、线程安全措施及迁移考量,指出从线程池到响应式再到虚拟线程的技术演进趋势,并针对不同应用场景给出版本选择建议。传统应用适合2.x稳定版,高并发I/O场景推荐3.x+虚拟线程,云原生应用可选择响应式模型,前沿项目可等待4.x的结构化并发特性。

weixin_37549398的博客 1285

Java-多线程并发-8.并发与响应式编程工具库

虽然 Vert.x 有自己的HTTP服务器和其他组件,但您也可以在Spring Boot中使用它来处理异步任务、使用EventBus等。在Spring Boot中使用Vert.x实现多线程编程的方式主要是利用Vert.x的异步和事件驱动特性。根据您的具体需求和场景,Akka提供了大量的功能和选项,这些选项可以帮助您更好地实现并发和多线程编程。在Spring Boot中,您可以创建一个Disruptor Bean,这样它就可以被注入到其他组件中。现在,您可以在您的服务或控制器中注入并使用 Vert.x。

yueerba126的博客 469

Spring 响应式编程,真香。有了它还扯什么高并发异步编程

WebFlux 是 Spring Framework5.0 中引入的一种新的反应式Web框架。通过Reactor项目实现Reactive Streams规范,完全异步和非阻塞框架。本身不会加快程序执行速度,但在高并发情况下借助异步IO能够以少量而稳定的线程处理更高的吞吐,规避文件IO/网络IO阻塞带来的线程堆积。一个简单的处理请求和响应的抽象,用来适配不同HTTP服务容器的API。关于Spring MVC 和Spring WebFlux 测评很多,本文引用下做简单说明。

weixin_74412978的博客 1252
上一篇: (12)自定义数据流(实战Docker事件推送的REST API)——响应式Spring的道法术器
下一篇: (13)Reactor的backpressure策略——响应式Spring的道法术器
享学IT
博客等级 码龄12年 1516粉丝 91原创
评论 12
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值