Spring Boot 整合——Spring retry有状态重试以及其注释和组件介绍

关于版本

依赖版本
springboot2.4.0
spring retry2.4.0

代码地址

因为每个例子涉及代码较多,且包含测试用例,如果都贴到文章中内容过多,所以只贴出了部分代码。全部的代码在这里:https://gitee.com/daifyutils/springboot-samples。

相关文章

Spring Boot 整合——Spring retry的基本使用

通过配置状态重试来使用CircuitBreaker带熔断功能的重试

无状态的重试

无状态重试,是在一个循环中执行完重试策略,即重试上下文保持在一个线程上下文中。

有状态的重试

有状态的重试时,上下文会保存在一个公共的缓存中。

  1. 注释中设置有状态的重试
@Retryable(stateful = true)
  1. JAVA中使用DefaultRetryState保存状态
    Object key = "retryState";
    boolean isForceRefresh = false;
    RetryState state = new DefaultRetryState(key, isForceRefresh);

带熔断功能的重试

使用CircuitBreaker我们可以实现熔断功能的重试。其需要设置三个参数

CircuitBreakerRetryPolicy retryPolicy =
        new CircuitBreakerRetryPolicy(new SimpleRetryPolicy(5));
// 此参数用来判断熔断器是否打开的超时时间
retryPolicy.setOpenTimeout(3000L);
// 此参数用来判断熔断器是否重置的超时时间
retryPolicy.setResetTimeout(9000L);

熔断逻辑的判断

其两个参数主要的应用是在判断熔断器电路是否打开的逻辑CircuitBreakerRetryPolicyisOpen

		public boolean isOpen() {
			long time = System.currentTimeMillis() - this.start;
			boolean retryable = this.policy.canRetry(this.context);
			if (!retryable) {// 如果不能重试
			    // 如果上次请求时间大于this.timeout 重置上下文
				if (time > this.timeout) {
					logger.trace("Closing");
					this.context = createDelegateContext(policy, getParent());
					this.start = System.currentTimeMillis();
					retryable = this.policy.canRetry(this.context);
				}
				else if (time < this.openWindow) {
				// 如果上次请求小于this.openWindow,则打开断路器
					if ((Boolean) getAttribute(CIRCUIT_OPEN) == false) {
						logger.trace("Opening circuit");
						setAttribute(CIRCUIT_OPEN, true);
					}
					this.start = System.currentTimeMillis();
					return true;
				}
			}
			else {// 可以进行重试
			// 如果距离上次请求时间大于openWindow则断路器闭合,重置上下文
				if (time > this.openWindow) {
					logger.trace("Resetting context");
					this.start = System.currentTimeMillis();
					this.context = createDelegateContext(policy, getParent());
				}
			}
			if (logger.isTraceEnabled()) {
				logger.trace("Open: " + !retryable);
			}
			setAttribute(CIRCUIT_OPEN, !retryable);
			return !retryable;
		}

上面的逻辑可以得到结论,总结下来。

this.timeout 为设置的 ResetTimeout (9000L)

this.openWindow 为设置的 OpenTimeout (3000L)

this.policy.canRetry(this.context) == false重试失败。

  1. 如果两次请求时间大于ResetTimeout则重置上下文后,重新判断canRetry

  2. 如果两次请求时间小于OpenTimeout则打开断路器

this.policy.canRetry(this.context) == true可以继续进行重试。

  1. 如果两次请求时间大于OpenTimeout则重置上下文

带熔断功能重试的例子

JAVA

    public void retryState(long sleepTime){
        RetryTemplate template = RetryTemplate.builder().retryOn(Exception.class).build();
        CircuitBreakerRetryPolicy retryPolicy =
                new CircuitBreakerRetryPolicy(new SimpleRetryPolicy(5));
        // 此参数用来判断熔断器是否打开的超时时间动
        retryPolicy.setOpenTimeout(2000L);
        // 此参数用来判断熔断器是否重置的超时时间
        retryPolicy.setResetTimeout(20000L);
        template.setRetryPolicy(retryPolicy);
        Object key = "retryState";
        boolean isForceRefresh = false;
        RetryState state = new DefaultRetryState(key, isForceRefresh);
        for (int i = 0; i < 20; i++) {
            try {
                String result = template.execute(new RetryCallback<String, RuntimeException>() {
                    @Override
                    public String doWithRetry(RetryContext context) throws RuntimeException {
                        log.info("重试次数:" + context.getRetryCount());
                        log.warn("业务执行失败!!!" + LocalTime.now());
                        try {
                            Thread.sleep(200L * 1);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        throw new RuntimeException("timeout");
                    }
                }, new RecoveryCallback<String>() {
                    @Override
                    public String recover(RetryContext context) throws Exception {
                        log.warn("业务执行recover!!!" + LocalTime.now());
                        return "default";
                    }
                },state);
                if (result.equals("default")) {
                    try {
                        log.info("请求被拒绝,休眠{}毫秒后再次请求。",sleepTime);
                        Thread.sleep(sleepTime);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            } catch (Exception e) {
                System.out.println(e);
            }
        }
    }

使用注解

    @CircuitBreaker(
            openTimeout = 3000L,
            resetTimeout = 7000L,
            maxAttempts = 5 // 最大重试次数
    )
    public String retryAnnotationState(){
        log.warn("业务执行失败!!!" + LocalTime.now());
        throw new RuntimeException("timeout");
    }

    @Recover
    public String recover() {
        log.info("请求被拒绝。");
        return "default";
    }

测试用例

以JAVA代码中的demo为测试例子

    /**
     * 有状态的重试,此时5次重试后熔断,重试时间小于openTimeout
     * @throws Exception
     */
    @Test
    public void baseRetry() throws Exception {
        retryStateService.retryState(1000L * 1);
    }

在熔断上下文被重置,然后使用小于OpenTimeout的频率发送请求,如此断路器会被持续打开,且上下文无法重置,请求会被持续性的熔断。

2021-01-17 21:43:43.711  INFO 17848 --- [           main] d.s.retry.service.RetryStateService      : 请求被拒绝,休眠1500毫秒后再次请求。
2021-01-17 21:43:45.211  WARN 17848 --- [           main] d.s.retry.service.RetryStateService      : 业务执行recover!!!21:43:45.211
2021-01-17 21:43:45.211  INFO 17848 --- [           main] d.s.retry.service.RetryStateService      : 请求被拒绝,休眠1500毫秒后再次请求。
2021-01-17 21:43:46.711  WARN 17848 --- [           main] d.s.retry.service.RetryStateService      : 业务执行recover!!!21:43:46.711
2021-01-17 21:43:46.711  INFO 17848 --- [           main] d.s.retry.service.RetryStateService      : 请求被拒绝,休眠1500毫秒后再次请求。
2021-01-17 21:43:48.212  WARN 17848 --- [           main] d.s.retry.service.RetryStateService      : 业务执行recover!!!21:43:48.212
2021-01-17 21:43:48.212  INFO 17848 --- [           main] d.s.retry.service.RetryStateService      : 请求被拒绝,休眠1500毫秒后再次请求。
2021-01-17 21:43:49.712  WARN 17848 --- [           main] d.s.retry.service.RetryStateService      : 业务执行recover!!!21:43:49.712
2021-01-17 21:43:49.712  INFO 17848 --- [           main] d.s.retry.service.RetryStateService      : 请求被拒绝,休眠1500毫秒后再次请求。
2021-01-17 21:43:51.212  WARN 17848 --- [           main] d.s.retry.service.RetryStateService      : 业务执行recover!!!21:43:51.212
2021-01-17 21:43:51.212  INFO 17848 --- [           main] d.s.retry.service.RetryStateService      : 请求被拒绝,休眠1500毫秒后再次请求。
2021-01-17 21:43:52.713  WARN 17848 --- [           main] d.s.retry.service.RetryStateService      : 业务执行recover!!!21:43:52.713
2021-01-17 21:43:52.713  INFO 17848 --- [           main] d.s.retry.service.RetryStateService      : 请求被拒绝,休眠1500毫秒后再次请求。

同样以JAVA中代码为测试例子

    /**
     * 有状态的重试,后熔断,重试总时间大于ResetTimeout
     * @throws Exception
     */
    @Test
    public void baseRetry2() throws Exception {
        retryStateService.retryState(2100L);
    }

在熔断上下文被重置,然后使用大于OpenTimeout的频率发送请求,则在时间到达ResetTimeout之后可以重新进行重试

2021-01-17 21:45:46.768  INFO 22244 --- [           main] d.s.retry.service.RetryStateService      : 重试次数:4
2021-01-17 21:45:46.768  WARN 22244 --- [           main] d.s.retry.service.RetryStateService      : 业务执行失败!!!21:45:46.768
java.lang.RuntimeException: timeout
2021-01-17 21:45:46.969  WARN 22244 --- [           main] d.s.retry.service.RetryStateService      : 业务执行recover!!!21:45:46.969
2021-01-17 21:45:46.969  INFO 22244 --- [           main] d.s.retry.service.RetryStateService      : 请求被拒绝,休眠2100毫秒后再次请求。
2021-01-17 21:45:49.070  WARN 22244 --- [           main] d.s.retry.service.RetryStateService      : 业务执行recover!!!21:45:49.070
2021-01-17 21:45:49.070  INFO 22244 --- [           main] d.s.retry.service.RetryStateService      : 请求被拒绝,休眠2100毫秒后再次请求。
2021-01-17 21:45:51.170  WARN 22244 --- [           main] d.s.retry.service.RetryStateService      : 业务执行recover!!!21:45:51.170
2021-01-17 21:45:51.170  INFO 22244 --- [           main] d.s.retry.service.RetryStateService      : 请求被拒绝,休眠2100毫秒后再次请求。
2021-01-17 21:45:53.271  WARN 22244 --- [           main] d.s.retry.service.RetryStateService      : 业务执行recover!!!21:45:53.271
2021-01-17 21:45:53.271  INFO 22244 --- [           main] d.s.retry.service.RetryStateService      : 请求被拒绝,休眠2100毫秒后再次请求。
2021-01-17 21:45:55.371  WARN 22244 --- [           main] d.s.retry.service.RetryStateService      : 业务执行recover!!!21:45:55.371
2021-01-17 21:45:55.371  INFO 22244 --- [           main] d.s.retry.service.RetryStateService      : 请求被拒绝,休眠2100毫秒后再次请求。
2021-01-17 21:45:57.471  WARN 22244 --- [           main] d.s.retry.service.RetryStateService      : 业务执行recover!!!21:45:57.471
2021-01-17 21:45:57.471  INFO 22244 --- [           main] d.s.retry.service.RetryStateService      : 请求被拒绝,休眠2100毫秒后再次请求。
2021-01-17 21:45:59.572  WARN 22244 --- [           main] d.s.retry.service.RetryStateService      : 业务执行recover!!!21:45:59.572
2021-01-17 21:45:59.572  INFO 22244 --- [           main] d.s.retry.service.RetryStateService      : 请求被拒绝,休眠2100毫秒后再次请求。
2021-01-17 21:46:01.672  WARN 22244 --- [           main] d.s.retry.service.RetryStateService      : 业务执行recover!!!21:46:01.672
2021-01-17 21:46:01.672  INFO 22244 --- [           main] d.s.retry.service.RetryStateService      : 请求被拒绝,休眠2100毫秒后再次请求。
2021-01-17 21:46:03.773  WARN 22244 --- [           main] d.s.retry.service.RetryStateService      : 业务执行recover!!!21:46:03.773
2021-01-17 21:46:03.773  INFO 22244 --- [           main] d.s.retry.service.RetryStateService      : 请求被拒绝,休眠2100毫秒后再次请求。
2021-01-17 21:46:05.873  WARN 22244 --- [           main] d.s.retry.service.RetryStateService      : 业务执行recover!!!21:46:05.873
2021-01-17 21:46:05.874  INFO 22244 --- [           main] d.s.retry.service.RetryStateService      : 请求被拒绝,休眠2100毫秒后再次请求。
2021-01-17 21:46:07.975  INFO 22244 --- [           main] d.s.retry.service.RetryStateService      : 重试次数:0
2021-01-17 21:46:07.975  WARN 22244 --- [           main] d.s.retry.service.RetryStateService      : 业务执行失败!!!21:46:07.975
java.lang.RuntimeException: timeout
2021-01-17 21:46:08.175  INFO 22244 --- [           main] d.s.retry.service.RetryStateService      : 重试次数:1
2021-01-17 21:46:08.175  WARN 22244 --- [           main] d.s.retry.service.RetryStateService      : 业务执行失败!!!21:46:08.175

使用注释和RetryTemplate的区别

这是一个没有解决的问题,有清楚怎么配置的同学可以告知下

使用RetryTemplate进行重试的时候,其会进行多次重试后达到最大重试次数后进入熔断时进入Recover方法。

而使用@CircuitBreaker进行熔断重试的时候,其进行多次重试时每次失败都会进入Recover方法然后熔断后才会只执行Recover方法。

重试注解介绍

用来定义重试策略的注解

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Retryable {

	// 设置兜底方法的方法名,默认为空获取当前类中Recover注解的方法
	String recover() default "";

	// 配置重试监听器bean名称
	String interceptor() default "";

	// 用来设置需要进行重试的异常
	Class<? extends Throwable>[] value() default {};

	// 等价于value一样是来确定需要进行重试的异常
	Class<? extends Throwable>[] include() default {};

	// 设置例外处理,例外的异常不需要处理
	Class<? extends Throwable>[] exclude() default {};

	// 定义重试统计的标签
	String label() default "";

	// 标识重试有状态的
	boolean stateful() default false;

	// 最大的重试次数
	int maxAttempts() default 3;

	// 返回一个重试最大次数的公式
	String maxAttemptsExpression() default "";

	// 为两次重试指定的Backoff属性
	Backoff backoff() default @Backoff();

	// 根据异常调用不同处理逻辑的公式
	String exceptionExpression() default "";
	// 重试接听器
	String[] listeners() default {};

}

用来定义兜底方法的注解

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Import(RetryConfiguration.class)
@Documented
public @interface Recover {

}

用来在入口类中定义是否启动重试功能

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@EnableAspectJAutoProxy(proxyTargetClass = false)
@Import(RetryConfiguration.class)
@Documented
public @interface EnableRetry {

	// 当proxyTargetClass属性为true时,使用CGLIB代理。默认使用基于标准Java接口的代理
	boolean proxyTargetClass() default false;

}

提供熔断功能的重试

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Retryable(stateful = true)
public @interface CircuitBreaker {

	// 
	Class<? extends Throwable>[] value() default {};

	// 
	Class<? extends Throwable>[] include() default {};

	// 
	Class<? extends Throwable>[] exclude() default {};

	// 
	int maxAttempts() default 3;

	// 
	String maxAttemptsExpression() default "";

	// 
	String label() default "";

	// 配置重置熔断器重新闭合的超时时间
	long resetTimeout() default 20000;

	// 熔断器重新闭合的超时时间的文字公式
	String resetTimeoutExpression() default "";

	// 配置熔断器电路打开的超时时间
	long openTimeout() default 5000;

	// 熔断器电路打开的超时时间的文字公式
	String openTimeoutExpression() default "";

	// 
	String exceptionExpression() default "";

}

退避策略

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Backoff {

	// 等同于delay,确定延迟时间
	long value() default 1000;

	// 指定重试的延迟时间
	long delay() default 0;

	// 最长的延迟时间
	long maxDelay() default 0;

	// 指定延迟的倍数
	double multiplier() default 0;

	// 延迟时间的函数公式
	String delayExpression() default "";

	// 最长延迟使劲的函数公式
	String maxDelayExpression() default "";

	// 延迟倍数的函数公式
	String multiplierExpression() default "";

	// 在multiplier设置的情况下设置该值为true将使再重试延迟随机化,
	boolean random() default false;

}

重试JAVA代码介绍

重试策略

重试实现类说明
NeverRetryPolicy只允许调用RetryCallback一次,不允许重试
AlwaysRetryPolicy允许无限重试,直到成功,此方式逻辑不当会导致死循环
SimpleRetryPolicy固定次数重试策略,默认重试最大次数为3次,RetryTemplate默认使用的策略
TimeoutRetryPolicy超时时间重试策略,默认超时时间为1秒,在指定的超时时间内允许重试
ExceptionClassifierRetryPolicy设置不同异常的重试策略,类似组合重试策略,区别在于这里只区分不同异常的重试
CircuitBreakerRetryPolicy有熔断功能的重试策略
CompositeRetryPolicy组合重试策略,有两种组合方式,乐观组合重试策略是指只要有一个策略允许重试即可以,悲观组合重试策略是指只要有一个策略不允许重试即可以

NeverRetryPolicy

    /**
     * 只允许调用RetryCallback一次,不允许重试
     * @return
     */
    public static RetryPolicy getNeverRetryPolicy() {
        NeverRetryPolicy neverRetryPolicy = new NeverRetryPolicy();
        return neverRetryPolicy;
    }

AlwaysRetryPolicy

    /**
     * 允许无限重试,直到成功,此方式逻辑不当会导致死循环
     * @return
     */
    public static RetryPolicy getAlwaysRetryPolicy() {
        AlwaysRetryPolicy alwaysRetryPolicy = new AlwaysRetryPolicy();
        return alwaysRetryPolicy;
    }

SimpleRetryPolicy

    /**
     * 固定次数重试策略,默认重试最大次数为3次,RetryTemplate默认使用的策略
     * @return
     */
    public static RetryPolicy getSimpleRetryPolicy() {
        SimpleRetryPolicy simpleRetryPolicy = new SimpleRetryPolicy();
        simpleRetryPolicy.setMaxAttempts(5);
        return simpleRetryPolicy;
    }

TimeoutRetryPolicy

    /**
     * 超时时间重试策略,默认超时时间为1秒,在指定的超时时间内允许重试
     * @return
     */
    public static RetryPolicy getTimeoutRetryPolicy() {
        TimeoutRetryPolicy timeoutRetryPolicy = new TimeoutRetryPolicy();
        timeoutRetryPolicy.setTimeout(3000L);
        return timeoutRetryPolicy;
    }

ExceptionClassifierRetryPolicy

    /**
     * 设置不同异常的重试策略,类似组合重试策略,区别在于这里只区分不同异常的重试
     * @return
     */
    public static RetryPolicy getExceptionClassifierRetryPolicy() {
        ExceptionClassifierRetryPolicy exceptionClassifierRetryPolicy = new ExceptionClassifierRetryPolicy();
        Map map = new HashMap();
        map.put(RuntimeException.class,getNeverRetryPolicy());
        map.put(Exception.class,getSimpleRetryPolicy());
        exceptionClassifierRetryPolicy.setPolicyMap(map);
        return exceptionClassifierRetryPolicy;
    }

CircuitBreakerRetryPolicy

    /**
     * 有熔断功能的重试策略,需设置3个参数openTimeout、resetTimeout和delegate
     * @return
     */
    public static RetryPolicy getCircuitBreakerRetryPolicy() {
        CircuitBreakerRetryPolicy circuitBreakerRetryPolicy = new CircuitBreakerRetryPolicy(new SimpleRetryPolicy());
        circuitBreakerRetryPolicy.setOpenTimeout(5000);
        circuitBreakerRetryPolicy.setResetTimeout(20000);
        return circuitBreakerRetryPolicy;
    }

CompositeRetryPolicy

    /**
     * 组合重试策略,有两种组合方式,乐观组合重试策略是指只要有一个策略允许重试即可以,悲观组合重试策略是指只要有一个策略不允许重试即可以,但不管哪种组合方式,组合中的每一个策略都会执行
     * @return
     */
    public static RetryPolicy getCompositeRetryPolicy() {
        CompositeRetryPolicy compositeRetryPolicy = new CompositeRetryPolicy();
        // 设置是否乐观重试策略
        compositeRetryPolicy.setOptimistic(true);
        compositeRetryPolicy.setPolicies(new RetryPolicy[]{getSimpleRetryPolicy(),getTimeoutRetryPolicy()});
        return compositeRetryPolicy;

    }

退避策略

退避实现类说明
NoBackOffPolicy无退避算法策略,每次重试时立即重试
FixedBackOffPolicy固定时间的退避策略,需设置参数sleeper和backOffPeriod,sleeper指定等待策略,默认是Thread.sleep,即线程休眠,backOffPeriod指定休眠时间,默认1秒
UniformRandomBackOffPolicy随机时间退避策略,需设置sleeper、minBackOffPeriod和maxBackOffPeriod,该策略在[minBackOffPeriod,maxBackOffPeriod之间取一个随机休眠时间,minBackOffPeriod默认500毫秒,maxBackOffPeriod默认1500毫秒
ExponentialBackOffPolicy指数退避策略,需设置参数sleeper、initialInterval、maxInterval和multiplier,initialInterval指定初始休眠时间,默认100毫秒,maxInterval指定最大休眠时间,默认30秒,multiplier指定乘数,即下一次休眠时间为当前休眠时间*multiplier
ExponentialRandomBackOffPolicy随机指数退避策略,引入随机乘数可以实现随机乘数回退

NoBackOffPolicy

无退避算法策略,每次重试时立即重试

    /**
     * 无退避策略
     * @return
     */
    public static BackOffPolicy getNoBackOffPolicy() {
        return new NoBackOffPolicy();
    }

FixedBackOffPolicy

    /**
     * 设置固定退避策略
     * @return
     */
    public static BackOffPolicy getFixedBackOffPolicy() {
        FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy();
        // 设置固定的延迟时间
        fixedBackOffPolicy.setBackOffPeriod(2000L);
        return fixedBackOffPolicy;
    }

UniformRandomBackOffPolicy

    /**
     * 设置随机退避策略,设置其最小时间和最大时间
     * @return
     */
    public static BackOffPolicy getUniformRandomBackOffPolicy() {
        UniformRandomBackOffPolicy uniformRandomBackOffPolicy = new UniformRandomBackOffPolicy();
        // 设置最小延迟时间
        uniformRandomBackOffPolicy.setMinBackOffPeriod(500L);
        // 设置最大延迟时间
        uniformRandomBackOffPolicy.setMaxBackOffPeriod(2000L);
        return uniformRandomBackOffPolicy;
    }

ExponentialBackOffPolicy

    public static BackOffPolicy getExponentialBackOffPolicy() {
        ExponentialBackOffPolicy exponentialBackOffPolicy = new ExponentialBackOffPolicy();
        // 设置初始延迟时间
        exponentialBackOffPolicy.setInitialInterval(300L);
        // 设置最大延迟时间
        exponentialBackOffPolicy.setMaxInterval(4000L);
        // 设置两次延迟时间的倍数
        exponentialBackOffPolicy.setMultiplier(3);
        return new NoBackOffPolicy();
    }

ExponentialRandomBackOffPolicy

    /**
     * 指数退避策略
     * @return
     */
    public static BackOffPolicy getExponentialRandomBackOffPolicy() {
        ExponentialRandomBackOffPolicy exponentialRandomBackOffPolicy = new ExponentialRandomBackOffPolicy();
        // 设置初始延迟时间
        exponentialRandomBackOffPolicy.setInitialInterval(300L);
        // 设置最大延迟时间
        exponentialRandomBackOffPolicy.setMaxInterval(4000L);
        // 设置两次延迟时间的倍数
        exponentialRandomBackOffPolicy.setMultiplier(3);
        return new NoBackOffPolicy();
    }
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大·风

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值