Spring Boot学习(五):消息(RabbitMQ)与检索(ElasticSearch)

十、Spring Boot与消息

消息简介:

(1)大多数应用中,可通过消息服务中间件来提升系统异步通信、扩展解耦能力。

(2)消息服务中两个重要概念:

消息代理(message broker)目的地(destination)

当消息发送者发送消息以后,将由消息代理接管,消息代理保证消息传递到指定目的地。

(3)消息队列主要有两种形式的目的地:

队列(queue):点对点消息通信(point-to-point)

主题(topic):发布(publish)/订阅(subscribe)消息通信

(4)消息代理规范

  • JMS(Java Message Service)JAVA消息服务

    基于JVM消息代理的规范,ActiveMQ、HornetMQ是JMS实现

  • AMQP(Advanced Message Queuing Protocal)

    高级消息队列协议,也是一个消息代理的规范,兼容JMS

    RabbitMQ是AMQP的实现

    作用:

    通过消息服务中间件来提升系统异步通信、扩展解耦能力

    当消息发送者发送消息以后,将由消息代理接管,消息代理保证消息传递到指定目的地

    应用场景:

    1、异步处理:用户注册操作和消息处理并行,提高响应速度。

    原始:

​ 改进:

​ 现在:

​ 2、应用解耦:在下单时库存系统不能正常使用。也不影响正常下单,因为下单后,订单系统写入消息队列就不再关心其它的后续操作,实现订单系统和库存系统的应用解耦。

​ 过去:

​ 现在:

​ 3、流量削峰:用户的请求,服务器接收后,首先写入消息队列。假如消息队列长度超过最大数量,则直接抛弃用户请求或跳转到错误页面。 秒杀业务根据消息队列中的请求信息,再做后续处理。

JMS和AMQP的对比:

JMSAMQP
定义Java api网络线级协议
跨语言
跨平台
Model提供两种消息模型: (1)、Peer-2-Peer (2)、Pub/sub提供了五种消息模型:(1)、direct exchange (2)、fanout exchange (3)、topic change (4)、headers exchange (5)、system exchange 本质来讲,后四种和JMS的pub/sub模型没有太大差别,仅是在路由机制上做了更详细的划分;
支持消息类型多种消息类型: TextMessage MapMessage BytesMessage StreamMessage ObjectMessage Message(只有消息头和属性)byte[]当实际应用时,有复杂的消息,可以将消息序列化后发送。
综合评价JMS 定义了JAVA API层面的标准;在java体系中,多个client均可以通过JMS进行交互,不需要应用修改代码,但是其对跨平台的支持较差;AMQP定义了wire-level层的协议标准;天然具有跨平台、跨语言特性。

(5)点对点式

  • 消息发送者发送消息,消息代理将其放入一个队列中,消息接收者从队列中获取消息内容,消息读取后被移除队列。
  • 消息只有唯一的发送者和接受者,但是并不只有一个接收者。

(6)发布订阅者模式

  • 发送者(发布者)发送消息到主题,多个接收者(订阅者)监听(订阅)这个主题,那么就会在消息到达时同时收到消息

RabbitMQ

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

(1)核心概念

  • Message

消息,消息是不具名的,它由消息头和消息体组成。

消息头,包括routing-key(路由键)、priority(相对于其他消息的优先权)、deliverty-mode(指出该消息可能需要持久性存储)

  • Publisher

消息的生产者,也是一个向交换器发布消息的客户端应用程序。

  • Exchange

交换器,将生产者消息路由给服务器中的队列

类型有direct(默认),fanout, topic, 和headers,具有不同转发策略

  • Queue

消息队列,保存消息直到发送给消费者

  • Binding

绑定,用于消息队列和交换器之间的关联

  • Connection

网络连接,比如一个TCP连接

  • Consumer

消息的消费者,表示一个从消息队列中取得消息的客户端应用程序

  • Virtual Host

虚拟主机,表示一批交换器、消息队列和相关对象。
vhost 是 AMQP 概念的基础,必须在连接时指定
RabbitMQ 默认的 vhost 是 /

  • Broker

消息队列服务器实体

运行机制

1、消息路由

AMQP 中增加了Exchange 和 Binding 的角色,Binding 决定交换器的消息应该发送到那个队列。

2、Exchange类型

  1. direct

    点对点模式,消息中的路由键(routing key)如果和 Binding 中的 binding
    key 一致,交换器就将消息发到对应的队列中。

  2. fanout

    广播模式,每个发到 fanout 类型交换器的消息都会分到所有绑定的队列上去

  3. topic

    将路由键和某个模式进行匹配,此时队列需要绑定到一个模式上。它将路由键和绑定键的字符串切分成单词,这些单词之间用点隔开。
    识别通配符:#匹配0个或多个单词,*匹配一个单词

SpringBoot中的RabbitMQ

1. 环境准备

在docker中安装rabbitmq并运行

# 5672为服务端口,15672为web控制台端口
docker run -d -p 5672:5672 -p 15672:15672 38e57f281891

导入依赖

   <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-amqp</artifactId>
   </dependency>
<!--自定义消息转化器Jackson2JsonMessageConverter所需依赖-->
   <dependency>
       <groupId>com.fasterxml.jackson.core</groupId>
       <artifactId>jackson-databind</artifactId>
   </dependency>

配置文件

# 指定rebbitmq服务器主机
spring.rabbitmq.host=192.168.31.162
#spring.rabbitmq.username=guest  默认值为guest
#spring.rabbitmq.password=guest	 默认值为guest

2. RabbitMQ的使用

RabbitAutoConfiguration中有内部类RabbitTemplateConfiguration,在该类中向容器中分别导入了RabbitTemplateAmqpAdmin

在测试类中分别注入

@Autowired
   private RabbitTemplate rabbitTemplate;

   @Autowired
   private AmqpAdmin amqpAdmin;
  • RabbitTemplate消息发送处理组件

    可用来发送和接收消息

//发送消息
rabbitTemplate.convertAndSend("amq.direct","ustc","aaaa");
      Book book = new Book();
      book.setName("西游记");
      book.setPrice(23.2f);
//Book要实现Serializable接口
      rabbitTemplate.convertAndSend("amq.direct","ustc",book);

//接收消息
Object o = rabbitTemplate.receiveAndConvert("ustc");
      System.out.println(o.getClass());  //class cn.edu.ustc.springboot.bean.Book
      System.out.println(o);			//Book{name='西游记', price=23.2}

默认的消息转化器是SimpleMessageConverter,对于对象以jdk序列化方式存储,若要以Json方式存储对象,就要自定义消息转换器

@Configuration
public class AmqpConfig {
    @Bean
    public MessageConverter messageConverter() {
        //在容器中导入Json的消息转换器
        return new Jackson2JsonMessageConverter();
    }
}
  • AmqpAdmin管理组件

    可用于创建和删除exchange、binding和queue

//创建Direct类型的Exchange
amqpAdmin.declareExchange(new DirectExchange("admin.direct"));
//创建Queue
amqpAdmin.declareQueue(new Queue("admin.test"));
//将创建的队列与Exchange绑定
amqpAdmin.declareBinding(new Binding("admin.test", Binding.DestinationType.QUEUE,"admin.direct","admin.test",null));

消息的监听

在回调方法上标注@RabbitListener注解,并设置其属性queues,注册监听队列,当该队列收到消息时,标注方法遍会调用

可分别使用Message和保存消息所属对象进行消息接收,若使用Object对象进行消息接收,实际上接收到的也是Message

@Service
public class BookService {
    @RabbitListener(queues = {"admin.test"})
    public void receive1(Book book){
        System.out.println("收到消息:"+book);
    }

    @RabbitListener(queues = {"admin.test"})
    public void receive1(Object object){
        System.out.println("收到消息:"+object.getClass());
        //收到消息:class org.springframework.amqp.core.Message
    }
    
    @RabbitListener(queues = {"admin.test"})
    public void receive2(Message message){
        System.out.println("收到消息"+message.getHeaders()+"---"+message.getPayload());
    }
}

十一、Spring Boot与检索

检索简介

我们的应用经常需要添加检索功能,开源的ElasticSearch是目前全文搜索引擎的首选。他可以快速的存储、搜索和分析海量数
据。Spring Boot通过整合Spring Data ElasticSearch为我们提供了非常便捷的检索功能支持;
Elasticsearch是一个分布式搜索服务,提供Restful API,底层基于Lucene,采用多shard(分片)的方式保证数据安全,并
且提供自动resharding的功能,github等大型的站点也是采用了ElasticSearch作为其搜索服务。

概念

员工文档 的形式存储为例:一个文档代表一个员工数据。存储数据到ElasticSearch 的行为叫做 索引 ,但在索引一个文档之前,需要确定将文档存储在哪里。一个ElasticSearch集群可以包含多个索引,相应的每个索引可以包含多个类型。这些不同的类型存储着多个文,每个文档又有多个属性

索引(名词):

如前所述,一个索引类似于传统关系数据库中的一个数据库,是一个存储关系型文档的地方。索引(index)的复数词为indices
或indexes。
索引(动词):
索引一个文档就是存储一个文档到一个索引(名词)中以便被检索和查询。这非常类似于SQL语句中的INSERT关键词,除了文档
已存在时,新文档会替换旧文档情况之外。

类似关系:

索引---数据库
类型---表
文档---表中的记录
属性---列

ES的安装与运行

与ES交互

  • 9200端口

    RESTful API通过HTTP通信

  • 9300端口

    Java客户端与ES的原生传输协议和集群交互

# 拉取ES镜像
docker pull elasticsearch:7.6.1
#运行ES
docker run -e "discovery.type=single-node" -e ES_JAVA_OPTS="-Xms256m -Xmx256m" -d -p 9200:9200 -p 9300:9300 --name ES03 41072cdeebc5

ES_JAVA_OPTS指定java虚拟机相关参数

-Xms256m 初始堆内存大小为256m

-Xmx256m 最大堆内存大小为256m

discovery.type=single-node 设置为单点启动

ES的基础入门

案例:创建一个员工目录,并支持各类型检索

索引员工文档

对于员工目录,我们将做如下操作:

  • 每个员工索引一个文档,文档包含该员工的所有信息。
  • 每个文档都将是 employee 类型
  • 该类型位于 索引 megacorp 内。
PUT /megacorp/employee/1
{
    "first_name" : "John",
    "last_name" :  "Smith",
    "age" :        25,
    "about" :      "I love to go rock climbing",
    "interests": [ "sports", "music" ]
}

注意,路径 /megacorp/employee/1 包含了三部分的信息:

  • megacorp

    索引名称

  • employee

    类型名称

  • 1

    特定雇员的ID

请求体 —— JSON 文档 —— 包含了这位员工的所有详细信息

{
    "_index": "megacorp",
    "_type": "employee",
    "_id": "1",
    "_version": 1,
    "result": "created",
    "_shards": {
        "total": 2,
        "successful": 1,
        "failed": 0
    },
    "_seq_no": 0,
    "_primary_term": 1
}

同理,添加更多员工

PUT /megacorp/employee/2
{
    "first_name" :  "Jane",
    "last_name" :   "Smith",
    "age" :         32,
    "about" :       "I like to collect rock albums",
    "interests":  [ "music" ]
}

PUT /megacorp/employee/3
{
    "first_name" :  "Douglas",
    "last_name" :   "Fir",
    "age" :         35,
    "about":        "I like to build cabinets",
    "interests":  [ "forestry" ]
}

检索文档

HTTP GET 请求并指定文档的地址——索引库、类型和ID。

GET /megacorp/employee/1

返回数据

{
    "_index": "megacorp",
    "_type": "employee",
    "_id": "1",
    "_version": 1,
    "_seq_no": 0,
    "_primary_term": 1,
    "found": true,
    "_source": {
        "first_name": "John",
        "last_name": "Smith",
        "age": 25,
        "about": "I love to go rock climbing",
        "interests": [
            "sports",
            "music"
        ]
    }
}

将 HTTP 命令由 PUT 改为 GET 可以用来检索文档,同样的,可以使用 DELETE 命令来删除文档,以及使用 HEAD 指令来检查文档是否存在。如果想更新已存在的文档,只需再次 PUT

轻量搜索

搜索所有雇员:

GET /megacorp/employee/_search

返回数据

{
    "took": 46,
    "timed_out": false,
    "_shards": {
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": {
            "value": 3,
            "relation": "eq"
        },
        "max_score": 1,
        "hits": [
            {
                "_index": "megacorp",
                "_type": "employee",
                "_id": "1",
                "_score": 1,
                "_source": {
                    "first_name": "John",
                    "last_name": "Smith",
                    "age": 25,
                    "about": "I love to go rock climbing",
                    "interests": [
                        "sports",
                        "music"
                    ]
                }
            },
            {
                "_index": "megacorp",
                "_type": "employee",
                "_id": "2",
                "_score": 1,
                "_source": {
                    "first_name": "Jane",
                    "last_name": "Smith",
                    "age": 32,
                    "about": "I like to collect rock albums",
                    "interests": [
                        "music"
                    ]
                }
            },
            {
                "_index": "megacorp",
                "_type": "employee",
                "_id": "3",
                "_score": 1,
                "_source": {
                    "first_name": "Douglas",
                    "last_name": "Fir",
                    "age": 35,
                    "about": "I like to build cabinets",
                    "interests": [
                        "forestry"
                    ]
                }
            }
        ]
    }
}

返回结果包括三个文档,放在数据hits中。

搜索姓氏为 Smith 的雇员

GET /megacorp/employee/_search?q=last_name:Smith

在请求路径中使用 _search 端点,并将查询本身赋值给参数 q= 。返回结果给出了所有的 Smith:

{
    "took": 23,
    "timed_out": false,
    "_shards": {
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": {
            "value": 2,
            "relation": "eq"
        },
        "max_score": 0.4700036,
        "hits": [
            {
                "_index": "megacorp",
                "_type": "employee",
                "_id": "1",
                "_score": 0.4700036,
                "_source": {
                    "first_name": "John",
                    "last_name": "Smith",
                    "age": 25,
                    "about": "I love to go rock climbing",
                    "interests": [
                        "sports",
                        "music"
                    ]
                }
            },
            {
                "_index": "megacorp",
                "_type": "employee",
                "_id": "2",
                "_score": 0.4700036,
                "_source": {
                    "first_name": "Jane",
                    "last_name": "Smith",
                    "age": 32,
                    "about": "I like to collect rock albums",
                    "interests": [
                        "music"
                    ]
                }
            }
        ]
    }
}

使用查询表达式搜索

Query-string 搜索通过命令非常方便地进行临时性的即席搜索 ,但它有自身的局限性。Elasticsearch 提供一个丰富灵活的查询语言叫做 查询表达式 , 它支持构建更加复杂和健壮的查询。

GET /megacorp/employee/_search
{
    "query" : {
        "match" : {
            "last_name" : "Smith"
        }
    }
}

返回效果与之前一样

更复杂的搜索

同样搜索姓氏为 Smith 的员工,但这次我们只需要年龄大于 30 的

GET /megacorp/employee/_search
{
    "query" : {
        "bool": {
            "must": {
                "match" : {
                    "last_name" : "smith" 
                }
            },
            "filter": {
                "range" : {
                    "age" : { "gt" : 30 } 
                }
            }
        }
    }
}

全文搜索

搜索下所有喜欢攀岩(rock climbing)的员工:

GET /megacorp/employee/_search
{
    "query" : {
        "match" : {
            "about" : "rock climbing"
        }
    }
}

返回结果

{
    "took": 13,
    "timed_out": false,
    "_shards": {
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": {
            "value": 2,
            "relation": "eq"
        },
        "max_score": 1.4167401,
        "hits": [
            {
                "_index": "megacorp",
                "_type": "employee",
                "_id": "1",
                "_score": 1.4167401,
                "_source": {
                    "first_name": "John",
                    "last_name": "Smith",
                    "age": 25,
                    "about": "I love to go rock climbing",
                    "interests": [
                        "sports",
                        "music"
                    ]
                }
            },
            {
                "_index": "megacorp",
                "_type": "employee",
                "_id": "2",
                "_score": 0.4589591,
                "_source": {
                    "first_name": "Jane",
                    "last_name": "Smith",
                    "age": 32,
                    "about": "I like to collect rock albums",
                    "interests": [
                        "music"
                    ]
                }
            }
        ]
    }
}

可以看到返回结果还带有相关性得分_score

短语搜索

精确匹配一系列单词或者短语 。 比如, 执行这样一个查询,短语 “rock climbing” 的形式紧挨着的雇员记录。

为此对 match 查询稍作调整,使用一个叫做 match_phrase 的查询

GET /megacorp/employee/_search
{
    "query" : {
        "match_phrase" : {
            "about" : "rock climbing"
        }
    }
}

高亮搜索

每个搜索结果中 高亮 部分文本片段

再次执行前面的查询,并增加一个新的 highlight 参数:

GET /megacorp/employee/_search
{
    "query" : {
        "match_phrase" : {
            "about" : "rock climbing"
        }
    },
    "highlight": {
        "fields" : {
            "about" : {}
        }
    }
}

返回结果

{
		...
    
        "hits": [
            {
                "_index": "megacorp",
                "_type": "employee",
                "_id": "1",
                "_score": 1.4167401,
                "_source": {
                    "first_name": "John",
                    "last_name": "Smith",
                    "age": 25,
                    "about": "I love to go rock climbing",
                    "interests": [
                        "sports",
                        "music"
                    ]
                },
                "highlight": {
                    "about": [
                        "I love to go <em>rock</em> <em>climbing</em>"
                    ]
                }
            }
        ]
    }
}

结果中还多了一个叫做 highlight 的部分。这个部分包含了 about 属性匹配的文本片段,并以 HTML 标签 <em> 封装

Springboot整合ElasticSearch

1. 概述

SpringBoot默认支持两种技术来和ES交互;

  • Jest(默认不生效)

    • 需要导入jest的工具包(io.searchbox.client.JestClient)
    • 从springboot 2.2.0以后被弃用
  • SpringData ElasticSearch

    版本适配说明

Spring Data ElasticsearchElasticsearch
3.2.x6.8.1
3.1.x6.2.2
3.0.x5.5.0
2.1.x2.4.0
2.0.x2.2.0
1.3.x1.5.2

Springboot 2.2.6对应于 Spring Data Elasticsearch 3.2.6,即适配Elasticsearch 6.8.1

2. 环境搭建

编写文件对应Javabean,指定索引名和类型

@Document(indexName = "ustc",type = "book")
public class Book {
    private Integer id;
    private String bookName;
    private String author;

    public Integer getId() {
        return id;
    }

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

    public String getBookName() {
        return bookName;
    }

    public void setBookName(String bookName) {
        this.bookName = bookName;
    }

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }

    @Override
    public String toString() {
        return "Book{" +
                "id=" + id +
                ", bookName='" + bookName + '\'' +
                ", author='" + author + '\'' +
                '}';
    }
}

3. ElasticSearch客户端

  • Transport Client

    在ES7中已经被弃用,将在ES8被移除

  • High Level REST Client

    ES的默认客户端

  • Reactive Client

    非官方驱动,基于WebClient

下面以REST客户端为例说明ES的使用

配置主机地址

方式一 配置类配置

注意:这种方式底层依赖于Http相关类,因此要导入web相关jar包

@Configuration
static class Config {
  @Bean
  RestHighLevelClient client() {

    ClientConfiguration clientConfiguration = ClientConfiguration.builder() 
      .connectedTo("localhost:9200")
      .build();

    return RestClients.create(clientConfiguration).rest();                  
  }
}

方式二 spring配置文件指定

spring.elasticsearch.rest.uris=http://192.168.31.162:9200

在测试类中注入客户端

@Autowired
RestHighLevelClient highLevelClient;

创建索引

IndexRequest request = new IndexRequest("ustc", "book",
        UUID.randomUUID().toString())
        .source(Collections.singletonMap("feature", "high-level-rest-client"))
        .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE);
IndexResponse index = highLevelClient.index(request, RequestOptions.DEFAULT);
System.out.println(index.toString());

下面为创建索引

{
    "_index": "ustc",
    "_type": "book",
    "_id": "0dc9f47a-7913-481d-a36d-e8f034a6a3ac",
    "_score": 1,
    "_source": {
        "feature": "high-level-rest-client"
    }
}

得到索引

//分别指定要获取的索引、类型、id
GetRequest getRequest = new GetRequest("ustc","book","0dc9f47a-7913-481d-a36d-e8f034a6a3ac");
GetResponse documentFields = highLevelClient.get(getRequest, RequestOptions.DEFAULT);
System.out.println(documentFields);

4. ElasticsearchRestTemplate

ES有两个模板,分别为ElasticsearchRestTemplateElasticsearchTemplate

分别对应于High Level REST ClientTransport Client(弃用),两个模板都实现了ElasticsearchOperations接口,因此使用时我们一般使用ElasticsearchOperations,具体实现方式由底层决定。

由于在AbstractElasticsearchConfiguration中已经向容器中导入了ElasticsearchRestTemplate,因此我们使用时可以直接注入

注入模板

@Autowired
ElasticsearchOperations elasticsearchOperations;

保存索引

Book book = new Book();
book.setAuthor("路遥");
book.setBookName("平凡的世界");
book.setId(1);
IndexQuery indexQuery = new IndexQueryBuilder()
    .withId(book.getId().toString())
    .withObject(book)
    .build();
String index = elasticsearchOperations.index(indexQuery);

查询索引

Book book = elasticsearchOperations.queryForObject(GetQuery.getById("1"), Book.class);

5. Elasticsearch Repositories

编写相关Repository并继承Repository或ElasticsearchRepository,泛型分别为<查询类,主键>

public interface BookRepository extends Repository<Book,Integer> {
    List<Book> findByBookNameAndAuthor(String bookName, String author);
}

查询的方法仅需按照一定规则命名即可实现功能,无需编写实现,如上findByBookNameAndAuthor()方法相当于ES的json查询

{
    "query": {
        "bool" : {
            "must" : [
                { "query_string" : { "query" : "?", "fields" : [ "bookName" ] } },
                { "query_string" : { "query" : "?", "fields" : [ "author" ] } }
            ]
        }
    }
}

@Query

此外,还可以使用@Query自定义请求json

interface BookRepository extends ElasticsearchRepository<Book, String> {
    @Query("{\"match\": {\"name\": {\"query\": \"?0\"}}}")
    Page<Book> findByName(String name,Pageable pageable);
}

若参数为John,相当于请求体为

{
  "query": {
    "match": {
      "name": {
        "query": "John"
      }
    }
  }
}
 Book.class);

5. Elasticsearch Repositories

编写相关Repository并继承Repository或ElasticsearchRepository,泛型分别为<查询类,主键>

public interface BookRepository extends Repository<Book,Integer> {
    List<Book> findByBookNameAndAuthor(String bookName, String author);
}

查询的方法仅需按照一定规则命名即可实现功能,无需编写实现,如上findByBookNameAndAuthor()方法相当于ES的json查询

{
    "query": {
        "bool" : {
            "must" : [
                { "query_string" : { "query" : "?", "fields" : [ "bookName" ] } },
                { "query_string" : { "query" : "?", "fields" : [ "author" ] } }
            ]
        }
    }
}

@Query

此外,还可以使用@Query自定义请求json

interface BookRepository extends ElasticsearchRepository<Book, String> {
    @Query("{\"match\": {\"name\": {\"query\": \"?0\"}}}")
    Page<Book> findByName(String name,Pageable pageable);
}

若参数为John,相当于请求体为

{
  "query": {
    "match": {
      "name": {
        "query": "John"
      }
    }
  }
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值