分布式消息服务中间价——《RabbitMQ》_消息中间价(1),一举拿下腾讯美团滴滴offer

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Golang全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip1024b (备注go)
img

正文

return type;
}
public void setType(String type) {
this.type = type;
}
@Override
public String toString() {
return “KouZhao{” +
“id='” + id + ‘’’ +
“, type='” + type + ‘’’ +
‘}’;
}
}

Producer.java

package com.lagou.demo;
import java.util.UUID;
import java.util.concurrent.BlockingQueue;
public class Producer implements Runnable {
private final BlockingQueue blockingQueue;
public Producer(BlockingQueue blockingQueue) {
this.blockingQueue = blockingQueue;
}
@Override
public void run() {
while (true) {
try {
Thread.sleep(200);
if (blockingQueue.remainingCapacity() > 0) {
KouZhao kouZhao = new
KouZhao(UUID.randomUUID().toString(), “N95”);
blockingQueue.add(kouZhao);
System.out.println(“我在生产口罩,当前库存是:” +
blockingQueue.size());
} else {
System.out.println(“我的仓库已经堆满了” +
blockingQueue.size() + “个口罩,快来买口罩啊!”);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

Consumer.java

package com.lagou.demo;
import java.util.concurrent.BlockingQueue;
public class Consumer implements Runnable {
private final BlockingQueue blockingQueue;
public Consumer(BlockingQueue blockingQueue) {
this.blockingQueue = blockingQueue;
}
@Override
public void run() {
while (true) {
try {
Thread.sleep(100);
long startTime = System.currentTimeMillis(); // 获取开始时间
KouZhao kouZhao = blockingQueue.take();
long endTime = System.currentTimeMillis(); // 获取结束时间
System.out.println(“我消费了口罩:” + kouZhao + “, 等到货时我阻
塞了” + (endTime - startTime) + “ms”);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

app.java

package com.lagou.demo;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class App {
public static void main(String[] args) throws InterruptedException {
BlockingQueue queue = new ArrayBlockingQueue<>(20);
Producer producer = new Producer(queue);
Consumer consumer = new Consumer(queue);
new Thread(producer).start();
Thread.sleep(20000);
new Thread(consumer).start();
}
}

上述代码放到生产环境显然是不行的,比如没有集群,没有分布式,玩儿法太单一,不能满足企业级应用的要求。。。
比如:消息有没有持久化?怎么确定消息一定能发送成功?怎么确定消息一定能被消费成功?高并发下的性能怎么样?系统可靠吗?
有没有Pub/Sub模式?有没有考虑过限流?。。。

主流消息中间件及选型

在传统金融机构、银行、政府机构等有一些老系统还在使用IBM等厂商提供的商用MQ产品。当前业界比较流行的开源消息中间件包括:ActiveMQ、RabbitMQ、RocketMQ、Kafka、ZeroMQ等,其中应用最为广泛的要数RabbitMQRocketMQKafka这三款。Redis在某种程度上也可以是实现类似“Queue”和“Pub/Sub”的机制,严格意义上不算消息中间件。

选取原则

首先,产品应该是开源的。开源意味着如果队列使用中遇到bug,可以很快修改,而不用等待开发者的更新。其次,产品必须是近几年比较流行的,要有一个活跃的社区。这样遇到问题很快就可以找到解决方法。同时流行也意味着bug较少。流行的产品一般跟周边系统兼容性比较好。最后,作为消息队列,要具备以下几个特性:

  1. 消息传输的可靠性:保证消息不会丢失。
  2. 支持集群,包括横向扩展,单点故障都可以解决。
  3. 性能要好,要能够满足业务的性能需求。
RabbitMQ

RabbitMQ开始是用在电信业务的可靠通信的,也是少有的几款支持AMQP协议的产品之一。
优点:

  1. 轻量级,快速,部署使用方便
  2. 支持灵活的路由配置。RabbitMQ中,在生产者和队列之间有一个交换器模块。根据配置的路由规则,生产者发送的消息可以发送到不同的队列中。路由规则很灵活,还可以自己实现。
  3. RabbitMQ的客户端支持大多数的编程语言。

缺点:

  1. 如果有大量消息堆积在队列中,性能会急剧下降
  2. RabbitMQ的性能在Kafka和RocketMQ中是最差的,每秒处理几万到几十万的消息。如果应用要求高的性能,不要选择RabbitMQ。
  3. RabbitMQ是Erlang开发的,功能扩展和二次开发代价很高。
RocketMQ

RocketMQ是一个开源的消息队列,使用java实现。借鉴了Kafka的设计并做了很多改进。RocketMQ主要用于有序,事务,流计算,消息推送,日志流处理,binlog分发等场景。经过了历次的双11考验,性能,稳定性可可靠性没的说。
RocketMQ几乎具备了消息队列应该具备的所有特性和功能。

  • java开发,阅读源代码、扩展、二次开发很方便。
  • 对电商领域的响应延迟做了很多优化。在大多数情况下,响应在毫秒级。如果应用很关注响应时间,可以使用RocketMQ。
  • 性能比RabbitMQ高一个数量级,每秒处理几十万的消息。

缺点:跟周边系统的整合和兼容不是很好。

Kafka

Kafka的可靠性,稳定性和功能特性基本满足大多数的应用场景。跟周边系统的兼容性是数一数二的,尤其是大数据和流计算领域,几乎所有相关的开源软件都支持Kafka。Kafka高效可伸缩消息持久化支持分区副本和容错。Kafka是ScalaJava开发的,对批处理和异步处理做了大量的设计,因此Kafka可以得到非常高的性能。它的异步消息的发送和接收是三个中最好的,但是跟RocketMQ拉不开数量级,每秒处理几十万的消息。如果是异步消息,并且开启了压缩,Kafka最终可以达到每秒处理2000w消息的级别。但是由于是异步的和批处理的,延迟也会高不适合电商场景。

RabbitMQRocketMQKafka
单机吞吐量1w量级10w量级10w量级
开发语言ErlangJavaJava和Scala
消息延迟微妙毫秒毫秒
消息丢失可能性很低参数优化后可以0丢失参数优化后可以0丢失
消费模式推拉推拉拉取
主题数量对吞吐量影响\几百上千个主题会对吞吐量有一个小的影响几十上百个主题会极大影响吞吐量
可用性高(主从)很高(主从)很高(分布式)
消息中间件应用场景

消息中间件的使用场景非常广泛,比如,12306购票的排队锁座,电商秒杀,大数据实时计算等。

电商秒杀案例:
比如6.18,活动从0:00开始,仅限前 200 名,秒杀即将开始时,用户会疯狂刷新 APP或者浏览器来保证自己能够尽早的看到商品。

  • 当秒杀开始前,用户在不断的刷新页面,系统应该如何应对高并发的读请求呢?
  • 在秒杀开始时,大量并发用户瞬间向系统请求生成订单,扣减库存,系统应该如何应对高并发的写请求呢?

系统应该如何应对高并发的读请求

  • 使用缓存策略将请求挡在上层中的缓存中
  • 能静态化的数据尽量做到静态化
  • 加入限流(比如对短时间之内来自某一个用户,某一个IP、某个设备的重复请求做丢弃处理)

系统应该如何应对高并发的写请求
生成订单,扣减库存,用户这些操作不经过缓存直达数据库。如果在 1s内,有 1 万个数据连接同时到达,系统的数据库会濒临崩溃。如何解决这个问题呢?我们可以使用 消息队列
消息队列的作用:

  • 削去秒杀场景下的峰值写流量——流量削峰
  • 通过异步处理简化秒杀请求中的业务流程——异步处理
  • 解耦,实现秒杀系统模块之间松耦合——解耦

削去秒杀场景下的峰值写流量
将秒杀请求暂存于消息队列,业务服务器响应用户“秒杀结果正在处理中。。。”,释放系统资源去处理其它用户的请求。
削峰填谷,削平短暂的流量高峰,消息堆积会造成请求延迟处理,但秒杀用户对于短暂延迟有一定容忍度。

秒杀商品有 1000 件,处理一次购买请求的时间是 500ms,那么总共就需要 500s 的时间。这时你部署 10 个队列处理程序,那么秒杀请求的处理时间就是 50s,也就是说用户需要等待 50s 才可以看到秒杀的结果,这是可以接受的。这时会并发 10 个请求到达数据库,并不会对数据库造成很大的压力。

通过异步处理简化秒杀请求中的业务流程:先处理主要的业务,异步处理次要的业务。如主要流程是生成订单、扣减库存;次要流程比如购买成功之后会给用户发优惠券,增加用户的积分。此时秒杀只要处理生成订单,扣减库存的耗时,发放优惠券、增加用户积分异步去处理了。解耦,实现秒杀系统模块之间松耦合

将秒杀数据同步给数据团队,有两种思路:

  1. 使用 HTTP 或者 RPC 同步调用,即提供一个接口,实时将数据推送给数据服务。系统的耦合度高,如果其中一个服务有问题,可能会导致另一个服务不可用。
  2. 使用消息队列
    将数据全部发送给消息队列,然后数据服务订阅这个消息队列,接收数据进行处理。

拉勾B端C端数据同步案例:

拉勾网站分B端和C端,B端面向企业用户,C端面向求职者。这两个模块业务处理逻辑不同,数据库表结构不同,实际上是处于解耦的状态。但是各自又需要对方的数据,需要共享:如

  1. 当C端求职者在更新简历之后,B端企业用户如何尽早看到该简历更新?
  2. 当B端企业用户发布新的职位需求后,C端用户如何尽早看到该职位信息?

无论是B端还是C端,都有各自的搜索引擎和缓存,B端需要获取C端的更新以更新搜索引擎和缓
存;C端需要获取B端的更新以更新C端的搜索引擎与缓存。

如何解决B端C端数据共享的问题?解决方式:

  1. 同步方式:B端和C端通过RPC或WebService的方式发布服务,让对方来调用,以获取对方的
    信息。求职者每更新一次简历,就调用一次B端的服务,进行数据的同步;B端企业用户每更
    新职位需求,就调用C端的服务,进行数据的同步。
  2. 异步方式:使用消息队列,B端将更新的数据发布到消息队列,C端将更新的数据发布到消息
    队列,B端订阅C端的消息队列,C端订阅B端的消息队列。

使用同步方式,B端和C端耦合比较紧密,如果其中一个服务有问题,可能会导致另一个服务不可用。比如C端的RPC挂掉,企业用户有可能无法发布新的职位信息,因为发布了对方也看不到;B端的RPC挂掉,求职者可能无法更新简历,因为即使简历更新了,对方也看不到。

你可能会想,可以让B端或C端在对方RPC挂掉的时候,先将该通知消息缓存起来,等对方服务恢复之后再进行同步。这正是引入异步方式,使用消息队列的目的。使用消息队列的异步方式,对B端C端进行解耦,只要消息队列可用,双方都可以将需要同步的信息发送到消息队列,对方在收到消息队列推送来的消息的时候,各自更新自己的搜索引擎,更新自己的缓存数据。

支付宝购买电影票
在这里插入图片描述

如上图,用户在支付宝购买了一张电影票后很快就收到消息推送和短信(电影院地址、几号厅、座位号、场次时间等),同时用户会积累一定的会员积分。这里,交易系统并不需要一直等待消息送达等动作都完成后才返回成功,允许一定延迟和瞬时不一致(最终一致性),而且后面两个动作通常可以并发执行。如果后期监控大盘想要获取实时交易数据,只需要新增个消费者程序并订阅该消息即可,交易系统对此并不感知,松耦合。

JMS规范和AMQP协议

RabbitMQ

RabbitMQ架构与实战

RabbitMQ介绍、概念、基础架构
RabbitMQ介绍

RabbitMQ,俗称“兔子MQ”(可见其轻巧,敏捷),是目前非常热门的一款开源消息中间件,不管是互联网行业还是传统行业都广泛使用(最早是为了解决电信行业系统之间的可靠通信而设计)。

  1. 高可靠性、易扩展、高可用、功能丰富
  2. 支持大多数(甚至冷门)的编程语言客户端。
  3. RabbitMQ遵循AMQP协议,自身采用Erlang(一种由爱立信开发的通用面向并发编程的语言)编写。
  4. RabbitMQ也支持MQTT等其他协议。

RabbitMQ具有很强大的插件扩展能力,官方和社区提供了非常丰富的插件可供选择:https://www.rabbitmq.com/community-plugins.html

RabbitMQ整体逻辑架构

在这里插入图片描述

RabbitMQ Exchange类型

RabbitMQ常用的交换器类型有: fanoutdirecttopicheaders 四种。
fanout
会把所有发送到该交换器的消息路由到所有与该交换器绑定的队列中
在这里插入图片描述
Direct
direct类型的交换器路由规则很简单,它会把消息路由到那些BindingKeyRoutingKey完全匹配的队列中,如下图:
在这里插入图片描述
Topic
topic类型的交换器在direct匹配规则上进行了扩展,也是将消息路由到BindingKey和RoutingKey相匹配的队列中,这里的匹配规则稍微不同,它约定:BindingKey和RoutingKey一样都是由"."分隔的字符串;BindingKey中可以存在两种特殊字符*和#,用于模糊匹配,其中*用于匹配一个单词,#用于匹配多个单词(可以是0个)。
在这里插入图片描述
Headers
headers类型的交换器不依赖于路由键的匹配规则来路由信息,而是根据发送的消息内容中的headers属性进行匹配。在绑定队列和交换器时指定一组键值对,当发送的消息到交换器时,RabbitMQ会获取到该消息的headers,对比其中的键值对是否完全匹配队列和交换器绑定时指定的键值对,如果匹配,消息就会路由到该队列。headers类型的交换器性能很差,不实用。

RabbitMQ数据存储

存储机制
RabbitMQ消息有两种类型

  • 持久化消息和非持久化消息
  • 这两种消息都会被写入磁盘

持久化消息在到达队列时写入磁盘,同时会内存中保存一份备份,当内存吃紧时,消息从内存中清除。这会提高一定的性能。
非持久化消息一般只存于内存中,当内存压力大时数据刷盘处理,以节省内存空间。
RabbitMQ存储层包含两个部分:队列索引消息存储
在这里插入图片描述
队列索引:rabbit_queue_index
索引维护队列的落盘消息的信息,如存储地点、是否已被给消费者接收、是否已被消费者ack等。每个队列都有相对应的索引。

索引使用顺序的段文件来存储,后缀为.idx,文件名从0开始累加,每个段文件中包含固定的segment_entry_count 条记录,默认值是16384。每个index从磁盘中读取消息的时候,至少要在内存中维护一个段文件,所以设置 queue_index_embed_msgs_below 值得时候要格外谨慎,一点点增大也可能会引起内存爆炸式增长。

消息存储:rabbit_msg_store
消息以键值对的形式存储到文件中,一个虚拟主机上的所有队列使用同一块存储,每个节点只有一个。存储分为持久化存储(msg_store_persistent)短暂存储(msg_store_transient)。持久化存储的内容在broker重启后不会丢失,短暂存储的内容在broker重启后丢失。

store使用文件来存储,后缀为.rdq,经过store处理的所有消息都会以追加的方式写入到该文件中,当该文件的大小超过指定的限制(file_size_limit)后,将会关闭该文件并创建一个新的文件以供新的消息写入。文件名从0开始进行累加。在进行消息的存储时,RabbitMQ会在ETS(Erlang TermStorage)表中记录消息在文件中的位置映射和文件的相关信息。

消息(包括消息头、消息体、属性)可以直接存储在index中,也可以存储在store中。最佳的方式是较小的消息存在index中,而较大的消息存在store中。这个消息大小的界定可以通过queue_index_embed_msgs_below 来配置,默认值为4096B。当一个消息小于设定的大小阈值时,就可以存储在index中,这样性能上可以得到优化。一个完整的消息大小小于这个值,就放到索引中,否则放到持久化消息文件中.

如果消息小于这个值,就在索引中存储,如果消息大于这个值就在store中存储
大于这个值的消息存储于msg_store_persistent目录中的.rdq文件中
小于这个值的消息存储于.idx索引文件中:

读取消息时,先根据消息的ID(msg_id)找到对应存储的文件,如果文件存在并且未被锁住,则直接打开文件,从指定位置读取消息内容。如果文件不存在或者被锁住了,则发送请求由store进行处理。

删除消息时,只是从ETS表删除指定消息的相关信息,同时更新消息对应的存储文件和相关信息。在执行消息删除操作时,并不立即对文件中的消息进行删除,也就是说消息依然在文件中,仅仅是标记为垃圾数据而已。当一个文件中都是垃圾数据时可以将这个文件删除。当检测到前后两个文件中的有效数据可以合并成一个文件,并且所有的垃圾数据的大小和所有文件(至少有3个文件存在的情况下)的数据大小的比值超过设置的阈值garbage_fraction(默认值0.5)时,才会触发垃圾回收,将这两个文件合并,执行合并的两个文件一定是逻辑上相邻的两个文件。合并逻辑:

  1. 锁定这两个文件
  2. 先整理前面的文件的有效数据,再整理后面的文件的有效数据
  3. 将后面文件的有效数据写入到前面的文件中
  4. 更新消息在ETS表中的记录
  5. 删除后面文件

队列结构
通常队列由rabbit_amqqueue_processbacking_queue这两部分组成,rabbit_amqqueue_process负责协议相关的消息处理,即接收生产者发布的消息、向消费者交付消息、处理消息的确认(包括生产端的confirm和消费端的ack)等。backing_queue是消息存储的具体形式和引擎,并向rabbit_amqqueue_process提供相关的接口以供调用。

如果消息投递的目的队列是空的,并且有消费者订阅了这个队列,那么该消息会直接发送给消费者,不会经过队列这一步。当消息无法直接投递给消费者时,需要暂时将消息存入队列,以便重新投递。
rabbit_variable_queue.erl 源码中定义了RabbitMQ队列的4种状态:

  1. alpha:消息索引和消息内容都存内存,最耗内存,很少消耗CPU
  2. beta:消息索引存内存,消息内存存磁盘
  3. gama:消息索引内存和磁盘都有,消息内容存磁盘
  4. delta:消息索引和内容都存磁盘,基本不消耗内存,消耗更多CPU和I/O操作

消息存入队列后,不是固定不变的,它会随着系统的负载在队列中不断流动,消息的状态会不断发送变化。持久化的消息,索引和内容都必须先保存在磁盘上,才会处于上述状态中的一种gama状态只有持久化消息才会有的状态。

在运行时,RabbitMQ会根据消息传递的速度定期计算一个当前内存中能够保存的最大消息数量(target_ram_count),如果alpha状态的消息数量大于此值,则会引起消息的状态转换,多余的消息可能会转换到beta、gama或者delta状态。区分这4种状态的主要作用是满足不同的内存和CPU需求。

对于普通没有设置优先级和镜像的队列来说,backing_queue的默认实现是rabbit_variable_queue,其内部通过5个子队列Q1、Q2、delta、Q3、Q4来体现消息的各个状态。
在这里插入图片描述
消费者获取消息也会引起消息的状态转换, 当消费者获取消息时

  1. 首先会从Q4中获取消息,如果获取成功则返回。
  2. 如果Q4为空,则尝试从Q3中获取消息,系统首先会判断Q3是否为空,如果为空则返回队列为空,即此时队列中无消息。
  3. 如果Q3不为空,则取出Q3中的消息;进而再判断此时Q3和Delta中的长度,如果都为空,则可以认为 Q2、Delta、 Q3、Q4 全部为空,此时将Q1中的消息直接转移至Q4,下次直接从Q4 中获取消息。
  4. 如果Q3为空,Delta不为空,则将Delta的消息转移至Q3中,下次可以直接从Q3中获取消息。在将消息从Delta转移到Q3的过程中,是按照索引分段读取的,首先读取某一段,然后判断读取的消息的个数与Delta中消息的个数是否相等,如果相等,则可以判定此时Delta中己无消息,则直接将Q2和刚读取到的消息一并放入到Q3中,如果不相等,仅将此次读取到的消息转移到Q3。

其实这一番论述也解释了另一个问题:为什么Q3和Delta都为空时,则可以认为 Q2、Delta、Q3、Q4全部为空?
通常在负载正常时,如果消费速度大于生产速度,对于不需要保证可靠不丢失的消息来说,极有可能只会处于alpha状态。
对于持久化消息,它一定会进入gamma状态,在开启publisher confirm机制时,只有到了gamma 状态时才会确认该消息己被接收,若消息消费速度足够快、内存也充足,这些消息也不会继续走到下一个状态。

为什么消息的堆积导致性能下降?
在系统负载较高时,消息若不能很快被消费掉,这些消息就会进入到很深的队列中去,这样会增加处理每个消息的平均开销。因为要花更多的时间和资源处理“堆积”的消息,如此用来处理新流入的消息的能力就会降低,使得后流入的消息又被积压到很深的队列中,继续增大处理每个消息的平均开销,继而情况变得越来越恶化,使得系统的处理能力大大降低。应对这一问题一般有3种措施:

  1. 增加prefetch_count的值,即一次发送多条消息给消费者,加快消息被消费的速度。
  2. 采用multiple ack,降低处理 ack 带来的开销
  3. 流量控制
RabbitMQ安装和配置

搭建环境

  1. 虚拟机软件:VMWare 15.1.0
  2. 操作系统:CentOS Linux release 7.7.1908
  3. Erlang:erlang-23.0.2-1.el7.x86_64
  4. RabbitMQ:rabbitmq-server-3.8.4-1.el7.noarch

RabbitMQ的安装需要首先安装Erlang,因为它是基于Erlang的VM运行的。RabbitMQ需要的依赖:socat和logrotate,logrotate操作系统中已经存在了,只需要安装socat就可以了。RabbitMQ与Erlang的兼容关系详见:https://www.rabbitmq.com/which-erlang.html

1、安装依赖:

yum install socat -y

2、安装Erlang:

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Go)
img

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
要首先安装Erlang,因为它是基于Erlang的VM运行的。RabbitMQ需要的依赖:socat和logrotate,logrotate操作系统中已经存在了,只需要安装socat就可以了。RabbitMQ与Erlang的兼容关系详见:https://www.rabbitmq.com/which-erlang.html

1、安装依赖:

yum install socat -y

2、安装Erlang:

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Go)
[外链图片转存中…(img-OyeCYJBT-1713467288425)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值