整合线程池

本文介绍了如何在Spring Boot中封装线程池,包括新增application.yml配置、properties类、配置类和服务实现,以实现线程池的统一管理和性能监控。此外,还推荐了两种线程池配置,一种用于常见短任务,另一种针对长耗时任务。最后,解答了Spring线程池与JDK线程池的区别,并讨论了动态调整线程池配置的方法。
摘要由CSDN通过智能技术生成

频繁的创建、销毁线程和线程池,会给系统带来额外的开销。未经池化统一管理的线程,则会导致系统内线程数上限不可控。随着访问数增加,系统内线程数持续增长,CPU负载逐步提高。极端情况下,甚至可能会导致CPU资源被吃满,整个服务不可用。

封装线程池

新增application.yml配置

这里主要是配置ThreadPoolTastExecutor比较重要的参数

# 定义一个线程池配置
thread:
  # 使用ThreadPoolExecutor作为线程池实现
  poolexecutor:
    core-pool-size: 10          # 线程池的基本大小,核心线程一直存活
    max-pool-size: 20          # 线程池最大大小
    queue-capacity: 200        # 线程池任务队列的容量,任务将队列塞满之后,扩展核心线程,线程总数最多不超过最大线程数
    keep-alive-seconds: 60     # 线程的空闲时间
    thread-name-prefix: thread-pool-executor- # 线程名称的前缀

properties类

这个是为了简化代码,不用@Value一个一个去获取值

@Component
@ConfigurationProperties(prefix = "thread.poolexecutor")
public class ThreadPoolProperties {

    private Integer corePoolSize;
    private Integer maxPoolSize;
    private Integer queueCapacity;
    private Integer keepAliveSeconds;
    private String threadNamePrefix;

		// getter,setter
}

配置类

配置线程池,并将其注入到bean中

// 开启异步支持的配置类
@EnableAsync
// 配置类
@Configuration
public class ThreadPoolConfig {

    // 自动注入线程池属性配置
    @Autowired
    private ThreadPoolProperties threadPoolProperties;

    /**
     * 创建并配置一个线程池任务执行器。
     * 
     * @return Executor 返回配置好的线程池任务执行器实例。
     */
    @Bean(name = "taskExecutor")
    public ThreadPoolTaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 根据配置设定线程池的核心大小,核心线程一直存活
        executor.setCorePoolSize(threadPoolProperties.getCorePoolSize());
        // 根据配置设定线程池的最大大小
        executor.setMaxPoolSize(threadPoolProperties.getMaxPoolSize());
        // 根据配置设定线程池队列的容量,任务将队列塞满之后,扩展核心线程,线程总数最多不超过最大线程数
        executor.setQueueCapacity(threadPoolProperties.getQueueCapacity());
        // 根据配置设定线程的保持活跃时间
        executor.setKeepAliveSeconds(threadPoolProperties.getKeepAliveSeconds());
        // 设置线程名称前缀
        executor.setThreadNamePrefix(threadPoolProperties.getThreadNamePrefix());
        // 线程池关闭时等待所有任务完成再销毁
        executor.setWaitForTasksToCompleteOnShutdown(true);
        // 设置拒绝执行处理器为CallerRunsPolicy,即调用者运行策略:任务超出线程池容量后,新任务交还主线程处理
        // 当提交的线程填满核心线程数,并且塞满了队列缓冲区,并且超过了最大线程数时就会触发拒绝策略;
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        return executor;
    }
}

封装Service和service实现类,方便管理

public interface ICustomAsync {
    void doAsync();
}
@Service // 服务组件
public class CustomAsyncImpl implements ICustomAsync {

    private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(CustomAsyncImpl.class); // 初始化日志记录器

    @Override
    @Async("taskExecutor") // 指定使用名为"taskExecutor"的线程池进行异步执行
    public void doAsync() {
        logger.info("=== async start ===");
        logger.info("线程: {}执行代码逻辑", Thread.currentThread().getName());
        logger.info("=== async end ===");
    }
}

测试

@SpringBootTest
public class CustomAsyncTest {

    @Autowired
    private ICustomAsync customAsync;

    @Test
    void doAsyncTests() {
        for (int i = 0; i < 10; i++) { 
            customAsync.doAsync();
        }
    }
}

返回结果:

2024-03-28 10:49:31,326 INFO  [thread-pool-executor-1] c.k.b.s.i.CustomAsyncImpl.(): === async start ===
2024-03-28 10:49:31,327 INFO  [thread-pool-executor-3] c.k.b.s.i.CustomAsyncImpl.(): === async start ===
2024-03-28 10:49:31,328 INFO  [thread-pool-executor-2] c.k.b.s.i.CustomAsyncImpl.(): === async start ===
2024-03-28 10:49:31,330 INFO  [thread-pool-executor-5] c.k.b.s.i.CustomAsyncImpl.(): === async start ===
2024-03-28 10:49:31,332 INFO  [thread-pool-executor-5] c.k.b.s.i.CustomAsyncImpl.(): 线程:thread-pool-executor-5执行代码逻辑
2024-03-28 10:49:31,332 INFO  [thread-pool-executor-5] c.k.b.s.i.CustomAsyncImpl.(): === async end ===
2024-03-28 10:49:31,331 INFO  [thread-pool-executor-2] c.k.b.s.i.CustomAsyncImpl.(): 线程:thread-pool-executor-2执行代码逻辑
2024-03-28 10:49:31,332 INFO  [thread-pool-executor-2] c.k.b.s.i.CustomAsyncImpl.(): === async end ===
2024-03-28 10:49:31,332 INFO  [thread-pool-executor-3] c.k.b.s.i.CustomAsyncImpl.(): 线程:thread-pool-executor-3执行代码逻辑
2024-03-28 10:49:31,333 INFO  [thread-pool-executor-3] c.k.b.s.i.CustomAsyncImpl.(): === async end ===
2024-03-28 10:49:31,344 INFO  [thread-pool-executor-4] c.k.b.s.i.CustomAsyncImpl.(): === async start ===
2024-03-28 10:49:31,346 INFO  [thread-pool-executor-4] c.k.b.s.i.CustomAsyncImpl.(): 线程:thread-pool-executor-4执行代码逻辑
2024-03-28 10:49:31,350 INFO  [thread-pool-executor-4] c.k.b.s.i.CustomAsyncImpl.(): === async end ===
2024-03-28 10:49:31,351 INFO  [thread-pool-executor-1] c.k.b.s.i.CustomAsyncImpl.(): 线程:thread-pool-executor-1执行代码逻辑
2024-03-28 10:49:31,354 INFO  [thread-pool-executor-1] c.k.b.s.i.CustomAsyncImpl.(): === async end ===
2024-03-28 10:49:31,357 INFO  [thread-pool-executor-6] c.k.b.s.i.CustomAsyncImpl.(): === async start ===
2024-03-28 10:49:31,358 INFO  [thread-pool-executor-6] c.k.b.s.i.CustomAsyncImpl.(): 线程:thread-pool-executor-6执行代码逻辑
2024-03-28 10:49:31,359 INFO  [thread-pool-executor-6] c.k.b.s.i.CustomAsyncImpl.(): === async end ===
2024-03-28 10:49:31,362 INFO  [thread-pool-executor-7] c.k.b.s.i.CustomAsyncImpl.(): === async start ===
2024-03-28 10:49:31,362 INFO  [thread-pool-executor-8] c.k.b.s.i.CustomAsyncImpl.(): === async start ===
2024-03-28 10:49:31,362 INFO  [thread-pool-executor-7] c.k.b.s.i.CustomAsyncImpl.(): 线程:thread-pool-executor-7执行代码逻辑
2024-03-28 10:49:31,362 INFO  [thread-pool-executor-8] c.k.b.s.i.CustomAsyncImpl.(): 线程:thread-pool-executor-8执行代码逻辑
2024-03-28 10:49:31,362 INFO  [thread-pool-executor-7] c.k.b.s.i.CustomAsyncImpl.(): === async end ===
2024-03-28 10:49:31,362 INFO  [thread-pool-executor-8] c.k.b.s.i.CustomAsyncImpl.(): === async end ===
2024-03-28 10:49:31,364 INFO  [thread-pool-executor-9] c.k.b.s.i.CustomAsyncImpl.(): === async start ===
2024-03-28 10:49:31,364 INFO  [thread-pool-executor-9] c.k.b.s.i.CustomAsyncImpl.(): 线程:thread-pool-executor-9执行代码逻辑
2024-03-28 10:49:31,364 INFO  [thread-pool-executor-9] c.k.b.s.i.CustomAsyncImpl.(): === async end ===
2024-03-28 10:49:31,367 INFO  [thread-pool-executor-10] c.k.b.s.i.CustomAsyncImpl.(): === async start ===
2024-03-28 10:49:31,367 INFO  [thread-pool-executor-10] c.k.b.s.i.CustomAsyncImpl.(): 线程:thread-pool-executor-10执行代码逻辑
2024-03-28 10:49:31,367 INFO  [thread-pool-executor-10] c.k.b.s.i.CustomAsyncImpl.(): === async end ===

可以发现,该方法使用了线程池.

扩展:打印线程池情况,占用性能

  • 编写 VisibleThreadPoolTaskExecutor 继承 ThreadPoolTaskExecutor
/**
 * 可见的线程池任务执行器,继承自ThreadPoolTaskExecutor,增强了线程池执行任务时的日志记录功能。
 */
public class VisibleThreadPoolTaskExecutor extends ThreadPoolTaskExecutor {

    private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory
            .getLogger(VisibleThreadPoolTaskExecutor.class);

    /**
     * 打印线程池当前状态的方法。
     * 
     * @param method 当前调用的方法名,用于日志标记。
     */
    private void log(String method) {
        ThreadPoolExecutor threadPoolExecutor = getThreadPoolExecutor();

        // 打印线程池关键信息到日志,包括线程名前缀、调用方法、任务总数、完成任务数、活跃线程数和队列长度。
        logger.info("线程池:{}, 执行方法:{},任务数量 [{}], 完成任务数量 [{}], 活跃线程数 [{}], 队列长度 [{}]",
                this.getThreadNamePrefix(),
                method,
                threadPoolExecutor.getTaskCount(),
                threadPoolExecutor.getCompletedTaskCount(),
                threadPoolExecutor.getActiveCount(),
                threadPoolExecutor.getQueue().size());
    }

    /**
     * 重写execute方法,记录执行日志。
     * 
     * @param task 将要执行的任务对象。
     */
    @Override
    public void execute(Runnable task) {
        log("execute");
        super.execute(task);
    }

    /**
     * 重写带启动超时的execute方法,记录执行日志。
     * 
     * @param task         将要执行的任务对象。
     * @param startTimeout 启动任务的超时时间。
     */
    @Override
    public void execute(Runnable task, long startTimeout) {
        log("execute");
        super.execute(task, startTimeout);
    }

    /**
     * 重写submit方法,记录执行日志。
     * 
     * @param task 将要提交的任务对象。
     * @return 返回Future对象,代表异步执行的结果。
     */
    @Override
    public Future<?> submit(Runnable task) {
        log("submit");
        return super.submit(task);
    }

    /**
     * 重写submit方法,记录执行日志。
     * 
     * @param task 将要提交的带返回值的任务对象。
     * @param <T>  返回值的类型。
     * @return 返回Future对象,代表异步执行的结果。
     */
    @Override
    public <T> Future<T> submit(Callable<T> task) {
        log("submit");
        return super.submit(task);
    }

    /**
     * 重写submitListenable方法,记录执行日志。
     * 
     * @param task 将要提交的任务对象。
     * @return 返回ListenableFuture对象,代表异步执行的结果。
     */
    @Override
    public ListenableFuture<?> submitListenable(Runnable task) {
        log("submitListenable");
        return super.submitListenable(task);
    }

    /**
     * 重写submitListenable方法,记录执行日志。
     * 
     * @param task 将要提交的带返回值的任务对象。
     * @param <T>  返回值的类型。
     * @return 返回ListenableFuture对象,代表异步执行的结果。
     */
    @Override
    public <T> ListenableFuture<T> submitListenable(Callable<T> task) {
        log("submitListenable");
        return super.submitListenable(task);
    }
}
  • 重新编写线程池配置类
// 开启异步支持的配置类
@EnableAsync
// 配置类
@Configuration
public class ThreadPoolConfig {

    // 自动注入线程池属性配置
    @Autowired
    private ThreadPoolProperties threadPoolProperties;

    @Bean(name = "visibleTaskExecutor")
    public Executor visible() {
        // 使用VisibleThreadPoolTaskExecutor作为类
        ThreadPoolTaskExecutor executor = new VisibleThreadPoolTaskExecutor();
        executor.setCorePoolSize(threadPoolProperties.getCorePoolSize());
        executor.setMaxPoolSize(threadPoolProperties.getMaxPoolSize());
        executor.setQueueCapacity(threadPoolProperties.getQueueCapacity());
        executor.setKeepAliveSeconds(threadPoolProperties.getKeepAliveSeconds());
        executor.setThreadNamePrefix(threadPoolProperties.getThreadNamePrefix());
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        return executor;
    }

    /**
     * 创建并配置一个线程池任务执行器。
     * 
     * @return Executor 返回配置好的线程池任务执行器实例。
     */
    @Bean(name = "taskExecutor")
    public ThreadPoolTaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 根据配置设定线程池的核心大小
        executor.setCorePoolSize(threadPoolProperties.getCorePoolSize());
        // 根据配置设定线程池的最大大小
        executor.setMaxPoolSize(threadPoolProperties.getMaxPoolSize());
        // 根据配置设定线程池队列的容量
        executor.setQueueCapacity(threadPoolProperties.getQueueCapacity());
        // 根据配置设定线程的保持活跃时间
        executor.setKeepAliveSeconds(threadPoolProperties.getKeepAliveSeconds());
        // 设置线程名称前缀
        executor.setThreadNamePrefix(threadPoolProperties.getThreadNamePrefix());
        // 线程池关闭时等待所有任务完成再销毁
        executor.setWaitForTasksToCompleteOnShutdown(true);
        // 设置拒绝执行处理器为CallerRunsPolicy,即调用者运行策略:任务超出线程池容量后,新任务交还主线程处理
        // 当提交的线程填满核心线程数,并且塞满了队列缓冲区,并且超过了最大线程数时就会触发拒绝策略;
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        return executor;
    }

}
  • service和实现类也重新编写
void visibleAsync();
/**
* 可视化,可以打印线程池情况
*/
@Override
@Async("visibleTaskExecutor")
public void visibleAsync() {
    logger.info("=== async start ===");
    logger.info("线程: {}执行代码逻辑", Thread.currentThread().getName());
    logger.info("=== async end ===");
}
  • 测试
@SpringBootTest
public class CustomAsyncTest {

    @Autowired
    private ICustomAsync customAsync;

    @Test
    void visibleAsyncTests() {
        for (int i = 0; i < 20; i++) {
            customAsync.visibleAsync();
        }
    }
}
  • 返回结果
2024-03-28 11:23:05,406 INFO  [main] c.k.b.c.VisibleThreadPoolTaskExecutor.(): 线程池:thread-pool-executor-, 执行方法:submit,任务数量 [0], 完成任务数量 [0], 活跃线程数 [0], 队列长度 [0]
2024-03-28 11:23:05,407 INFO  [main] c.k.b.c.VisibleThreadPoolTaskExecutor.(): 线程池:thread-pool-executor-, 执行方法:submit,任务数量 [1], 完成任务数量 [0], 活跃线程数 [1], 队列长度 [0]
2024-03-28 11:23:05,407 INFO  [main] c.k.b.c.VisibleThreadPoolTaskExecutor.(): 线程池:thread-pool-executor-, 执行方法:submit,任务数量 [2], 完成任务数量 [0], 活跃线程数 [2], 队列长度 [0]
2024-03-28 11:23:05,408 INFO  [main] c.k.b.c.VisibleThreadPoolTaskExecutor.(): 线程池:thread-pool-executor-, 执行方法:submit,任务数量 [3], 完成任务数量 [0], 活跃线程数 [3], 队列长度 [0]
2024-03-28 11:23:05,408 INFO  [main] c.k.b.c.VisibleThreadPoolTaskExecutor.(): 线程池:thread-pool-executor-, 执行方法:submit,任务数量 [4], 完成任务数量 [0], 活跃线程数 [4], 队列长度 [0]
2024-03-28 11:23:05,409 INFO  [main] c.k.b.c.VisibleThreadPoolTaskExecutor.(): 线程池:thread-pool-executor-, 执行方法:submit,任务数量 [5], 完成任务数量 [0], 活跃线程数 [5], 队列长度 [0]
2024-03-28 11:23:05,409 INFO  [main] c.k.b.c.VisibleThreadPoolTaskExecutor.(): 线程池:thread-pool-executor-, 执行方法:submit,任务数量 [6], 完成任务数量 [0], 活跃线程数 [6], 队列长度 [0]
2024-03-28 11:23:05,409 INFO  [main] c.k.b.c.VisibleThreadPoolTaskExecutor.(): 线程池:thread-pool-executor-, 执行方法:submit,任务数量 [7], 完成任务数量 [0], 活跃线程数 [7], 队列长度 [0]
2024-03-28 11:23:05,409 INFO  [main] c.k.b.c.VisibleThreadPoolTaskExecutor.(): 线程池:thread-pool-executor-, 执行方法:submit,任务数量 [8], 完成任务数量 [0], 活跃线程数 [8], 队列长度 [0]
2024-03-28 11:23:05,409 INFO  [main] c.k.b.c.VisibleThreadPoolTaskExecutor.(): 线程池:thread-pool-executor-, 执行方法:submit,任务数量 [9], 完成任务数量 [0], 活跃线程数 [9], 队列长度 [0]
2024-03-28 11:23:05,414 INFO  [main] c.k.b.c.VisibleThreadPoolTaskExecutor.(): 线程池:thread-pool-executor-, 执行方法:submit,任务数量 [10], 完成任务数量 [0], 活跃线程数 [10], 队列长度 [0]
2024-03-28 11:23:05,415 INFO  [main] c.k.b.c.VisibleThreadPoolTaskExecutor.(): 线程池:thread-pool-executor-, 执行方法:submit,任务数量 [11], 完成任务数量 [0], 活跃线程数 [10], 队列长度 [1]
2024-03-28 11:23:05,415 INFO  [main] c.k.b.c.VisibleThreadPoolTaskExecutor.(): 线程池:thread-pool-executor-, 执行方法:submit,任务数量 [12], 完成任务数量 [0], 活跃线程数 [10], 队列长度 [2]
2024-03-28 11:23:05,416 INFO  [main] c.k.b.c.VisibleThreadPoolTaskExecutor.(): 线程池:thread-pool-executor-, 执行方法:submit,任务数量 [13], 完成任务数量 [0], 活跃线程数 [10], 队列长度 [3]
2024-03-28 11:23:05,417 INFO  [main] c.k.b.c.VisibleThreadPoolTaskExecutor.(): 线程池:thread-pool-executor-, 执行方法:submit,任务数量 [14], 完成任务数量 [0], 活跃线程数 [10], 队列长度 [4]
2024-03-28 11:23:05,417 INFO  [main] c.k.b.c.VisibleThreadPoolTaskExecutor.(): 线程池:thread-pool-executor-, 执行方法:submit,任务数量 [15], 完成任务数量 [0], 活跃线程数 [10], 队列长度 [5]
2024-03-28 11:23:05,423 INFO  [main] c.k.b.c.VisibleThreadPoolTaskExecutor.(): 线程池:thread-pool-executor-, 执行方法:submit,任务数量 [16], 完成任务数量 [0], 活跃线程数 [10], 队列长度 [6]
2024-03-28 11:23:05,425 INFO  [main] c.k.b.c.VisibleThreadPoolTaskExecutor.(): 线程池:thread-pool-executor-, 执行方法:submit,任务数量 [17], 完成任务数量 [0], 活跃线程数 [10], 队列长度 [7]
2024-03-28 11:23:05,427 INFO  [main] c.k.b.c.VisibleThreadPoolTaskExecutor.(): 线程池:thread-pool-executor-, 执行方法:submit,任务数量 [18], 完成任务数量 [0], 活跃线程数 [10], 队列长度 [8]
2024-03-28 11:23:05,430 INFO  [main] c.k.b.c.VisibleThreadPoolTaskExecutor.(): 线程池:thread-pool-executor-, 执行方法:submit,任务数量 [19], 完成任务数量 [0], 活跃线程数 [10], 队列长度 [9]
2024-03-28 11:23:05,431 INFO  [thread-pool-executor-1] c.k.b.s.i.CustomAsyncImpl.(): === async start ===
2024-03-28 11:23:05,432 INFO  [thread-pool-executor-1] c.k.b.s.i.CustomAsyncImpl.(): 线程: thread-pool-executor-1执行代码逻辑
2024-03-28 11:23:05,432 INFO  [thread-pool-executor-1] c.k.b.s.i.CustomAsyncImpl.(): === async end ===
2024-03-28 11:23:05,433 INFO  [thread-pool-executor-1] c.k.b.s.i.CustomAsyncImpl.(): === async start ===
2024-03-28 11:23:05,435 INFO  [thread-pool-executor-1] c.k.b.s.i.CustomAsyncImpl.(): 线程: thread-pool-executor-1执行代码逻辑
2024-03-28 11:23:05,441 INFO  [thread-pool-executor-1] c.k.b.s.i.CustomAsyncImpl.(): === async end ===
2024-03-28 11:23:05,441 INFO  [thread-pool-executor-1] c.k.b.s.i.CustomAsyncImpl.(): === async start ===
2024-03-28 11:23:05,441 INFO  [thread-pool-executor-1] c.k.b.s.i.CustomAsyncImpl.(): 线程: thread-pool-executor-1执行代码逻辑
......

每次执行的时候,都会打印对应的线程池情况了

线程池配置推荐

常用线程池

commonTaskExecutor 该线程池核心线程20,缓存队列60,最大线程100

核心线程满载之后,新任务会先放到缓存队列中。适用于触发频率高、耗时短的任务。因为这种任务会迅速释放掉核心线程,然后继续消化缓存队列中的任务。这样既能保证队列中任务不会等太久,也降低了频繁创建销毁线程的资源开销。

长耗时线程池

对于耗时长的任务,如果使用常用线程池处理,会存在耗时长任务逐渐打满线程池负载,而触发频率高、耗时短的任务没有空闲线程可用的可能性。因此有必要单独为耗时长的任务配置一个线程池。

expensiveTaskExecutor 该线程池核心线程20,缓存队列0,最大线程100

核心线程满载之后,会立即开启新线程。适用于触发频率低、耗时长的任务。因为这类任务占用单个线程时间长,缓存队列为0,有利于缩短任务平均执行时间。

注:以上两个线程池配置,是基于网络模块自身业务特征,在实践中总结出来的,并不完全适用于其他模块,请根据各自模块业务特征进行微调

FAQ

Spring的线程池ThreadPoolTaskExecutor,和JDK自带线程池ThreadPoolExecutor的区别是什么?

ThreadPoolTaskExecutorThreadPoolExecutor的基础上进行了一层封装,增加了submitListenable方法。

submitListenable方法返回的ListenableFuture接口对象,可以用来添加线程执行完毕后成功和失败的回调方法。

Future.get()方法相比,通过回调的方式,不会阻塞主线程,线程执行结果或抛出的异常通过回调函数异步处理。

示例代码如下:

final ListenableFuture<String> listenableFuture = threadPoolTaskExecutor
    .submitListenable(() -> {
    	TimeUnit.SECONDS.sleep(2);
    	return "result";
	});
listenableFuture.addCallback(data -> log.info(data), e -> log.error(e.getMessage(), e));

是否有办法动态调整线程池配置?

可以将线程池的三个核心参数corePoolSizequeueCapacitymaxPoolSize,配置到配置中心中,项目内线程池从配置中心中取这个配置。

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值