RabbitMQ 消息中间件

学习笔记

Channel
网络信道,几乎所有操作都在Channel中进行,Channel是进行消息读写的通道。客户端可建立多个Channel,每个Channel代表个会话。

Message
消息,服务器和应用程序之间传送的数据,由Properties和Body组成。Properties可以对消息进行修饰,比如消息的优先级、延迟等高级特性;Body则是消息体内容,即我们要传输的数据。

ConnectionFactory
Connection的制造工厂

Connection
仅仅创建了客户端到Broker之间的连接后,客户端还是不能发送消息的。需要为每一个Connection创建Channel,AMQP协议规定只有通过Channel才能执行AMQP的命令。一个Connection可以包含多个Channel。之所以需要Channel,是因为TCP连接的建立和释放都是十分昂贵的,如果一个客户端每一个线程都需要与Broker交互,如果每一个线程都建立一个TCP连接,暂且不考虑TCP连接是否浪费,就算操作系统也无法承受每秒建立如此多的TCP连接。RabbitMQ建议客户端线程之间不要共用Channel,至少要保证共用Channel的线程发送消息必须是串行的,但是建议尽量共用Connection。

Virtual Host
虚拟地址,是一个逻辑概念,用于进行逻辑隔离,是最上层的消息路由。一个Virtual Host里面可以有若干个Exchange和Queue,同一个Virtual Host里面不能有相同名称的Exchange或者Queue;
Virtual Host是权限控制的最小粒度;

Exchange
交换机,用于接收消息,可根据路由键将消息转发到绑定的队列;

Binding:
Exchange和Queue之间的虚拟连接,Exchange在与多个Message Queue发生Binding后会生成一张路由表,路由表中存储着Message Queue所需消息的限制条件即Binding Key。当Exchange收到Message时会解析其Header得到Routing Key,Exchange根据Routing Key与Exchange Type将Message路由到Message Queue。Binding Key由Consumer在Binding Exchange与Message Queue时指定,而Routing Key由Producer发送Message时指定,两者的匹配方式由Exchange Type决定

Routing Key
一个路由规则,虚拟机可用它来确定如何路由一个特定的消息;

Queue
也称作Message Queue,即消息队列,用于保存消息并将他们转发给消费者;

生产者消息投递过程

1.生产者连接到Broker 建立一个连接,然后开启一个信道

2.接着生产者声明一个交换器 ,并设置相关属性,比如交换机类型、是否持久化、是否自动删除、是否内置等

3.生产者声明一个队列井设置相关属性,比如是否排他、是否持久化、是否自动删除、消息最大过期时间、消息最大长度、消息最大字节数等

4.生产者通过路由键将交换器和队列绑定起来

5.生产者发送消息至Broker ,发送的消息包含消息体和含有路由键、交换器、优先级、是否持久化、过期时间、延时时间等信息的标签

6.相应的交换器根据接收到的路由键查找相匹配的队列如果找到 ,则将从生产者发送过来的消息存入相应的队列中

7.如果没有找到 ,则根据生产者配置的属性选择丢弃还是回退给生产者

8.关闭信道

9.关闭连接

在这里插入图片描述

https://www.rabbitmq.com/getstarted.html

依赖(生产端和消费端依赖是一样的)

 <dependencies>
        <!-- https://mvnrepository.com/artifact/com.rabbitmq/amqp-client -->
        <dependency>
            <groupId>com.rabbitmq</groupId>
            <artifactId>amqp-client</artifactId>
            <version>5.7.3</version>
        </dependency>
    </dependencies>

入门案例 :Hello实例

生产端

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
/**
 * 生产端
 */
public class Send {
    private final static String QUEUE_NAME = "hello";

    public static void main(String[] argv) throws Exception {
        //创建一个Connection的制造工厂ConnectionFactory
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        //通过ConnectionFactory创建一个Connection;与Broker建立连接
        try (Connection connection = factory.newConnection();
             //通过Connection创建信道
             Channel channel = connection.createChannel()) {
            /**
             *  Queue.DeclareOk queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete,
             *                                  Map<String, Object> arguments) throws IOException;
             *  参数解析:
             *  1.queue:队列名字
             *  2.durable:RabbitMQ默认将消息存储在内存中,若RabbitMQ宕机,那么整个队列会丢失.可以把这个属性设置成true,表示这个队列需要做持久化.
             *  这个属性只是声明队列是持久化的,RabbitMQ宕机或者重启之后,队列依然存在,但是里面的消息没有持久化,也会丢失.所以需要针对消息也做持久化.
             *  channel.basicPublish("",QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN,msg.getBytes());
             *  3.exclusive:如果你想创建一个只有自己可见的队列,即不允许其它用户访问,RabbitMQ允许你将一个Queue声明成为排他性的(Exclusive Queue)。
             *  该队列的特点是:
             * 	    1>只对首次声明它的连接(Connection)可见
             * 	    2>会在其连接断开的时候自动删除。
             * 	 4.autoDelete:当所有消费客户端连接断开后,是否自动删除队列 true:删除false:不删除
             * 	 5.Map<String, Object> arguments 如果你想额外传递一些参数可以使用该参数
             */
            //声明队列
            channel.queueDeclare(QUEUE_NAME, false, false, false, null);
            String message = "Hello World!";
            /**
             *   void basicPublish(String exchange, String routingKey, boolean mandatory, boolean immediate, BasicProperties props, byte[] body)
             *             throws IOException;
             * 1.routingKey:路由键,#匹配0个或多个单词,*匹配一个单词,在topic exchange做消息转发用
             * 2.mandatory:true:如果exchange根据自身类型和消息routeKey无法找到一个符合条件的queue,那么会调用basic.return方法将消息返还给生产者。false:出现上述情形broker会直接将消息扔掉
             * 3.immediate:true:如果exchange在将消息route到queue(s)时发现对应的queue上没有消费者,那么这条消息不会放入队列中。当与消息routeKey关联的所有queue(一个或多个)都没有消费者时,该消息会通过basic.return方法返还给生产者。
             * 4.BasicProperties :需要注意的是BasicProperties.deliveryMode,0:不持久化 1:持久化 这里指的是消息的持久化,配合channel(durable=true),queue(durable)可以实现,即使服务器宕机,消息仍然保留
             * 5.简单来说:mandatory标志告诉服务器至少将该消息route到一个队列中,否则将消息返还给生产者;immediate标志告诉服务器如果该消息关联的queue上有消费者,则马上将消息投递给它,如果所有queue都没有消费者,直接把消息返还给生产者,不用将消息入队列等待消费
             */
            channel.basicPublish("", QUEUE_NAME, null, message.getBytes("UTF-8"));
            System.out.println(" [x] Sent '" + message + "'");
        }
    }
}

消费端

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;

/**
 * 消费端
 */
public class Recv {
    private final static String QUEUE_NAME = "hello";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");

        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println(" [x] Received '" + message + "'");
        };
        /**
         * 创建 一个消费者,这个消费者从QUEUE_NAME队列中取信息 然后进行消费。
         * String basicConsume(String queue, boolean autoAck, Consumer callback) throws IOException;
         * 参数解析:
         * 1.queue:队列名称,必须与生产端一致
         * 2.autoAck:是否自动ack,如果不自动ack,需要使用channel.ack、channel.nack、channel.basicReject 进行消息应答,
         * 设置为true 会导致 如果处理消息过程中,消费者出现了异常或者关闭了,消息会丢失
         * ACK (Acknowledge character)即是确认字符,在数据通信中,接收站发给发送站的一种传输类控制字符。表示发来的数据已确认接收无误。
         */
        channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> { });
    }
}

先启动消费端在启动生产端,观察控制台输出.

自动签收:channel.basicConsume(QUEUE_NAME, true, deliverCallback,
consumerTag -> { });

手动签收:channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
channel.basicConsume(QUEUE_NAME, false, deliverCallback, consumerTag
-> { }); 处理时有异常: channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false,
true);

Work模式
参与角色: 一个消息生产者P,一个消息存储队列Q,多个消息消费者C

能够较好的解决资源密集型场景的问题,不需要像Hello World那样孤注一掷的等唯一的消费者消费完

特点:

默认情况下:
1:平均分配消息
2:采用轮询方式分配消息

修改:

fetchCount = 1, 设置为1, 启动多劳多得模式

channel.basicQos(1)
每个消费者默认预取1存在本地, 处理完后再去队列中获取
图示:
在这里插入图片描述

worker实例

生产端

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.MessageProperties;
public class NewTask {
    private static final String TASK_QUEUE_NAME = "task_queue";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel()) {
            channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);

            String message = "taskqueue";
            //通过循环遍历,发布20条消息
            for(int i=0;i<20;i++){
                channel.basicPublish("", TASK_QUEUE_NAME,
                        MessageProperties.PERSISTENT_TEXT_PLAIN,
                        (message+i).getBytes("UTF-8"));
            }

            System.out.println(" [x] Sent '" + message + "'");
        }
    }
}

消费端 Worker1

public class Worker1 {
    private static final String TASK_QUEUE_NAME = "task_queue";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        final Connection connection = factory.newConnection();
        final Channel channel = connection.createChannel();

        channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);
        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");

        //开启能者多劳模式
        channel.basicQos(1);

        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            try{
                TimeUnit.SECONDS.sleep(1);
            }catch (InterruptedException e){
                e.printStackTrace();
            }finally{
                channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
            }
            System.out.println(" [x] Received '" + message + "'");
        };
        channel.basicConsume(TASK_QUEUE_NAME, false, deliverCallback, consumerTag -> { });
    }
   }

消费端 Worker2


import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;

import java.util.concurrent.TimeUnit;

public class Worker2 {
    private static final String TASK_QUEUE_NAME = "task_queue";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        final Connection connection = factory.newConnection();
        final Channel channel = connection.createChannel();

        channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);
        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");

        //开启能者多劳模式
        channel.basicQos(1);

        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
            }
            System.out.println(" [x] Received '" + message + "'");
        };
        channel.basicConsume(TASK_QUEUE_NAME, false, deliverCallback, consumerTag -> { });
    }
}

在上面的两个消费端中,worker1设置睡1s,worker2设置睡2秒,用于表示处理生产端信息的能力。并都开启了能者多劳的模式。能者多劳模式,即处理能力强的处理更多的生产端发布的信息请求。
现启动两个消费端;再启动生产端,观察控制台输出。
结果:明显只睡一秒的能消费更多。如果移除channel.basicQos(1)这行代码,即关闭能者多劳模式,则默认是使用轮循模式,观察控制台可发现两个消费端处理的一样多。

Pub/Sub模式
发布订阅模式:

参与角色: 一个消息生产者P,一个交换机x,多个消息队列Q,多个消息消费者C

特点:
1:一个消费者绑定一个消息队列
2:交换机将消息分别发送到消费者绑定的消息队列中, 每个队列都能接收到消息

图解:
在这里插入图片描述

交换机
交换机类型:

fanout:
不处理路由键。你只需要简单的将队列绑定到交换机上。一个发送到交换机的消息都会被转发到与该交换机绑定的所有队列上。

direct:
处理路由键。需要将一个队列绑定到交换机上,要求该消息与一个特定的路由键完全匹配

topic:
将路由键和某模式进行匹配。此时队列需要绑定要一个模式上。符号“#”匹配一个或多个词,符号“”匹配不多不少一个词

headers:
不处理路由键。而是根据发送的消息内容中的headers属性进行匹配。在绑定Queue与Exchange时指定一组键值对;当消息发送到RabbitMQ时会取到该消息的headers与Exchange绑定时指定的键值对进行匹配;如果完全匹配则消息会路由到该队列,否则不会路由到该队列。

示例

生产者


import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;


public class EmitLog {
    private static final String EXCHANGE_NAME = "logs";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel()) {
            /**
             *   可参考:https://blog.csdn.net/message_lx/article/details/77584771
             *   Exchange.DeclareOk exchangeDeclare(String exchange,
             *                                               String type,
             *                                               boolean durable,
             *                                               boolean autoDelete,
             *                                               boolean internal,
             *                                               Map<String, Object> arguments) throws IOException;
             *  1.exchange: 交换机名称
             *  2.type: 交换器的类型,常见的有direct,fanout,topic等
             *   1.>Direct Exchange 根据route key 直接找到队列
             *   2.>Topic Exchange 根据route key 匹配队列
             *   3.>fanout Exchange 不处理route key 全网发送,所有绑定的队列都发送
             *  3.durable :设置是否持久化。durable设置为true时表示持久化,反之非持久化.持久化可以将交换器存入磁盘,在服务器重启的时候不会丢失相关信息。
             *  4.autoDelete:设置是否自动删除。autoDelete设置为true时,则表示自动删除。自动删除的前提是至少有一个队列或者交换器与这个交换器绑定,之后,
             *  所有与这个交换器绑定的队列或者交换器都与此解绑。不能错误的理解—当与此交换器连接的客户端都断开连接时,RabbitMq会自动删除本交换器
             *  5.internal:设置是否内置的。如果设置为true,则表示是内置的交换器,客户端程序无法直接发送消息到这个交换器中,只能通过交换器路由到交换器这种方式。
             *  6.arguments:其它一些结构化的参数,比如:alternate-exchange
             */
            channel.exchangeDeclare(EXCHANGE_NAME, "fanout");

            String message = "info: Hello World!";

            //对于交换机,消息,队列的持久化可参考 https://blog.csdn.net/chenxyt/article/details/79236556
            channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes("UTF-8"));
            System.out.println(" [x] Sent '" + message + "'");
        }
    }

}

消费者


import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;


public class ReceiveLogs {
    private static final String EXCHANGE_NAME = "logs";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        //
        channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
        String queueName = channel.queueDeclare().getQueue();
        channel.queueBind(queueName, EXCHANGE_NAME, "");

        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");

        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println(" [x] Received '" + message + "'");
        };
        channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {
        });
    }
}

现启动消费者在启动生产端,观察控制台输出

Routing模式
路由模式:

参与角色: 一个消息生产者P,一个交换机x,多个消息队列Q,多个消息消费者C

特点:
1:一个消费者绑定一个消息队列
2:每个消息队列绑定一个或者多个特定的路由key(特定的字符串)
3:生产者生产消息时,给消息绑定一个特定路由key
4:交换机识别消息的路由key,并与消息队列绑定的路由key配对,能配对成功(完全匹配)才推送消息

交换机类型:
direct:
处理路由键。需要将一个队列绑定到交换机上,要求该消息与一个特定的路由键完全匹配

图解:
在这里插入图片描述
由图可知,当消息到交换机中后,交换机根据不同队列能接受那些类型的消息进行分发,与上一种模式相比,Pub/Sub模式不会进行选择性分发消息给不通的队列。

实例

生产者:

import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;


public class EmitLogDirect {
    private static final String EXCHANGE_NAME = "direct_logs";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel()) {
            channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);

            String rkey = "info";
            String message = "directMsg";

            channel.basicPublish(EXCHANGE_NAME, rkey, null, message.getBytes("UTF-8"));
            System.out.println(" [x] Sent '" + rkey + "':'" + message + "'");
        }
    }
}

消费者 ReceiveLogsDirect

import com.rabbitmq.client.*;


public class ReceiveLogsDirect {
    private static final String EXCHANGE_NAME = "direct_logs";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
        String queueName = channel.queueDeclare().getQueue();

		//队列绑定到交换机
        channel.queueBind(queueName, EXCHANGE_NAME, "info");
        channel.queueBind(queueName, EXCHANGE_NAME, "error");
        channel.queueBind(queueName, EXCHANGE_NAME, "warning");
        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");

        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println(" [x] Received '" + delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
        };
        channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {
        });
    }
}

消费者 ReceiveLogsDirect1


import com.rabbitmq.client.*;


public class ReceiveLogsDirect {
    private static final String EXCHANGE_NAME = "direct_logs";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
        String queueName = channel.queueDeclare().getQueue();

        //绑定队列到交换机
        channel.queueBind(queueName, EXCHANGE_NAME, "error");

        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");

        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println(" [x] Received '" + delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
        };
        channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {
        });
    }
}

先启动消费者,再启动生产者,观察控制台,由于上产者发送的是"info" 的消息,可以发现只有 绑定了info类型的交换机的消费者能接收到消息,channel.queueBind(queueName, EXCHANGE_NAME, “info”)。

Topic模式
主题模式:

参与角色: 一个消息生产者P,一个交换机x,多个消息队列Q,多个消息消费者C

图解:
在这里插入图片描述

特点:
1:一个消费者绑定一个消息队列
2:每个消息队列绑定一个或者多个特定的路由key匹配规则(带有正则规则的字符串)
3:生产者生产消息时,给消息绑定一个特定路由key
4:交换机识别消息的路由key,并与消息队列绑定的路由key匹配规则进行匹配,匹配成功(符合规则)才推送消息

交换机类型:
topic:
将路由键和某模式进行匹配。此时队列需要绑定要一个模式上。符号“#”匹配一个或多个词,符号“”匹配不多不少一个词。

生产者

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;


public class EmitLogTopic {
    private static final String EXCHANGE_NAME = "topic_logs";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel()) {

            channel.exchangeDeclare(EXCHANGE_NAME, "topic");

		   //关键代码
            String routingKey = "order.save";
            String message = "topicMsg";

            channel.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes("UTF-8"));
            System.out.println(" [x] Sent '" + routingKey + "':'" + message + "'");
        }
    }
}

消费者

public class ReceiveLogsTopic {
    private static final String EXCHANGE_NAME = "topic_logs";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        channel.exchangeDeclare(EXCHANGE_NAME, "topic");
        String queueName = channel.queueDeclare().getQueue();

	    //关键代码
        channel.queueBind(queueName, EXCHANGE_NAME, "order.*");

        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");

        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println(" [x] Received '" + delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
        };
        channel.basicConsume(queueName, true, deliverCallback, consumerTag -> { });
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值