RabbitMQ 消息中间件

RabbitMQ是一个由erlang开发的AMQP(Advanced Message Queue )的开源实现

PHP

安装服务端

通过终端输入以下命令进行安装

sudo apt install rabbitmq-server 

查看已安装的插件列表

rabbitmq-plugins list

开启web管理插件

sudo rabbitmq-plugins enable rabbitmq_management 

chown -R rabbitmq:rabbitmq /var/lib/rabbitmq/

插件rabbitmq_management启动成功后就可以通过web页面进行RabbitMQ的监控和管理(默认账户和密码都是guest) 

使用浏览器登录:http://localhost:15672/

wget https://github.com/FreedWu/RabbitMQ/blob/master/docs/rabbitmq.config.example
cp rabbitmq.config.example /etc/rabbitmq/rabbitmq.config

这是因为账号guest具有所有的操作权限,并且又是默认账号,出于安全因素的考虑,guest用户只能通过localhost登录使用,并建议修改guest用户的密码以及新建其他账号管理使用rabbitmq(该功能是在3.3.0版本引入的)添加用户

sudo rabbitmqctl add_user admin 123456

添加到administrator用户组

sudo rabbitmqctl set_user_tags admin administrator

设置读写等权限 注意"/"代表virtual host

sudo rabbitmqctl set_permissions -p "/" admin ".*" ".*" ".*"

查看用户

rabbitmqctl list_users

查看消息条数

sudo rabbitmqctl list_queues

安装amqp扩展,需要先安装rabbitmq-c。rabbitmq-c是一个用于C语言的,与AMQP server进行交互的client库,AMQP协议为版本0-9-1。rabbitmq-c与server进行交互前需要首先进行login操作,在操作后,可以根据AMQP协议规范,执行一系列操作。

wget -c https://github.com/alanxz/rabbitmq-c/releases/download/v0.8.0/rabbitmq-c-0.8.0.tar.gz
tar zxf rabbitmq-c-0.8.0.tar.gz
cd rabbitmq-c-0.8.0
./configure --prefix=/usr/local/rabbitmq-c
make && make install 

安装成功后可以查看librabbitmq version

安装amqp扩展

wget http://pecl.php.net/get/amqp-1.10.2.tgz
tar zxf amqp-1.10.2.tgz
cd amqp-1.10.2 
/usr/local/php/bin/phpize
./configure --with-php-config=/usr/local/php/bin/php-config --with-amqp --with-librabbitmq-dir=/usr/local/rabbitmq-c
make && make install

php.ini增加extension = amqp.so 重启php-fpm 

RabbitMQ+PHP展示实例

新建一个信道。

$channel = new AMQPChannel($connection);

新建一个交换机Exchange,并定义属性,交换机有四种类型的AMQP_EX_TYPE_DIRECTAMQP_EX_TYPE_FANOUTAMQP_EX_TYPE_HEADERAMQP_EX_TYPE_TOPIC,这里使用直连型DIRECT。

$exchange = new AMQPExchange($channel);
$exchange->setName($exchangeName);
$exchange->setType(AMQP_EX_TYPE_DIRECT);
$exchange->setFlags(AMQP_DURABLE);
$exchange->declareExchange();

Delete表示删除这个Queue,而purge表示清除所有在Queue里面的消息

 

$queue = new \AMQPQueue($channel);
$queue->setName($this->queue);
$res = $queue->purge();
if ($res) {
    $io->success(sprintf('清空队列【%s】成功', self::QUEUE_NAME));
}

新建生产者

<?php
$connArgs = array(
    'host' => '127.0.0.1',
    'port' => '5672',
    'login' => 'guest',
    'password' => 'guest',
    'vhost' => '/',
);
$exchangeName = 'e_zcj';
$route = 'key_1';
$conn = new AMQPConnection($connArgs);
if (!$conn->connect()) {
    die("Cannot connect to the broker!\n");
}
$channel = new AMQPChannel($conn);
$ex = new AMQPExchange($channel);
$ex->setName($exchangeName);
//$ex->setType('fanout');
//$ex->declareExchange();

for ($i = 0; $i < 21; $i++) {
    $message = json_encode(['MainType' => 'mq'.$i, 'data' => 'hello world'.$i]);
    echo "Send Message:".$ex->publish($message, $route)."\n";
}

$conn->disconnect();

只发布了推送的数据,没有消费。在rabbitmq后台是看不到数据的,必须配置消费队列

 

属性名    属性描述
Virtual host    虚拟主机
Name            交换机名称,同一个Virtual host下不能有相同的Name
Type             交换机类型
Durability       是否持久化,Durable:是 Transient:否
Auto delete    当最后一个绑定被删除后,该交换机将被删除
Internal          是否是内部专用exchange,是的话就意味着我们不能往exchange里面发送消息
Arguments    参数,是AMQP协议留给AMQP实现做扩展使用的

大数据推送(跨天10分钟推送一次)

// 获取当月第一天和最后一天的时间戳
$firstDayTimestamp = strtotime($startDate);
$lastDayTimestamp = strtotime($endDate);
// 外层循环:遍历当月的日期
$currentTimestamp = $firstDayTimestamp;
while ($currentTimestamp <= $lastDayTimestamp) {
    $currentDate = date('Y-m-d', $currentTimestamp);
    $currentTime = strtotime($currentDate);

    // 内层循环:每10分钟一个循环
    $interval = 10 * 60; // 10分钟的秒数
    $endTime = strtotime($currentDate.' 23:59:59');
    $innerTimestamp = $currentTime;
    while ($innerTimestamp <= $endTime) {
        $innerTime = date('H:i', $innerTimestamp);
        $innerTimestamp += $interval;
        $endCurrentTime = date('Y-m-d H:i:s', $innerTimestamp);
        $sql = "select * from {$tableName} where time_start >= '{$currentDate} {$innerTime}:00' and time_start < '{$endCurrentTime}'";
        $this->output->writeln($sql);
        $list = $conn->fetchAll($sql);
        if (!empty($list)) {
            foreach ($list as $value) {
                //sleep(1); 
                $this->mqPublishDataSingle($value, 'test.aa', 'win');
            }
            $this->output->writeln("{$tableName}---话单数据导入完毕");
        }
    }

    // 增加一天,进行下一次外层循环
    $currentTimestamp = strtotime('+1 day', $currentTimestamp);
}

消费者自动建表

思路:当消费数据入库表不存在时,报错同时数据不ACK.利用这个特性通过报错信息来建表,有表之后就能正常ACK

try {
    $result = $conn->exec( $sql );
} catch ( \Exception $e ) {
    //话单消费自动建表功能
    if (strpos($e->getMessage(), "doesn't exist") !== false && strpos($e->getMessage(), "Table '") !== false) {
        $date = date('Ym');
        //创建话单消费表
        $flag = $this->createCdrConsumerTable($date, $conn);
    }
}

新建消费者

<?php
$conn_args = array(  
    'host' => '127.0.0.1',  
    'port' => '5672',  
    'login' => 'guest',  
    'password' => 'guest',  
    'prefetch_count' => 20, 
    'vhost' => '/'  
);
//交换机名消息是不能直接发送到队列,它需要发送到交换器
$q_name = 'my_queue'; //队列名
$e_name = 'e_zcj';  //交换机名
$k_route = "key_1"; //路由key
//连接到borker和实例一个channel
$conn = new AMQPConnection($conn_args);
if (!$conn->connect()) {
    // AMQP服务器端可称为broker
    die("Cannot connect to the broker");
}
//实例channel
//大部分的业务操作是在rabbitmq中提供Channel这个接口中完
//成的,在php相应的扩展中的Amapchanenl需要连接的实例包
//括定义Queue、定义Exchange、绑定Queue与Exchange、发布消息
$channel = new AMQPChannel($conn);
$channel->qos(0, $conn_args['prefetch_count']); //消费者处理最大条数数据;
//创建交换机
$ex = new AMQPExchange($channel);
$ex->setName($e_name);
//设置交换机类型
$ex->setType(AMQP_EX_TYPE_DIRECT); //direct类型
$ex->setFlags(AMQP_DURABLE); //持久化 
//输出交换机状态
echo "Exchange Status:" . $ex->declare() . "\n";

//创建队列
$q = new AMQPQueue($channel);
$q->setName($q_name); //队列名
//持久化
$q->setFlags(AMQP_DURABLE);
echo "Message Total:" . $q->declare() . "\n";

//绑定交换机与队列,并指定路由键
echo "Queue Bind: " . $q->bind($e_name, $k_route) . "\n";

echo "MESSAGE:\n";

while (true) {
    //消息者回调函数,处理生产者发送过来的数据
    $q->consume("processMessage");
    //$q->consume('processMessage', AMQP_AUTOACK); //自动ACK应答
}
$conn->disconnect();
function processMessage($envelope, $queue)
{
    $deliveryTag = $envelope->getDeliveryTag();
    $body = $envelope->getBody();
    $data = json_decode($body, true);
    if (JSON_ERROR_NONE !== json_last_error()) {  
        $queue->ack($deliveryTag);  
        return true;  
    }  
    echo $data['MainType']."\n";  
    $queue->ack($deliveryTag); 
}

消息确认机制

首先RabbitMQ支持消息确认机制来保证消息被consumer正常处理不丢失,当然也可以通过no-ack不使用确认机制。RabbitMQ默认是使用ACK确认机制的。当Consumer接收到RabbitMQ发布的消息时需要在适当的时机发送一个ACK确认的包来告知RabbitMQ,自己接收到了消息并成功处理。这时队列就把这条消息删除了,如果消费端接收了消息,但是没有给返回ack应答,那么这条消息会继续存在unacked状态下,占据队列的空间。所以建议是在处理完消息任务后发送。

那如果不发送会怎样呢?

$msg = $envelope->getBody();
sleep(1);  //sleep1秒模拟任务处理
echo $msg."\n"; //处理消息
//$queue->ack($envelope->getDeliveryTag()); //手动发送ACK应答

在RabbitMQ中有一个prefetch_count的概念,这个参数的意思是允许Consumer最多同时处理unacked任务数量。

/etc/rabbitmq/rabbitmq.config

我的版本的RabbitMQ默认这个参数是3,也就是说如果某一个Consumer在收到消息后没有发送ACK确认包,RabbitMQ就会任务Consumer还在处理任务,当有3个消息都没有发送ACK确认包时,RabbitMQ就不会再发送消息给该Consumer。如果prefetch_count的值超过了30,那么网络带宽限制开始占主导地位,此时进一步增加prefetch_count的值就会变得收效甚微。也就是说,官方是建议把prefetch_count设置为30.

参数heartbeat参数表示心跳频率,60则表示每60S发一次心跳包。连接长时间未响应(超过了心跳检测时间),服务端自动断开连接,而客户端的连接显示依然在,这时对服务端做一些响应操作时,会报错:Library error: a socket error occurred

  • 1.将心跳检测间隔设置更长:
  • 2.在处理代码过程中,频繁的调用declareQueue()方法(必须在连接还在的时候调用,或者做其他响应队列操作),原理是,频繁的对消息队列的服务端做相应,保持互动。

心跳间隔(heartbeat interval)
心跳间隔为服务端和客户端心跳检测时间间隔,一般为心跳超时read_write_timeout的一半(心跳超时/2),即2次心跳检测失败便认为连接不可达。

客户端和服务端的所有流量都会被记做为心跳。所以客户端可以仅在必要的时候,也就是没有其他流量的情况下发送心跳。也可以忽略其他流量,随时根据心跳间隔发送心跳。

心跳超时值设置为多少合适?
根据实践反馈,对于大多数环境来说,最佳设置范围是5-20秒。小于5秒很可能会引起误报,误报原因通常为瞬时的网路拥堵或者短时间的服务流控制等等,一般会马上恢复,我们不能将这些情况视为服务不可用。
listeners.tcp.default = 5672 对外通信端口配置
num_acceptors.tcp = 10  接受tcp连接的erlang进程数。这个值是在客户端设的,公式如下:最大连接数=connection_max +num_acceptors -1

AMQP有两种处理方式:

  • 自动确认模式(automatic acknowledgement model):当RabbbitMQ将消息发送给应用后,消费者端自动回送一个确认消息,此时RabbitMQ删除此消息。
  • 显式确认模式(explicit acknowledgement model):消费者收到消息后,可以在执行一些逻辑后,消费者自己决定什么时候发送确认回执(acknowledgement),RabbitMQ收到回执后才删除消息,这样就保证消费端不会丢失消息

拒绝消息

当消费者接收到某条消息后,处理过程有可能失败,这时消费者可以拒绝此消息。在拒绝消息时,消费者会告诉RabbitMQ如何处理这条消息:销毁它或者重新放入队列。 可以有两种方式拒绝此消息

  • channel.basicReject:只支持对一条消息进行拒绝
  • channel.basicNack 提供一次对多条消息进行拒绝的功能 

生产者消息类型

在AMQP模型中,Exchange是接受生产者消息并将消息路由到消息队列的关键组件。RabbitMQ常用的Exchange Type有fanout、direct、topic、headers这四种,下面分别进行介绍。

生产者在发送消息时,都需要指定一个RoutingKey和Exchange,Exchange在接到该RoutingKey以后,会判断该ExchangeType:

Direct Exchange

处理路由键。需要将一个队列绑定到交换机上,要求该消息与一个特定的路由键完全匹配。这是一个完整的匹配。如果一个队列绑定到该交换机上要求路由键 “abc”,则只有被标记为“abc”的消息才被转发,不会转发abc.def,也不会转发dog.ghi,只会转发abc。

Fanout Exchange

不处理路由键。你只需要简单的将队列绑定到交换机上。一个发送到交换机的消息都会被转发到与该交换机绑定的所有队列上。很像子网广播,每台子网内的主机都获得了一份复制的消息。Fanout交换机转发消息是最快的。

Topic Exchange

将路由键和某模式进行匹配。此时队列需要绑定要一个模式上。符号“#”匹配一个或多个词(#只能写在.号左右,且不能挨着字符),符号“*”匹配一个词。因此“abc.#”能够匹配到“abc.def.ghi”,但是“abc.*” 只会匹配到“abc.def”。

Headers Exchanges

不处理路由键。而是根据发送的消息内容中的headers属性进行匹配。在绑定Queue与Exchange时指定一组键值对;当消息发送到RabbitMQ时会取到该消息的headers与Exchange绑定时指定的键值对进行匹配;如果完全匹配则消息会路由到该队列,否则不会路由到该队列。headers属性是一个键值对,可以是Hashtable,键值对的值可以是任何类型。而fanout,direct,topic 的路由键都需要要字符串形式的。

匹配规则x-match有下列两种类型:

  • x-match = all   :表示所有的键值对都匹配才能转发到消息
  • x-match = any :表示只要有键值对匹配就能转发消息

1:Binding的时候,至少需要指定两个参数,其中的一个是x-match = all 或者x-match = any

2:Binding的时候,不需要指定Routing key

3:发送消息的时候,不需要指定Routing key

4:转发消息的时候,忽略Routing key

5:如果是x-match = all则发送的headers不能比Binding的参数少,否则匹配不上

直接将消息转发到所有Binding的对应的Queue中这种Exchange在路由转发的时候,忽略Routing key这种Exchange效率最高Fanout > Direct > Topic。Topic Exchange可以实现Direct Exchange,Fanout Exchange的效果

消费消息有2种方式:

a)  一种是通过basic.consume命令,订阅某一个队列中的消息,channel会自动在处理完上一条消息之后,接收下一条消息。(同一个channel消息处理是串行的)。除非关闭channel或者取消订阅,否则客户端将会一直接收队列的消息。 

b)  另外一种方式是通过basic.get命令主动获取队列中的消息,但是绝对不可以通过循环调用basic.get来代替basic.consume,这是因为basic.get RabbitMQ在实际执行的时候,是首先consume某一个队列,然后检索第一条消息,然后再取消订阅。如果是高吞吐率的消费者,最好还是建议使用basic.consume。

while ($envelope = $queue->get(AMQP_NOPARAM)) {
    $msg = $envelope->getBody();
    if (preg_match("/.*?消息2.*?/", $msg)) {
        // 对消息2执行确定响应
        $queue->ack($envelope->getDeliveryTag());
    } else {  //拒绝消息
        $queue->nack($envelope->getDeliveryTag());
    }
}

消息分配策略

如果有多个消费者同时订阅同一个队列的话,RabbitMQ是采用循环的方式分发消息的,每一条消息只能被一个订阅者接收。例如,有队列Queue,其中ClientA和ClientB都Consume了该队列,MessageA到达队列后,被分派到ClientA,ClientA服务器收到响应,服务器删除MessageA;再有一条消息MessageB抵达队列,服务器根据“循环推送”原则,将消息会发给ClientB,然后收到ClientB的确认后,删除MessageB;等到再下一条消息时,服务器会再将消息发送给ClientA。

消息由两部分组成: 

payload and  label."payload"是实际要传输的数据,至于数据的格式RabbitMQ并不关心,"label"描述payload,包括exchange name 和可选的topic tag.消息一旦到了consumer那里就只有payload部分了,label部分并没有带过来.RabbitMQ并不告诉你消息是谁发出的.这好比你收到一封信但是信封上是空白的.当然想知道是谁发的还是有办法的,在消息内容中包含发送者的信息就可以了.

消息的consumer和producer对应的概念是sending和receiving并不对应client和server.通过channel我们可以创建很多并行的传输 TCP链接不再成为瓶颈,我们可以把RabbitMQ当做应用程序级别的路由器.

VirtualHost

像mysql服务有数据库的概念并且可以设置用户对库和表等对象的操作权限,RabbitMQ也有类似的权限管理。
在RabbitMQ中可以虚拟消息服务器 VirtualHost,每个VirtualHost 相当于一个相对独立的RabbitMQ服务器,每个VirtualHost之间是相互隔离的。exchange、queue、message不能互通
在 RabbitMQ 中无法通过AMQP创建 VirtualHost,可以通过 RabbitMQ 的WEB管理界面创建,也可以通过下面的命令行创建:

Socket error: could not connect to host.

没有开启远程访问权限

Library error: connection closed unexpectedly - Potential login failure.

要开启AMQP端口监听,在配置文件/etc/rabbitmq/rabbitmq.config中把 {rabbit, [{{tcp_listeners, [5672]},前面的%%去掉,后面','和去掉,重启服务后,通过命令

sudo rabbitmqctl environment

查看系统默认设置.检查是否生效

什么情况下会造成大量消息积压?

可能消费者突然挂了我们没有发现,可能消费者处理消费能力出现问题,与生产者产生消息速度不匹配。

如何解决?

1、积压消息太多,恢复速度后消费速度慢,效率低下,那我们就要增加消费速度了。这时候可以申请资源新建一个topic增加partition分区,比如原来是3个,那我们就增加到30个,并且建立30个消费者去同时消费,那速度自然就增加了10倍。原来的3个就需要将原本的数据也写入新的topic中。等到积压消息处理完了,再改回原本的消费者中。

2、如果用了rabbitMQ,并且设置了超时时间,消息积压的时候就有可能会出现超时丢失的情况。一般情况下生产环境是不会设置超时时间的,但是万一设置了出现这种情况,我们首先是要排除问题保证后续消息处理正常,不会再造成丢失。然后等过了数据高峰期再写个临时程序,将丢失的数据一点点查询出来,重新写入topic中。

3、如果长时间没有消费导致磁盘满了怎么办,如果不能临时扩容,那么只有两种方案,要么将消息扔掉等过了高峰期走上面的第二种方案,去补偿数据;要么新建立一个消费者,将消息快速消费存入一个新的地方。

其实这种情况说白了,并没有一种高深的技术性方案,就是靠临时方案去解决,只能我们尽量避免这种情况出现。

SpringBoot集成多个RabbitMq

引入依赖

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
            <version>2.5.6</version>
        </dependency>
</dependencies>

SpringBoot配置文件

下面新增了enable属性来控制启用

spring:
  rabbitmq:
    mq1:
      host: 127.0.0.1
      port: 5672
      username: admin
      password: ****
      enable: false ##队列是否启用,在Configuration中来确认是否初始化MQ
    mq2:
      host: 127.0.0.2
      port: 5672
      username: admin
      password: ***
      enable: false

配置类(多个类似,只写一个)

import lombok.Data;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;



@Data
@Component("mq1RabbitmqConfig")
@ConfigurationProperties(prefix = "spring.rabbitmq.mq1") //读取mq1的配置信息
@ConditionalOnProperty(name = "spring.rabbitmq.mq1.enable", havingValue = "true") //是否启用
public class Mq1RabbitConfig {

  private String host;
  private Integer port;
  private String username;
  private String password;
  private String virtualHost;

  /**
   * 命名mq1的ConnectionFactory,如果项目中只有一个mq则不必如此
   *
   * @return
   */
  @Bean(value = "mq1ConnectionFactory")
  public ConnectionFactory createConnectionFactory() {
    CachingConnectionFactory connectionFactory = new CachingConnectionFactory();
    connectionFactory.setHost(host);
    connectionFactory.setPort(port);
    connectionFactory.setUsername(username);
    connectionFactory.setPassword(password);
    connectionFactory.setVirtualHost(virtualHost);
    return connectionFactory;
  }

  /**
   * 命名mq1的RabbitTemplate,如果项目中只有一个mq则不必如此
   *
   * @param connectionFactory
   * @return
   */
  @Bean(name = "mq1RabbitTemplate")
  public RabbitTemplate brainRabbitTemplate(
      @Qualifier("mq1ConnectionFactory") ConnectionFactory connectionFactory) {
    RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
    rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
    return rabbitTemplate;
  }

  /**
   * 命名mq1的RabbitListenerContainerFactory,如果项目中只有一个mq则不必如此
   *
   * @param connectionFactory
   * @return
   */
  @Bean("mq1RabbitListenerContainerFactory")
  public SimpleRabbitListenerContainerFactory mq1RabbitListenerContainerFactory(
      @Qualifier("mq1ConnectionFactory") ConnectionFactory connectionFactory) {
    SimpleRabbitListenerContainerFactory listenerContainerFactory = new SimpleRabbitListenerContainerFactory();
    listenerContainerFactory.setConnectionFactory(connectionFactory);
    //--加上这句
    listenerContainerFactory.setMessageConverter(new VosMessageConverter());
    return listenerContainerFactory;
  }
}

生产者Producer

import com.alibaba.fastjson.JSON;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;

@Component
@ConditionalOnProperty(name = "spring.rabbitmq.mq1.enable", havingValue = "true") //是否启用
public class Mq1Producer {

  /**
   * 初始化mq1的RabbitTemplate对象,如果项目中只有一个MQ,则无需这么麻烦
   */
  @Resource(name = "mq1RabbitTemplate")
  private RabbitTemplate mq1RabbitTemplate;

  public void sendMessage(Object message) {
    mq1RabbitTemplate.convertAndSend("fanout.exchange", null, JSON.toJSONString(message));
  }
}

消费者Consumer

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.stereotype.Component;

@Slf4j
@Component
@ConditionalOnExpression(value = "${spring.rabbitmq.mq1.enable:true}") //mq1队列启用才初始化
public class Mq1RabbitListener {

  @RabbitListener(queues = "test", containerFactory = "mq1RabbitListenerContainerFactory")
  public void winChanCdr(String messsageBody) {
    log.info("成功消费消息");
  }
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值