Apache Camel指南-第十九章:消息路由的高级使用方法(下)

负载均衡器

总览

负载均衡模式允许你委托信息处理几个端点之一,采用各种不同的负载均衡策略。

Java DSL示例

下列路线目标端点之间分配的传入消息,mock:xmock:ymock:z,使用循环负载平衡策略:

from("direct:start").loadBalance().roundRobin().to("mock:x", "mock:y", "mock:z");

XML配置示例

以下示例显示了如何在XML中配置相同的路由:

<camelContext xmlns="http://camel.apache.org/schema/spring">
  <route>
    <from uri="direct:start"/>
    <loadBalance>
        <roundRobin/>
        <to uri="mock:x"/>
        <to uri="mock:y"/>
        <to uri="mock:z"/>
    </loadBalance>
  </route>
</camelContext>

负载均衡策略

Apache Camel负载均衡器支持以下负载均衡策略:

  • 轮循
  • 随机
  • 粘性负载平衡
  • 主题
  • 故障转移
  • 加权轮循和加权随机
  • 自定义负载均衡器
  • 断路器

轮循

循环负载平衡策略在所有目标端点之间循环,将每个传入消息发送到该循环中的下一个端点。例如,如果目标端点列表是,mock:xmock:ymock:z,则传入的消息被发送到端点的以下序列:mock:xmock:ymock:zmock:xmock:ymock:z,等。

您可以在Java DSL中指定循环负载均衡策略,如下所示:

from("direct:start").loadBalance().roundRobin().to("mock:x", "mock:y", "mock:z");

或者,您可以使用XML配置相同的路由,如下所示:

<camelContext xmlns="http://camel.apache.org/schema/spring">
  <route>
    <from uri="direct:start"/>
    <loadBalance>
        <roundRobin/>
        <to uri="mock:x"/>
        <to uri="mock:y"/>
        <to uri="mock:z"/>
    </loadBalance>
  </route>
</camelContext>

随机

随机负载平衡策略从指定列表中随机选择目标端点。

您可以在Java DSL中指定随机负载平衡策略,如下所示:

from("direct:start").loadBalance().random().to("mock:x", "mock:y", "mock:z");

或者,您可以使用XML配置相同的路由,如下所示:

<camelContext xmlns="http://camel.apache.org/schema/spring">
  <route>
    <from uri="direct:start"/>
    <loadBalance>
        <random/>
        <to uri="mock:x"/>
        <to uri="mock:y"/>
        <to uri="mock:z"/>
    </loadBalance>
  </route>
</camelContext>

粘性负载平衡

粘性负载平衡策略将In消息定向到通过从指定表达式计算哈希值而选择的端点。此负载平衡策略的优点是,始终将相同值的表达式发送到同一服务器。例如,通过从包含用户名的标头中计算哈希值,可以确保将来自特定用户的消息始终发送到同一目标端点。另一种有用的方法是指定一个表达式,该表达式从传入消息中提取会话ID。这样可以确保将属于同一会话的所有消息发送到同一目标端点。

您可以在Java DSL中指定粘性负载平衡策略,如下所示:

from("direct:start").loadBalance().sticky(header("username")).to("mock:x", "mock:y", "mock:z");

或者,您可以使用XML配置相同的路由,如下所示:

<camelContext xmlns="http://camel.apache.org/schema/spring">
  <route>
    <from uri="direct:start"/>
    <loadBalance>
      <sticky>
        <correlationExpression>
          <simple>header.username</simple>
        </correlationExpression>
      </sticky>
      <to uri="mock:x"/>
      <to uri="mock:y"/>
      <to uri="mock:z"/>
    </loadBalance>
  </route>
</camelContext>

注意
当您将sticky选项添加到故障转移负载平衡器时,负载平衡器将从最后一个已知的正常端点开始。

主题

主题负载平衡策略将每个In消息的副本发送到所有列出的目标端点(有效地将消息广播到所有目标,例如JMS主题)。

您可以使用Java DSL来指定主题负载平衡策略,如下所示:

from("direct:start").loadBalance().topic().to("mock:x", "mock:y", "mock:z");

或者,您可以使用XML配置相同的路由,如下所示:

<camelContext xmlns="http://camel.apache.org/schema/spring">
  <route>
    <from uri="direct:start"/>
    <loadBalance>
        <topic/>
        <to uri="mock:x"/>
        <to uri="mock:y"/>
        <to uri="mock:z"/>
    </loadBalance>
  </route>
</camelContext>

故障转移

可作为Apache Camel 2.0failover负载平衡器能够尝试的情况下的Exchange失败,并在下一个处理器exception处理期间。您可以failover使用触发故障转移的特定例外列表配置。如果未指定任何例外,则故障转移将由任何例外触发。故障转移负载平衡器使用与onException异常子句相同的策略来匹配异常。

如果使用流,则启用流缓存

如果使用流传输,则在使用故障转移负载平衡器时应启用流缓存。这是必需的,以便在故障转移时可以重新读取流。

以下示例配置为仅在引发IOException异常时进行故障转移:

from("direct:start")
    // here we will load balance if IOException was thrown
    // any other kind of exception will result in the Exchange as failed
    // to failover over any kind of exception we can just omit the exception
    // in the failOver DSL
    .loadBalance().failover(IOException.class)
        .to("direct:x", "direct:y", "direct:z");

您可以选择指定多个异常进行故障转移,如下所示:

// enable redelivery so failover can react
errorHandler(defaultErrorHandler().maximumRedeliveries(5));

from("direct:foo")
    .loadBalance()
    .failover(IOException.class, MyOtherException.class)
    .to("direct:a", "direct:b");

您可以使用XML配置相同的路由,如下所示:

<route errorHandlerRef="myErrorHandler">
    <from uri="direct:foo"/>
    <loadBalance>
        <failover>
            <exception>java.io.IOException</exception>
            <exception>com.mycompany.MyOtherException</exception>
        </failover>
        <to uri="direct:a"/>
        <to uri="direct:b"/>
    </loadBalance>
</route>

以下示例显示了如何在循环模式下进行故障转移:

from("direct:start")
    //在状态循环模式下使用故障转移负载平衡器,
    //这意味着它将在发生异常时立即进行故障转移
    //因为它不继承错误处理程序。它还将继续重试,因为
    //它配置为无限期重试。
    .loadBalance().failover(-1, false, true)
    .to("direct:bad", "direct:bad2", "direct:good", "direct:good2");

您可以使用XML配置相同的路由,如下所示:

<route>
    <from uri="direct:start"/>
    <loadBalance>
        <!-- failover using stateful round robin,
        which will keep retrying the 4 endpoints indefinitely.
        You can set the maximumFailoverAttempt to break out after X attempts -->
        <failover roundRobin="true"/>
        <to uri="direct:bad"/>
        <to uri="direct:bad2"/>
        <to uri="direct:good"/>
        <to uri="direct:good2"/>
    </loadBalance>
</route>

如果要尽快故障转移到下一个端点,可以inheritErrorHandler通过配置禁用inheritErrorHandler=false。通过禁用错误处理程序,可以确保它不干预。这允许故障转移负载平衡器尽快处理故障转移。如果您还启用了该roundRobin模式,则它将继续重试直到成功。然后,您可以将maximumFailoverAttempts选项配置为较高的值,以使其最终耗尽并失败。

加权轮循和加权随机

在许多企业环境中,处理能力不平等的服务器节点承载着服务,通常最好根据各个服务器的处理能力来分配负载。甲加权循环算法或加权随机算法可以用来解决这一问题。

加权负载平衡策略允许您为每个服务器相对于其他服务器指定处理负载分配比率。您可以将此值指定为每个服务器的正处理权重。较大的数字表示服务器可以处理较大的负载。处理权重用于确定每个处理端点相对于其他处理端点的有效负载分配比率。

下表描述了可以使用的参数:

表8.3。加权期权

选项类型默认描述
roundRobinbooleanfalse轮询的默认值为false。在没有此设置或参数的情况下,使用的负载均衡算法是随机的。
distributionRatioDelimiterString,distributionRatioDelimiter是用来指定的分隔符distributionRatio。如果未指定此属性,则逗号,是默认的定界符。

以下Java DSL示例显示了如何定义加权循环路由和加权随机路由:

// Java
// round-robin
from("direct:start")
  .loadBalance().weighted(true, "4:2:1" distributionRatioDelimiter=":")
  .to("mock:x", "mock:y", "mock:z");

//random
from("direct:start")
  .loadBalance().weighted(false, "4,2,1")
  .to("mock:x", "mock:y", "mock:z");

您可以使用XML配置循环路由,如下所示:

<!-- round-robin -->
<route>
  <from uri="direct:start"/>
  <loadBalance>
    <weighted roundRobin="true" distributionRatio="4:2:1" distributionRatioDelimiter=":" />
    <to uri="mock:x"/>
    <to uri="mock:y"/>
    <to uri="mock:z"/>
  </loadBalance>
</route>

自定义负载均衡器

您也可以使用自定义负载均衡器(例如您自己的实现)。

使用Java DSL的示例:

from("direct:start")
     // using our custom load balancer
     .loadBalance(new MyLoadBalancer())
     .to("mock:x", "mock:y", "mock:z");

和使用XML DSL的相同示例:

<!-- this is the implementation of our custom load balancer -->
 <bean id="myBalancer" class="org.apache.camel.processor.CustomLoadBalanceTest$MyLoadBalancer"/>

 <camelContext xmlns="http://camel.apache.org/schema/spring">
   <route>
     <from uri="direct:start"/>
     <loadBalance>
       <!-- refer to my custom load balancer -->
       <custom ref="myBalancer"/>
       <!-- these are the endpoints to balancer -->
       <to uri="mock:x"/>
       <to uri="mock:y"/>
       <to uri="mock:z"/>
     </loadBalance>
   </route>
 </camelContext>

注意,在上面的XML DSL中,我们使用,仅在Camel 2.8及更高版本中可用。在旧版本中,您必须执行以下操作:

       <loadBalance ref="myBalancer">
         <!-- these are the endpoints to balancer -->
         <to uri="mock:x"/>
         <to uri="mock:y"/>
         <to uri="mock:z"/>
       </loadBalance>

要实现自定义负载平衡器,您可以扩展一些支持类,例如LoadBalancerSupportSimpleLoadBalancerSupport。前者支持异步路由引擎,而后者则不支持。这是一个例子:

public static class MyLoadBalancer extends LoadBalancerSupport {

     public boolean process(Exchange exchange, AsyncCallback callback) {
         String body = exchange.getIn().getBody(String.class);
         try {
             if ("x".equals(body)) {
                 getProcessors().get(0).process(exchange);
             } else if ("y".equals(body)) {
                 getProcessors().get(1).process(exchange);
             } else {
                 getProcessors().get(2).process(exchange);
             }
         } catch (Throwable e) {
             exchange.setException(e);
         }
         callback.done(true);
         return true;
     }
 }

断路器

断路器负载均衡器是一种有状态模式,用于监视某些异常的所有调用。最初,断路器处于闭合状态并传递所有消息。如果存在故障并且达到阈值,它将进入打开状态并拒绝所有呼叫,直到halfOpenAfter达到超时。超时后,如果有新的呼叫,断路器将传递所有消息。如果结果成功,则断路器将移至闭合状态,否则,断路器将移回断开状态。

Java DSL示例:

from("direct:start").loadBalance()
    .circuitBreaker(2, 1000L, MyCustomException.class)
    .to("mock:result");

Spring XML示例:

<camelContext id="camel" xmlns="http://camel.apache.org/schema/spring">
    <route>
    <from uri="direct:start"/>
    <loadBalance>
        <circuitBreaker threshold="2" halfOpenAfter="1000">
            <exception>MyCustomException</exception>
        </circuitBreaker>
        <to uri="mock:result"/>
    </loadBalance>
</route>
</camelContext>

Hystrix

总览

自Camel 2.18起可用。

其可提供在Camel路由断路器集成。Hystrix是一个延迟和容错库,旨在

  • 隔离对远程系统,服务和第三方库的访问点
  • 停止级联故障
  • 在不可避免发生故障的复杂分布式系统中实现弹性

如果使用maven,则将以下依赖项添加到pom.xml文件中以使用Hystrix:

<dependency>
      <groupId>org.apache.camel</groupId>
      <artifactId>camel-hystrix</artifactId>
      <version>x.x.x</version>
      <!-- Specify the same version as your Camel core version. -->
</dependency>

Java DSL示例

以下是示例路由,显示了Hystrix端点,该端点通过回退到内联的回退路由来防止运行缓慢。默认情况下,超时请求就是1000ms这样,HTTP端点必须相当快才能成功。

from("direct:start")
    .hystrix()
        .to("http://fooservice.com/slow")
    .onFallback()
        .transform().constant("Fallback message")
    .end()
    .to("mock:result");

XML配置示例

以下是相同的示例,但使用XML:

<camelContext xmlns="http://camel.apache.org/schema/spring">
  <route>
    <from uri="direct:start"/>
    <hystrix>
      <to uri="http://fooservice.com/slow"/>
      <onFallback>
        <transform>
          <constant>Fallback message</constant>
        </transform>
      </onFallback>
    </hystrix>
    <to uri="mock:result"/>
  </route>
</camelContext>

使用Hystrix后备功能

onFallback()方法用于本地处理,您可以在其中转换消息或调用bean或其他作为后备方法。如果需要通过网络调用外部服务,则应使用该onFallbackViaNetwork()方法,该方法在HystrixCommand使用其自己的线程池的独立对象中运行,因此不会耗尽第一个命令对象。

Hystrix配置示例

Hystrix具有许多选项,如下节所示。下面的示例显示了Java DSL,该Java DSL将执行超时设置为5秒而不是默认的1秒,并且让断路器在状态被触发为“跳闸”之前再次尝试10秒而不是5秒(默认值)之前等待打开。

from("direct:start")
    .hystrix()
        .hystrixConfiguration()
             .executionTimeoutInMilliseconds(5000).circuitBreakerSleepWindowInMilliseconds(10000)
        .end()
        .to("http://fooservice.com/slow")
    .onFallback()
        .transform().constant("Fallback message")
    .end()
    .to("mock:result");

以下是相同的示例,但使用XML:

<camelContext xmlns="http://camel.apache.org/schema/spring">
<camelContext xmlns="http://camel.apache.org/schema/spring">
  <route>
    <from uri="direct:start"/>
    <hystrix>
      <hystrixConfiguration executionTimeoutInMilliseconds="5000" circuitBreakerSleepWindowInMilliseconds="10000"/>
      <to uri="http://fooservice.com/slow"/>
      <onFallback>
        <transform>
          <constant>Fallback message</constant>
        </transform>
      </onFallback>
    </hystrix>
    <to uri="mock:result"/>
  </route>
</camelContext>

您还可以全局配置Hystrix,然后参考该
配置。例如:

<camelContext xmlns="http://camel.apache.org/schema/spring">
   <!-- This is a shared config that you can refer to from all Hystrix patterns. -->
   <hystrixConfiguration id="sharedConfig" executionTimeoutInMilliseconds="5000" circuitBreakerSleepWindowInMilliseconds="10000"/>

   <route>
         <from uri="direct:start"/>
         <hystrix hystrixConfigurationRef="sharedConfig">
         <to uri="http://fooservice.com/slow"/>
         <onFallback>
            <transform>
               <constant>Fallback message</constant>
            </transform>
         </onFallback>
      </hystrix>
      <to uri="mock:result"/>
   </route>
</camelContext>

多播

总览

组播模式具有固定目的地图案,其与兼容的InOut消息交换模式。这与收件人列表相反,后者仅与InOnly交换模式兼容。

组播模式

使用自定义聚合策略进行组播

而多播处理器接收多个输出响应于原始请求(一个来自每个接收者的)消息,原始主叫用户仅期望接收一个单一的答复。因此,在消息交换的回复分支上存在固有的不匹配,并且要克服此不匹配,必须为多播处理器提供自定义聚合策略。聚合策略类负责将所有Out消息聚合为单个回复消息。

考虑电子拍卖服务的示例,其中卖方将要出售的物品提供给买方列表。买方各自为该项目出价,卖方自动选择价格最高的出价。您可以使用multicast()DSL命令实现将要约分配给固定购买者列表的逻辑,如下所示:

from("cxf:bean:offer").multicast(new HighestBidAggregationStrategy()).
    to("cxf:bean:Buyer1", "cxf:bean:Buyer2", "cxf:bean:Buyer3");

如果卖方由端点,代表cxf:bean:offer和买家通过端点代表cxf:bean:Buyer1cxf:bean:Buyer2cxf:bean:Buyer3。为了合并从各个购买者那里收到的出价,多播处理器使用聚合策略HighestBidAggregationStrategy。您可以HighestBidAggregationStrategy在Java中实现,如下所示:

// Java
import org.apache.camel.processor.aggregate.AggregationStrategy;
import org.apache.camel.Exchange;

public class HighestBidAggregationStrategy implements AggregationStrategy {
    public Exchange aggregate(Exchange oldExchange, Exchange newExchange) {
        float oldBid = oldExchange.getOut().getHeader("Bid", Float.class);
        float newBid = newExchange.getOut().getHeader("Bid", Float.class);
        return (newBid > oldBid) ? newExchange : oldExchange;
    }
}

假设买方将出价插入标题为的标头中Bid

并行处理

默认情况下,多播处理器一个接一个地调用每个收件人端点(按照命令中列出的顺序to())。在某些情况下,这可能会导致无法接受的长时间延迟。为了避免较长的等待时间,可以选择通过添加parallelProcessing()子句来启用并行处理。例如,要在电子拍卖示例中启用并行处理,请如下定义路线:

from("cxf:bean:offer")
    .multicast(new HighestBidAggregationStrategy())
        .parallelProcessing()
        .to("cxf:bean:Buyer1", "cxf:bean:Buyer2", "cxf:bean:Buyer3");

现在,多播处理器使用线程池为每个端点调用买方端点,该线程池具有一个线程。

如果要自定义调用买方端点的线程池的大小,则可以调用该executorService()方法以指定自己的自定义执行程序服务。例如:

from("cxf:bean:offer")
    .multicast(new HighestBidAggregationStrategy())
        .executorService(MyExecutor)
        .to("cxf:bean:Buyer1", "cxf:bean:Buyer2", "cxf:bean:Buyer3");

其中MyExecutor是java.util.concurrent.ExecutorService类型的实例。

当交换具有InOut模式时,将使用聚合策略来聚合答复消息。默认的聚合策略采用最新的答复消息,并丢弃较早的答复。例如,以如下的路线,用户定制的策略,MyAggregationStrategy被用来从端点聚集的答复direct:adirect:b以及direct:c

from("direct:start")
  .multicast(new MyAggregationStrategy())
      .parallelProcessing()
      .timeout(500)
      .to("direct:a", "direct:b", "direct:c")
  .end()
  .to("mock:result");

XML配置示例

以下示例显示如何在XML中配置类似的路由,其中该路由使用自定义聚合策略和自定义线程执行程序:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
       http://camel.apache.org/schema/spring http://camel.apache.org/schema/spring/camel-spring.xsd
    ">

  <camelContext xmlns="http://camel.apache.org/schema/spring">
    <route>
      <from uri="cxf:bean:offer"/>
      <multicast strategyRef="highestBidAggregationStrategy"
                 parallelProcessing="true"
                 threadPoolRef="myThreadExcutor">
         <to uri="cxf:bean:Buyer1"/>
         <to uri="cxf:bean:Buyer2"/>
         <to uri="cxf:bean:Buyer3"/>
      </multicast>
    </route>
  </camelContext>

  <bean id="highestBidAggregationStrategy" class="com.acme.example.HighestBidAggregationStrategy"/>
  <bean id="myThreadExcutor" class="com.acme.example.MyThreadExcutor"/>

</beans>

其中parallelProcessing属性和threadPoolRef属性都是可选的。仅当要自定义多播处理器的线程行为时才需要设置它们。

对传出邮件应用自定义处理

组播模式复制源Exchange和组播复制。默认情况下,路由器会对源消息进行浅表复制。在浅表副本中,仅通过引用复制原始邮件的标头和有效负载,以便链接原始邮件的结果副本。因为多播消息的浅表副本已链接,所以如果消息正文是可变的,则无法应用自定义处理。您应用于发送到一个终结点的副本的自定义处理也将应用于发送到每个其他终结点的副本。

注意
尽管该multicast语法允许您process在该multicast子句中调用DSL命令,但这在语义上没有意义,并且其效果相同onPrepare(实际上,在这种情况下,processDSL命令无效)。

准备消息时使用onPrepare执行自定义逻辑

如果要在将每个消息副本发送到其端点之前对每个消息副本应用自定义处理,则可以onPrepare在该multicast子句中调用DSL命令。该onPrepare命令只插入一个自定义的处理器后,该消息已被浅拷贝,只是之前的消息被分派到其端点。例如,在下面的途径,所述CustomProc处理器被调用上的消息发送到direct:a与所述CustomProc处理器还调用上发送到消息direct:b

from("direct:start")
  .multicast().onPrepare(new CustomProc())
  .to("direct:a").to("direct:b");

onPrepareDSL命令的 一个常见用例是对消息的某些或所有元素执行深度复制。例如,以下CustomProc处理器类执行消息主体的深层复制,其中消息主体被假定为Type *BodyType*,并且该深层复制通过方法进行*BodyType*.deepCopy()

// Java
import org.apache.camel.*;
...
public class CustomProc implements Processor {

    public void process(Exchange exchange) throws Exception {
        BodyType body = exchange.getIn().getBody(BodyType.class);

        // Make a _deep_ copy of of the body object
        BodyType clone =  BodyType.deepCopy();
        exchange.getIn().setBody(clone);

        // Headers and attachments have already been
        // shallow-copied. If you need deep copies,
        // add some more code here.
    }
}

您可以onPrepare用来实现要在Exchange多播之前执行的任何类型的自定义逻辑。

注意
建议实践为不可变的对象进行设计。

例如,如果您具有可变的消息正文作为此Animal类:

public class Animal implements Serializable {

     private int id;
     private String name;

     public Animal() {
     }

     public Animal(int id, String name) {
         this.id = id;
         this.name = name;
     }

     public Animal deepClone() {
         Animal clone = new Animal();
         clone.setId(getId());
         clone.setName(getName());
         return clone;
     }

     public int getId() {
         return id;
     }

     public void setId(int id) {
         this.id = id;
     }

     public String getName() {
         return name;
     }

     public void setName(String name) {
         this.name = name;
     }

     @Override
     public String toString() {
         return id + " " + name;
     }
 }

然后,我们可以创建一个深度克隆处理器来克隆消息正文:

public class AnimalDeepClonePrepare implements Processor {

     public void process(Exchange exchange) throws Exception {
         Animal body = exchange.getIn().getBody(Animal.class);

         // do a deep clone of the body which wont affect when doing multicasting
         Animal clone = body.deepClone();
         exchange.getIn().setBody(clone);
     }
 }

然后,我们可以使用如下所示的选项在多播路由中使用AnimalDeepClonePrepare类onPrepare

from("direct:start")
     .multicast().onPrepare(new AnimalDeepClonePrepare()).to("direct:a").to("direct:b");

和XML DSL中的相同示例

<camelContext xmlns="http://camel.apache.org/schema/spring">
     <route>
         <from uri="direct:start"/>
         <!-- use on prepare with multicast -->
         <multicast onPrepareRef="animalDeepClonePrepare">
             <to uri="direct:a"/>
             <to uri="direct:b"/>
         </multicast>
     </route>

     <route>
         <from uri="direct:a"/>
         <process ref="processorA"/>
         <to uri="mock:a"/>
     </route>
     <route>
         <from uri="direct:b"/>
         <process ref="processorB"/>
         <to uri="mock:b"/>
     </route>
 </camelContext>

 <!-- the on prepare Processor which performs the deep cloning -->
 <bean id="animalDeepClonePrepare" class="org.apache.camel.processor.AnimalDeepClonePrepare"/>

 <!-- processors used for the last two routes, as part of unit test -->
 <bean id="processorA" class="org.apache.camel.processor.MulticastOnPrepareTest$ProcessorA"/>
 <bean id="processorB" class="org.apache.camel.processor.MulticastOnPrepareTest$ProcessorB"/>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

沙子可可

你的鼓励是我创造的动力!

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

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

打赏作者

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

抵扣说明:

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

余额充值