Java小白学习指南【day55】---通俗易懂RabbitMQ消息队列

RabbitMQ消息队列

一、基本概念

官方地址:http://www.rabbitmq.com/

1、使用场景

  • 提高系统响应速度

任务异步处理。将不需要同步处理的并且耗时长的操作由消息队列通知消息接收方进行异步处理。提高了应用程序的响应时间。

  • 提高系统稳定性

系统挂了关系,操作内容放到消息队列。

  • 服务调用异步化

服务没有直接的调用关系,而是通过队列进行服务通信

  • 服务解耦

应用程序解耦合 MQ相当于一个中介,生产方通过MQ与消费方交互,它将应用程序进行解耦合。

  • 排序保证FIFO

遵循队列先进先出的特点

  • 消除峰值

异步化提速(发消息),提高系统稳定性(多系统调用),服务解耦(5-10个服务),排序保证,消除峰值

2、执行流程图

image-20210105170103637

3、常见的MQ

ActiveMQ,RabbitMQ,ZeroMQ,Kafka,MetaMQ,RocketMQ、Redis。

使用RabbitMQ的原因:

1、使得简单,功能强大。

2、基于AMQP协议。

3、社区活跃,文档完善。

4、高并发性能好,这主要得益于Erlang语言。

5、Spring Boot默认已集成RabbitMQ

二、RabbitMQ安装

RabbitMQ需要安装Erlang/OTP,并保持版本匹配,如下图:

image-20210105170447925

本项目使用Erlang/OTP 20.3版本和RabbitMQ3.7.3版本

1、下载erlang

地址:http://erlang.org/download/otp_win64_20.3.exe

需要配置环境变量:ERLANG_HOME(软件路径),在path添加路径(bin路径)

2、下载RabbitMQ

下载地址:https://github.com/rabbitmq/rabbitmq-server/releases/tag/v3.7.3

安装rabbitMQ的管理插件,方便在浏览器端管理RabbitMQ ,进入到RabbitMQ的sbin目录,使用cmd执行命令:rabbitmq-plugins.bat enable rabbitmq_management,安装成功后重新RabbitMQ

登录RabbitMQ

进入浏览器,输入:http://localhost:15672 ,初始账号和密码:guest/guest

3、RabbitMQ的工作原理

image-20210105200853460

Broker:消息队列服务进程,此进程包括两个部分:Exchange和Queue。

Exchange:消息队列交换机,按一定的规则将消息路由转发到某个队列,对消息进行过虑。

Queue:消息队列,存储消息的队列,消息到达队列并转发给指定的消费方。

Producer:消息生产者,即生产方客户端,生产方客户端将消息发送到MQ。

Consumer:消息消费者,即消费方客户端,接收MQ转发的消息。

三、Hello RabbitMQ

image-20210106091924643

1、导入依赖

创建普通的Maven项目,导入需要的rabbitmq的依赖:

<dependencies>
    <!-- https://mvnrepository.com/artifact/com.rabbitmq/amqp-client -->
    <dependency>
        <groupId>com.rabbitmq</groupId>
        <artifactId>amqp-client</artifactId>
        <!--和springboot2.0.5对应-->
        <version>5.4.1</version>
    </dependency>
    </dependencies>

2、创建连接工具

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

public class ConnectionUtil {
    /**
     * 建立与RabbitMQ的连接
     * @return
     * @throws Exception
     */
    public static Connection getConnection() throws Exception {
        //定义连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        //设置服务地址
        factory.setHost("127.0.0.1");
        //端口
        factory.setPort(5672);
        //设置账号信息,用户名、密码、vhost
        factory.setVirtualHost("/");
        factory.setUsername("guest");
        factory.setPassword("guest");
        // 通过工程获取连接
        Connection connection = factory.newConnection();
        return connection;
    }
}

3、创建生产者

步骤:

  • 创建生产者
  • 创建连接
  • 创建队列
  • 发送消息(使用默认交换机)
public class Sender {
    //队列命名
    public static final String NAME_EXCHANGE_HELLO = "name_exchange_hello";
    public static void main(String[] args) {
        Connection connection = null;
        Channel channel = null;
        //1、创建生产者
        try {
            connection = ConnectionUtil.getConnection();
            //2、创建连接
            channel = connection.createChannel();
            //3、创建队列
            channel.queueDeclare(NAME_EXCHANGE_HELLO, //队列名
                    true,//是否持久化
                    false,//是否独占此通道
                    false,//队列不使用时是否自动删除
                    null);//其余参数
            //4、发送消息(使用默认交换机)
            channel.basicPublish("",
                    NAME_EXCHANGE_HELLO, //routingKey消息的路由key,简单理解使用的队列名
                    null, //其余配置
                    "hello rabbitMQ!!".getBytes());//发送的消息
            System.out.println("生产者已就绪");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //释放资源
            try {
                channel.close();
                connection.close();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (TimeoutException e) {
                e.printStackTrace();
            }
        }
    }
}

4、创建消费者

  • 创建消费者
  • 创建连接
  • 监听队列
  • 消费消息
public class Receiver {
    public static void main(String[] args) {
        //1、创建生产者
        try {
            Connection connection = ConnectionUtil.getConnection();
            //2、创建连接
            Channel channel = connection.createChannel();
            //3、监听队列
            DefaultConsumer callback = new DefaultConsumer(channel) {
                @Override
                public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                    super.handleDelivery(consumerTag, envelope, properties, body);
                    System.out.println("接收消息=======================>");
                    System.out.println("消息ID:"+envelope.getDeliveryTag());
                    System.out.println("交换机:"+envelope.getExchange());
                    System.out.println("消费者标签:"+consumerTag);
                    System.out.println("接收到的消息:"+new String(body));
                    System.out.println("<=======================接收完毕");
                }
            };
            //4、消费消息
            channel.basicConsume(Sender.NAME_EXCHANGE_HELLO,//队列名
                    true, //是否自动签收
                    callback);//消费者接收到消息后会调用此函数
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

**注意:**自动签收存在一定问题,如果消费者在接收消息的过程中出现问题,没有接收成功,或者接收了部分,但是因为设置的是自动签收,消息也没有在队列中。此时就造成了消息的丢失,针对这种情况,我们根据消息的重要程度,应当使用手动签收。

image-20210106063916192

四、Work queues

image-20210106091901198

只是比上述Hello程序多一个接收者,同时可以对消费者处理消息的能力进行设置,“能者多劳”!

public class Receiver2 {

    public static void main(String[] args) {
        //1、创建生产者
        try {
            Connection connection = ConnectionUtil.getConnection();
            //2、创建连接
            Channel channel = connection.createChannel();
            //这是消费者同时处理消息能力
            channel.basicQos(1);
            //3、监听队列
            DefaultConsumer callback = new DefaultConsumer(channel) {
                @Override
                public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                    super.handleDelivery(consumerTag, envelope, properties, body);
                    try {
                        //模拟处理消息所用时间
                        Thread.sleep(20000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("接收消息=======================>");
                    System.out.println("消息ID:"+envelope.getDeliveryTag());
                    System.out.println("交换机:"+envelope.getExchange());
                    System.out.println("消费者标签:"+consumerTag);
                    System.out.println("接收到的消息:"+new String(body));
                    System.out.println("<=======================接收完毕");
                    channel.basicAck(envelope.getDeliveryTag(), true);
                }
            };
            //4、消费消息
            channel.basicConsume(Sender.NAME_EXCHANGE_HELLO,//队列名
                    false, //是否自动签收
                    callback);//消费者接收到消息后会调用此函数
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

五、订阅模式

1、Fanout广播

image-20210106091942223

将消息交给所有绑定到交换机的队列,不需要指定路由键routingkey。

生产者:

  • 创建生产者
  • 创建通道
  • 创建交换机
  • 发送消息
public class Sender {
    //队列命名
    public static final String NAME_EXCHANGE_FANOUT = "name_exchange_fanout";
    public static void main(String[] args) {
        Connection connection = null;
        Channel channel = null;
        //1、创建生产者
        try {
            connection = ConnectionUtil.getConnection();
            //2、创建连接
            channel = connection.createChannel();
            //3、创建交换机,不创建队列
            channel.exchangeDeclare(NAME_EXCHANGE_FANOUT,
                    BuiltinExchangeType.FANOUT,
                    true);
            //4、发送消息(使用默认交换机)
            channel.basicPublish(NAME_EXCHANGE_FANOUT,//交换机
                    "", //routingKey消息的路由key,简单理解使用的队列名
                    null, //其余配置
                    "这里是广播模式!!".getBytes());//发送的消息
            System.out.println("生产者已就绪");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //释放资源
            try {
                channel.close();
                connection.close();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (TimeoutException e) {
                e.printStackTrace();
            }
        }
    }
}

消费者:

  • 创建消费者
  • 创建通道
  • 创建队列
  • 绑定到交换机(不创建路由键)
  • 监听队列
  • 消费消息
public class Receiver {
    public static final String NAME_QUEUE_FANOUT1 = "name_queue_fanout1";

    public static void main(String[] args) throws Exception {
        //1、创建生产者
        Connection connection = ConnectionUtil.getConnection();
        //2、创建连接
        Channel channel = connection.createChannel();
        channel.basicQos(1);
        //3、创建队列
        channel.queueDeclare(NAME_QUEUE_FANOUT1,
                true, //是否持久化
                false, //是否独占通道
                false, //是否自动删除队列
                null);
        //4、绑定到交换机
        channel.queueBind(NAME_QUEUE_FANOUT1,
                Sender.NAME_EXCHANGE_FANOUT,
                "");//广播模式不需要rountingkey
        //5、监听队列
        DefaultConsumer callback = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                super.handleDelivery(consumerTag, envelope, properties, body);
                System.out.println("接收消息=======================>");
                System.out.println("消息ID:" + envelope.getDeliveryTag());
                System.out.println("交换机:" + envelope.getExchange());
                System.out.println("消费者标签:" + consumerTag);
                System.out.println("接收到的消息:" + new String(body));
                System.out.println("<=======================接收完毕");
                channel.basicAck(envelope.getDeliveryTag(), true);
            }
        };
        //6、消费消息
        channel.basicConsume(NAME_QUEUE_FANOUT1,//队列名
                false, //是否自动签收
                callback);//消费者接收到消息后会调用此函数
    }
}

可以设置两个队列进行测试

2、Direct定向

在某些场景下,我们希望不同的消息被不同的队列消费。这时就要用到Direct类型的Exchange。在Direct模型下,队列与交换机的绑定,不能是任意绑定了,而是要指定一个RoutingKey(路由key)

消息的发送方在向Exchange发送消息时,也必须指定消息的routing key。

image-20210106214556235

生产者:

  • 创建生产者
  • 创建通道
  • 创建交换机
  • 发送消息(需要指定路由键)
public class Sender {
    //队列命名
    public static final String NAME_EXCHANGE_DIRECT = "name_exchange_direct";
    public static void main(String[] args) {
        Connection connection = null;
        Channel channel = null;
        //1、创建生产者
        try {
            connection = ConnectionUtil.getConnection();
            //2、创建连接
            channel = connection.createChannel();
            //3、创建交换机,不创建队列
            channel.exchangeDeclare(NAME_EXCHANGE_DIRECT,
                    BuiltinExchangeType.DIRECT,//订阅模式
                    true);
            //4、发送消息(使用默认交换机)
            channel.basicPublish(NAME_EXCHANGE_DIRECT,
                    "student", //routingKey消息的路由key,简单理解使用的队列名
                    null, //其余配置
                    "这里是定向模式!!".getBytes());//发送的消息
            System.out.println("生产者已就绪");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //释放资源
            try {
                channel.close();
                connection.close();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (TimeoutException e) {
                e.printStackTrace();
            }
        }
    }
}

消费者:

  • 创建消费者
  • 创建通道
  • 创建队列
  • 绑定到交换机(要指定路由键)
  • 监听队列
  • 消费消息
public class Receiver2 {
    public static final String NAME_QUEUE_DIRECT2 = "name_queue_direct2";
    public static void main(String[] args) throws Exception {
        //1、创建生产者
            Connection connection = ConnectionUtil.getConnection();
            //2、创建连接
            Channel channel = connection.createChannel();
            //3、创建队列
            channel.queueDeclare(NAME_QUEUE_DIRECT2,
                    true, //是否持久化
                    false, //是否独占通道
                    false, //是否自动删除队列
                    null);
            //4、绑定到交换机
            channel.queueBind(NAME_QUEUE_DIRECT2,
                    Sender.NAME_EXCHANGE_DIRECT,
                    "student");//路由键
            //5、监听队列
            DefaultConsumer callback = new DefaultConsumer(channel) {
                @Override
                public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                    super.handleDelivery(consumerTag, envelope, properties, body);
                    System.out.println("接收消息=======================>");
                    System.out.println("消息ID:"+envelope.getDeliveryTag());
                    System.out.println("交换机:"+envelope.getExchange());
                    System.out.println("消费者标签:"+consumerTag);
                    System.out.println("接收到的消息:"+new String(body));
                    System.out.println("<=======================接收完毕");
                    channel.basicAck(envelope.getDeliveryTag(), true);
                }
            };
            //6、消费消息
            channel.basicConsume(NAME_QUEUE_DIRECT2,//队列名
                    false, //是否自动签收
                    callback);//消费者接收到消息后会调用此函数
    }
}

可以创建两个消费者,使用不同的路由键routingKey进行对比

3、Topic通配符

与direct模式类似,生产者只需要将类型改变为TOPIC,不同之处是在路由键routingKey中可以使用通配符

  • #:匹配一个或多个词

  • *:匹配一个词

六、持久化

1、交换机持久化

image-20210106220550369

2、队列持久化

image-20210106220608170

3、消息持久化

image-20210106220723864

七、Springboot整合Rabbitmq

新建Maven项目

1、集成MQ

导入依赖

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.5.RELEASE</version>
    </parent>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>

        <!--spirngboot集成rabbitmq-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
    </dependencies>

配置yml

server:
  port: 44000
spring:
  application:
    name: test-rabbitmq-producer
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest
    virtualHost: /
    listener:
      simple:
        acknowledge-mode: manual #手动签收
        prefetch: 1
    publisher-confirms: true #消息由生产者发送到交换机成功、失败回调,成功与失败都会进行回调
    # 消息由交换机发送到队列失败回调,只是在失败的时候执行回调,若消息都未执行到交换机就失败,则不会执行该回调
    publisher-returns: true
    template:
      mandatory: true # 必须设置成true 消息路由失败通知监听者,而不是将消息丢弃

2、MQ配置

  • 配置交换机
  • 配置队列
  • 配置绑定
public class MQConfig {

    //交换机名
    public static final String NAME_EXCHANGE_DIRECT = "name_exchange_direct";
    //队列名
    public static final String NAME_QUEUE_DIRECT = "name_queue_direct";
    //路由键
    public static final String NAME_ROUTING_KEY_DIRECT = "name_routing_key_direct";

    //创建交换机
    @Bean
    public Exchange creatExchange(){
        return ExchangeBuilder.directExchange(NAME_EXCHANGE_DIRECT).durable(true).build();
    }
    //创建队列
    @Bean
    public Queue creatQueue(){
        return new Queue(NAME_QUEUE_DIRECT, true);
    }
    //绑定交换机
    @Bean
    public Binding creatBinding(){
        return BindingBuilder.bind(creatQueue()).to(creatExchange())
                .with(NAME_ROUTING_KEY_DIRECT).noargs();
    }
}

3、发送消息

  • 创建一个测试类
  • 注入RabbitTemplate
  • 发送消息(可以发送对象,注意对象需要进行序列化!!!
@RunWith(SpringRunner.class)
@SpringBootTest(classes = RabbitMQApp.class)
public class Sender {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void testSender(){
        //发送消息
        rabbitTemplate.convertAndSend(NAME_EXCHANGE_SPRINGBOOT, //交换机
                NAME_ROUTING_KEY_DIRECT, //路由键
                new Student("张三", 18, true));
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

4、消费消息

  • 编写一个类交给spring管理
  • 编写消费方法
  • 消费方法的参数(载荷即消息内容,通道,消息抽象对象)
  • 手动签收
@Component
public class Consumer {
    //监听队列
    @RabbitListener(queues = {NAME_QUEUE_SPRINGBOOT})//可以监听多个队列
    public void getMessage(@Payload Student student, Message message,Channel channel){
        System.out.println("消息内容为:"+student);
        //消息ID
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        System.out.println("消息ID:"+deliveryTag);
        ack(channel, deliveryTag);
    }

    /**
     * 手动签收
     * @param channel
     * @param deliveryTag
     */
    private void ack(Channel channel, long deliveryTag) {
        //手动签收
        try {
            channel.basicAck(deliveryTag,
                    false);//是否接受多个消息
        } catch (IOException e) {
            e.printStackTrace();
            try {
                channel.basicNack(deliveryTag, false,
                        true);//是否拒绝签收后退回到队列
            } catch (IOException e1) {
                e1.printStackTrace();
            }
        }
    }
}

5、消息投递失败处理

  • yml进行配置

  • 编写回调处理器

    • 客户端投递到交换机回调(成功失败均会回调)
    • 交换机到队列回调(失败会回调)
  • 消息失败的处理方案

    • 重试

    • 保存日志

    • 发送报警邮件或者短信

    • 持久发送消息到DB结合定时任务不停重复发送,设置最大重复发送次数

回调处理器

@Component//交给spring管理
public class RabbitMQCallback implements RabbitTemplate.ConfirmCallback ,RabbitTemplate.ReturnCallback {

    /**
     * 消息从客户端投递到交换机的回调,不论成功与失败,都会执行
     * @param correlationData
     * @param ack
     * @param cause
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        System.out.println("这里是客户端投递到交换机的回调");
        System.out.println("correlationData:"+correlationData);
        System.out.println("是否投递成功ack:"+ack);
        System.out.println("原因cause:"+cause);

    }

    /**
     * 消息从交换机到队列的回调,失败才会执行,如果客户端投递到交换机就失败了,呢么这个回调也不会执行
     * @param message 返回的信息
     * @param replyCode 回复码
     * @param replyText 失败原因
     * @param exchange 那台交换机
     * @param routingKey 路由键
     */
    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
        System.out.println("这里是消息从交换机到队列的回调");
        System.out.println("返回的信息message"+message);
        System.out.println("回复码replyCode:"+replyCode);
        System.out.println("失败原因replyText:"+replyText);
        System.out.println("交换机exchange:"+exchange);
        System.out.println("路由键routingKey:"+routingKey);
    }
}

需要将回调处理器注入进生产者中

.....
    @Autowired
    private RabbitMQCallback rabbitMQCallback;
    @Test
    public void testSender(){
        //设置消息回调
        //客户端到交换机回调
        rabbitTemplate.setConfirmCallback(rabbitMQCallback);
        //交换机到队列回调
        rabbitTemplate.setReturnCallback(rabbitMQCallback);
.....

消息发送失败处理方式

  • 在yml中进行配置,输出日志信息

    • logging:
        file: logs/errorMQ.txt #日志保存路径
      
  • 在回调处理器中加入日志信息

@Component//交给spring管理
public class RabbitMQCallback implements RabbitTemplate.ConfirmCallback ,RabbitTemplate.ReturnCallback {
    //定义失败后重复发送次数
    int reNum = 1;
    //获取日志信息
    private Logger logger = LoggerFactory.getLogger(RabbitMQCallback.class);
    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 消息从客户端投递到交换机的回调,不论成功与失败,都会执行
     * @param correlationData
     * @param ack
     * @param cause
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
		........
        logger.info("消息是否投递成功:{},原因是:{}",ack,cause);
    }

    /**
     * 消息从交换机到队列的回调,失败才会执行,如果客户端投递到交换机就失败了,呢么这个回调也不会执行
     * @param message 返回的信息
     * @param replyCode 回复码
     * @param replyText 失败原因
     * @param exchange 那台交换机
     * @param routingKey 路由键
     */
    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
		........
        logger.info("消息投递失败信息:{},原因是:{},交换机为:{},路由键是:{},失败的信息为:{}",
                replyCode,replyText,exchange,routingKey,message);
        //失败后尝试重复发送
        if (reNum>0){
            rabbitTemplate.convertAndSend(exchange, routingKey, message);
            reNum--;
        }
    }
}

6、投递JSON消息

  • 配置RabbitTemplate
  • 配置RabbitListenerContainerFactory
  • 消费者标签@RabbitListener增加属性containerFactory(需要把之前传递的对象的序列化实现去掉,用我们自己设定的序列化

MQConfig

    /**
     * JSON序列化,指定JSON转换器
     * @param connectionFactory
     * @return
     */
    @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
        return rabbitTemplate;
    }

    /**
     * 定义一个监听的工厂,指定JSON转换器,反序列化
     * @param connectionFactory
     * @return
     */
    @Bean("rabbitListenerContainerFactory")
    public RabbitListenerContainerFactory<?> rabbitListenerContainerFactory(ConnectionFactory connectionFactory){
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory);
        factory.setMessageConverter(new Jackson2JsonMessageConverter());
        factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
        factory.setPrefetchCount(1);
        return factory;
    }

image-20210107110652475

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值