MongoDB一万字精华总结

概述

MongoDB是属于文档型的NoSQL数据库,也就是文档数据库。文档数据库区别于传统的其它数据库,它是用来管理文档。在传统的数据库中,信息被分割成离散的数据段,而在文档数据库中,文档是处理信息的基本单位,
一个文档相当于关系数据库中的一条记录。

数据结构

总体的数据结构与关系型数据库对比表格:

关系型数据库mongodb
database(数据库)database(数据库)
table(表)collection(集合)
row(行)document(文档)
column(列)field(字段)
index(索引)index(索引)

其中document是bson格式的,例子:

{
   field1: value1,
   field2: value2,
   field3: value3,
   ...
   fieldN: valueN
}

其数据结构类型表格(参照官网的):

类型数字别名说明(只解释难理解的)
Double1“double”
String2“string”
Object3“object”这里的数据结构类型,做内嵌文档使用。
Array4“array”
Binary data5“binData”
Undefined6“undefined”Deprecated.
ObjectId7“objectId”用来创建id的对象,存储后显示为ObjectId(“id”)
Boolean8“bool”
Date9“date”
Null10“null”
Regular Expression11“regex”
DBPointer12“dbPointer”Deprecated.
JavaScript13“javascript”
Symbol14“symbol”Deprecated.
JavaScript (with scope)15“javascriptWithScope”
32-bit integer16“int”
Timestamp17“timestamp”
64-bit integer18“long”
Decimal12819“decimal”New in version 3.4.
Min key-1“minKey”
Max key127“maxKey”

存储引擎

存储引擎是数据库的组成部分,负责管理数据在内存和磁盘中的存储方式。
MongoDB支持多个存储引擎,因为不同的引擎对于特定的工作负载表现更好。

MMAPv1

3.0版本以前,MongoDB只有一个存储引擎——MMAP,MongoDB3.0引进了一个新的存储引擎——WiredTiger,同时对原有的MMAP引擎进行改进,产生MMAPv1存储引擎,并将其设置为MongoD3.0的默认存储引擎。然而MMAP引擎的一些弊端在MMAPv1引擎依旧存在,3.2版本开始,MongoDB已将默认的存储引擎设置为WiredTiger,从MongoDB 4.0起已弃用MMAPv1引擎。
作为MongoDB原生的存储引擎,MMAPv1也是有它自己的优势的。MMAPv1基于内存映射文件,它擅长于大容量插入、读取和就地更新的工作负载。

文件结构

使用MMAPv1存储引擎,每个数据库由一个.ns文件和一个或多个数据文件组成,假设数据库名称为mydb,则.ns文件名称为mydb.ns,数据文件名称为:mydb.0,mydb.1,mydb.2…,文件编号从0开始,文件大小从64MB开始,依次倍增,最大为2GB。

.ns文件实际上是一个hash表,用于快速定位某个集合在数据文件中存储的起始位置。每个数据文件被划分成多个extent,每个extent只包含一个集合的数据,同一个集合的所有extent之间使用双向链表连接,一个extent包含多个文档,同一个extent中的所有文档也使用双向链表连接,所以,同一个集合中所有文档是使用双向链表连接的。
.ns文件的数据组织结构如下:
在这里插入图片描述

数据文件的数据组织结构如下:
在这里插入图片描述

内存使用

为了保证连续的存储空间,避免产生磁盘碎片,MMAPv1对数据文件的使用采用预分配策略:数据库创建之后,先创建一个编号为0的文件,大小为64M,当这个文件有一半以上被使用时,再创建一个编号为1的文件,大小是上一个文件的两倍,即128M,依此类推,直到创建文件大小达到2G,以后再创建的文件大小就都是2G了。

使用MMAPv1存储引擎,MongoDB会自动使用所有机器的空闲的内存作为它的cache,即MongoDB会使用尽可能多的空闲的内存。但MongoDB使用的内存由系统资源监视器监视,由系统控制,可随时回收,如果其他的进程突然需要服务器大量的内存,MongoDB将会让出内存给其他的进程。当然,使用MMAPv1存储引擎的时候,分配的内存越大,MongoDB的性能就越好。

MMAPv1存储引擎是在数据库级别分配文件的,将每个数据库中所有的集合和索引都混合存储在数据库文件中,即使删除了某个集合或索引,其占用的磁盘空间也很难及时自动回收。

WiredTiger

WiredTiger是在MongoDB3.0版本引入的,并且在MongoDB3.2版本开始成为MongoDB默认的存储引擎。相比较MMAPv1,WiredTiger功能更强大,而且具有更高的性能。

文件结构

WiredTIger则在集合和索引级别分配文件,将每个数据库中所有的集合和索引都存储在单独的文件中,集合或索引删除后,其对应文件即可删除,磁盘空间回收方便。
WiredTiger的一些数据文件:

  • mongod.lock:用于防止多个进程连接同一个WiredTiger数据库
  • .wt文件:存储各个集合的数据,每个文件100MB
  • WiredTiger.wt:用于存储所有集合的元数据信息
  • WiredTiger.turtle:用于存储WiredTiger.wt的元数据信息
  • journal文件夹:用于存储日志文件(Write ahead log)

WiredTiger存储引擎使用文档级别锁,同一时刻多个写操作可以修改同一个集合中不同的文档,但不能修改同一个文档。

对于大多数读写操作,WiredTiger 使用乐观并发控制。WiredTiger 仅在全局、数据库和集合级别使用意图锁。当存储引擎检测到两个操作之间的冲突时,会引发写入冲突,导致 MongoDB 透明地重试该操作。

一些全局操作,通常是涉及多个数据库的短期操作,仍然需要全局“实例范围”锁。其他一些操作,例如删除集合,仍然需要独占数据库锁。

mvcc

WiredTiger 使用多版本并发控制 (MVCC)。在操作开始时,WiredTiger 会为操作提供数据的时间点快照。快照呈现内存中数据的一致视图。
mvcc会在下面单独作为一个模块来讲。

内存使用

按照MongoDB默认的配置,WiredTiger的写操作会先写入Cache(BTree结构),当Cache大小达到128KB时便将其持久化到预写日志文件(Write ahead log)。WiredTiger每60s或日志文件大小达到2GB时会做一次检查点Checkpoint,产生指定时间点的数据库快照(内存中数据的一致性视图),将快照中的所有数据以一致性方式持久化到数据文件中,保证数据文件和内存数据是一致的。Wiredtiger连接初始化时,首先将数据恢复至最新的快照状态,然后根据预写日志文件恢复数据,以保证存储可靠性。

使用WiredTiger存储引擎时,MongoDB数据缓存分两部分:内部缓存和文件系统缓存。内部缓存大小可以使用–wiredTigerCacheSizeGB参数来设置,默认值为:256MB或(RAM - 1 GB) 的 50%之间,取两值中较大者。文件系统缓存大小则不固定,MongoDB自动使用系统空闲的内存,且数据在文件系统缓存中是压缩存储的。

使用 WiredTiger存储引擎时,MongoDB 支持所有集合和索引的压缩。压缩以增加 CPU 为代价最大限度地减少了存储使用。
默认情况下,WiredTiger 对所有集合使用 Snappy 块压缩,对所有索引使用前缀压缩。压缩默认值可在全局级别配置,也可以在集合和索引创建期间基于每个集合和每个索引进行设置。
对于集合,还可以使用以下块压缩库:

  • zlib
  • zstd(从 MongoDB 4.2 开始可用)

In-Memory

In-Memory存储引擎将数据库数据都存储在内存中,只将少量的元数据和诊断日志、临时数据存储到硬盘文件中,避免了磁盘I/O操作,查询速度很快。

In-Memory存储引擎使用文档级别锁,同一时刻多个写操作可以修改同一个集合中不同的文档,但不能修改同一个文档。

内存使用

In-Memory需要将数据库的数据、索引和操作日志等内容存储到内存中。可以通过参数–inMemorySizeGB设置它占用的内存大小,默认为:(RAM - 1 GB) 的 50%。

In-Memory不需要单独的日志文件,不存在记录日志和等待数据持久化的问题。当MongoDB实例关机或系统异常终止时,所有存储在内存中的数据都将会丢失。

In-Memory虽然不将数据写入硬盘,但还是会记录oplog。利用这个特性,可以在集群中使用In-Memory的MongoDB作为主数据库,使用WiredTiger的MongoDB作为备份数据库,然后将主数据库的oplog推送给备份数据库进行持久化存储,这样即使主数据库关机或异常崩溃,重启后还可以从备份数据库中同步数据。

索引

索引是加快查询的手段,MongoDB的索引支持选择正序、倒序创建,值1指定按升序排列项目的索引。的值-1指定按降序排列项目的索引。
应用程序在索引构建期间可能会遇到性能下降,包括对集合的读/写访问。

类型

单键索引

单键索引(Single Field)顾名思义就是单个字段作为索引列,MongoDB的所有collection默认都有一个单键索引_id。

复合索引

单键索引(Single Field)顾名思义就是多个字段组合作为索引列,使用复合索引时要注意字段的顺序,如下添加一个name和age的复合索引,name正序,age倒序,document首先按照name正序排序,然后name相同的document按age进行倒序排序。mongoDB中一个复合索引最多可以包含32个字段。

多键索引

多键索引(MutiKey)是建在数组上的索引,可指定某个数组字段下的字段。

文本索引

文本索引(Text)来支持对字符串内容的文本搜索查询。 文本索引可以包括其值为字符串或字符串元素数组的任何字段。一个集合只能有一个文本搜索索引,但该索引可以覆盖多个字段。

权重

对于文本索引,索引字段的权重表示该字段相对于其他索引字段在文本搜索分数方面的重要性。
对于文档中的每个索引字段,MongoDB 将匹配数乘以权重并将结果相加。使用这个总和,MongoDB 然后计算文档的分数。
索引字段的默认权重为 1。

通配符索引

通配符索引(Wildcard)是在一个字段或一组字段上创建索引以支持查询。由于 MongoDB 支持动态模式,因此应用程序可以查询名称无法提前知道或任意的字段。

二维球体索引

二维球体索引(2dsphere)支持计算类地球体上的几何形状的查询, 它支持所有 MongoDB 地理空间查询:包含、交叉和邻近查询。

二维索引

二维索引(2d)对存储为二维平面上的点的数据使用索引。该2d索引适用于 MongoDB 2.2 及更早版本中使用的旧坐标对。
在以下情况下使用2d索引:

  • 数据库具有来自 MongoDB 2.2 或更早版本的旧版坐标对,并且不打算将任何位置数据存储为GeoJSON对象。
哈希索引

哈希索引(Hashed)就是将field的值进行hash计算后作为索引,其强大之处在于实现O(1)查找,当然用哈希索引最主要的功能也就是实现定值查找,对于经常需要排序或查询范围查询的集合不要使用哈希索引。

MongoDB 不支持对哈希索引指定唯一约束。您可以改为创建一个附加的非散列索引,该索引具有对该字段的唯一约束。MongoDB 可以使用该非散列索引来强制该字段的唯一性。

属性

TTL 索引

TTL 索引是特殊的单字段索引,MongoDB 可以使用它在一定时间或特定时钟时间后自动从集合中删除文档。数据过期对于某些类型的信息很有用,例如机器生成的事件数据、日志和会话信息,这些信息只需要在数据库中保留有限的时间。

TTL 索引不保证过期数据会在过期后立即被删除,文档过期和 MongoDB 从数据库中删除文档的时间之间可能存在延迟。
删除过期文档的后台任务每 60 秒运行一次,因此,在文档到期和后台任务运行之间的时间段内,文档可能会保留在集合中。
由于删除操作的持续时间取决于数据库实例的工作负载,因此在后台任务运行之间的 60 秒时间段之后mongod,过期数据可能会存在一段时间。

使用限制:

  • TTL 索引是单字段索引。复合索引不支持 TTL 并忽略该 expireAfterSeconds选项。
  • 该_id字段不支持 TTL 索引。
  • 无法在上限集合上创建 TTL 索引,因为 MongoDB 无法从上限集合中删除文档。
  • 不能在时间序列集合上创建 TTL 索引。类似的功能是通过自动删除时间序列集合来提供的。
  • 不能用于createIndex()更改expireAfterSeconds现有索引的值。而是将 collMod数据库命令与 index收集标志结合使用。否则,要更改现有索引的选项值,必须先删除索引并重新创建。
  • 如果某个字段已经存在非 TTL 单字段索引,则无法在同一字段上创建 TTL 索引,因为您无法创建具有相同键规范且仅选项不同的索引。要将非 TTL 单字段索引更改为 TTL 索引,必须先删除索引并使用该 expireAfterSeconds选项重新创建。
唯一索引

唯一索引(Unique)确保索引字段不存储重复值,即强制索引字段的唯一性。默认情况下,MongoDB 在创建集合期间会在_id字段上创建唯一索引。

如果文档在唯一索引中没有索引字段的值,则索引将为此文档存储空值。由于唯一性约束,MongoDB 将只允许一个缺少索引字段的文档。如果有多个文档没有索引字段的值或缺少索引字段,则索引构建将失败并出现重复键错误。

稀疏索引

稀疏索引(Sparse)仅包含具有索引字段的文档的条目,即使索引字段包含空值。索引会跳过任何缺少索引字段的文档。索引是“稀疏的”,因为它不包括集合的所有文档。相比之下,非稀疏索引包含集合中的所有文档,为那些不包含索引字段的文档存储空值。

从 MongoDB 3.2 开始,MongoDB 提供了创建 部分索引的选项。部分索引提供了稀疏索引功能的超集。如果您使用的是 MongoDB 3.2 或更高版本,则应优先使用部分索引而不是稀疏索引。

部分索引

部分索引(Partial)仅索引集合中满足指定过滤器表达式的文档。通过对集合中的文档子集进行索引,部分索引具有较低的存储要求并降低了索引创建和维护的性能成本。
举个例子,以下操作创建一个复合索引,该索引仅索引rating字段大于 5 的文档:

db.restaurants.createIndex(
   { cuisine: 1, name: 1 },
   { partialFilterExpression: { rating: { $gt: 5 } } }
)
不区分大小写的索引

不区分大小写的索引(Case Insensitive)支持执行字符串比较而不考虑大小写的查询。

隐藏索引

隐藏索引(Hidden)对查询计划程序不可见,并且不能用于支持查询。

通过对规划器隐藏索引,用户可以在不实际删除索引的情况下评估删除索引的潜在影响。如果影响是负面的,用户可以取消隐藏索引,而不必重新创建已删除的索引。

集群

由于单机垂直扩展能力的局限,水平扩展的方式则显得更加的靠谱。 MongoDB 自带了这种能力,可以将数据存储到多个机器上以提供更大的容量和负载能力。
此外,同时为了保证数据的高可用,MongoDB 采用副本集的方式来实现数据复制。
一个典型的MongoDB集群架构会同时采用分片+副本集的方式,如下图:
在这里插入图片描述

主从复制

主从复制是 MongoDB 最早使用的复制方式, 该复制方式易于配置,并且可以支持任意数量的从节点服务器,与使用单节点模式相比有如下优点:

  • 在从服务器上存储数据副本,提高了数据的可用性, 并可以保证数据的安全性。
  • 可配置读写分离,主节点负责写操作,从节点负责读操作,将读写压力分开,提高系统的稳定性。

MongoDB 的主从复制至少需要两个服务器或者节点。其中一个是主节点,负责处理客户端请求,其它的都是从节点,负责同步主节点的数据。

主节点记录在其上执行的所有写操作,从节点定期轮询主节点获取这些操作,然后再对自己的数据副本执行这些操作。由于和主节点执行了相同的操作,从节点就能保持与主节点的数据同步。

主节点的操作记录称为oplog(operation log),它被存储在 MongoDB 的 local 数据库中。oplog 中的每个文档都代表主节点上执行的一个操作。需要重点强调的是oplog只记录改变数据库状态的操作。比如,查询操作就不会被存储在oplog中。这是因为oplog只是作为从节点与主节点保持数据同步的机制。

然而,主从复制并非生产环境下推荐的复制方式,主要原因如下两点:

  • 灾备都是完全人工的 如果主节点发生故障失败,管理员必须关闭一个从服务器,然后作为主节点重新启动它。然后应用程序必须重新配置连接新的主节点。
  • 数据恢复困难 因为oplog只在主节点存在,故障失败需要在新的服务器上创建新的oplog,这意味着任意存在的节点需要重新从新的主节点同步oplog。

因此,在新版本的MongoDB中已经不再支持使用主从复制这种复制方式了,取而代之的是使用副本集复制方式。

副本集

MongoDB副本集(Replica Set)其实就是具有自动故障恢复功能的主从集群,和主从复制最大的区别就是在副本集中没有固定的“主节点;整个副本集会选出一个节点作为“主节点”,当其挂掉后,再在剩下的从节点中选举一个节点成为新的“主节点”,在副本集中总有一个主节点(primary)和一个或多个备份节点(secondary)。

官方推荐的副本集最小配置需要有三个节点:一个主节点接收和处理所有的写操作,两个备份节点通过复制主节点的操作来对主节点的数据进行同步备份。

在这里插入图片描述

成员
成员说明
Secondary正常情况下,复制集的Seconary会参与Primary选举(自身也可能会被选为Primary),并从Primary同步最新写入的数据,以保证与Primary存储相同的数据。
Secondary可以提供读服务,增加Secondary节点可以提供复制集的读服务能力,同时提升复制集的可用性。另外,Mongodb支持对复制集的Secondary节点进行灵活的配置,以适应多种场景的需求。
ArbiterArbiter节点只参与投票,不能被选为Primary,并且不从Primary同步数据。

比如你部署了一个2个节点的复制集,1个Primary,1个Secondary,任意节点宕机,复制集将不能提供服务了(无法选出Primary),这时可以给复制集添加一个Arbiter节点,即使有节点宕机,仍能选出Primary。
Arbiter本身不存储数据,是非常轻量级的服务,当复制集成员为偶数时,最好加入一个Arbiter节点,以提升复制集可用性。 |
| Priority0 | Priority0节点的选举优先级为0,不会被选举为Primary
比如你跨机房A、B部署了一个复制集,并且想指定Primary必须在A机房,这时可以将B机房的复制集成员Priority设置为0,这样Primary就一定会是A机房的成员。
(注意:如果这样部署,最好将『大多数』节点部署在A机房,否则网络分区时可能无法选出Primary) |
| Vote0 | Mongodb 3.0里,复制集成员最多50个,参与Primary选举投票的成员最多7个,其他成员(Vote0)的vote属性必须设置为0,即不参与投票。 |
| Hidden | Hidden节点不能被选为主(Priority为0),并且对Driver不可见。因Hidden节点不会接受Driver的请求,可使用Hidden节点做一些数据备份、离线计算的任务,不会影响复制集的服务。 |
| Delayed | Delayed节点必须是Hidden节点,并且其数据落后与Primary一段时间(可配置,比如1个小时)。
因Delayed节点的数据比Primary落后一段时间,当错误或者无效的数据写入Primary时,可通过Delayed节点的数据来恢复到之前的时间点。 |

选举

副本集通过replSetInitiate命令(或mongo shell的rs.initiate())进行初始化,初始化后各个成员间开始发送心跳消息,并发起Priamry选举操作,获得『大多数』成员投票支持的节点,会成为Primary,其余节点成为Secondary。
假设复制集内投票成员数量为N,当副本集内存活成员数量不足N/2 + 1时,整个复制集将无法选举出Primary,复制集将无法提供写服务,处于只读状态,通常建议将副本集成员数量设置为奇数。

仲裁者

在某些情况下(例如,当有主服务器和辅助服务器,但成本限制禁止添加另一个辅助服务器时),我们可以选择将仲裁器添加到副本集。
仲裁器参与主节点的选举,但仲裁器没有数据集的副本,不能成为主节点。
仲裁者拥有准确的1选举投票。默认情况下,仲裁者具有优先权0。
例如,在以下具有 2 个数据承载成员(主要和次要)的副本集中,仲裁器允许该集合拥有奇数票数来打破平局:
在这里插入图片描述

oplog

oplog是local库下的一个固定集合,Secondary就是通过查看Primary 的oplog这个集合来进行复制的。每个节点都有oplog,记录这从主节点复制过来的信息,这样每个成员都可以作为同步源给其他节点。
oplog 可以说是Mongodb Replication的纽带了。

当第一次启动一个副本集成员时,如果不指定 oplog 大小,MongoDB 会创建一个基于操作系统的默认大小的 oplog。
在创建 oplog 之前,可以使用oplogSizeMB选项指定其大小。
首次启动副本集成员后,使用replSetResizeOplog管理命令更改 oplog 大小,使其能够在不重新启动进程replSetResizeOplog的情况下动态调整 oplog 的大小。

下面,看看一条 oplog 的具体形式:

{
"ts" : Timestamp(1446011584, 2),
"h" : NumberLong("1687359108795812092"),
"v" : 2,
"op" : "i",
"ns" : "test.nosql",
"o" : { "_id" : ObjectId("563062c0b085733f34ab4129"), "name" : "mongodb", "score" : "100" }
}

其中的一些关键字段有:

  • ts 操作的 optime,该字段不仅仅包含了操作的时间戳(timestamp),还包含一个自增的计数器值。
  • h 操作的全局唯一表示
  • v oplog 的版本信息
  • op 操作类型,比如 i=insert,u=update…
  • ns 操作集合,形式为 database.collection
  • o 指具体的操作内容,对于一个 insert 操作,则包含了整个文档的内容
心跳

在高可用的实现机制中,心跳(heartbeat)是非常关键的,判断一个节点是否宕机就取决于这个节点的心跳是否还是正常的。
副本集中的每个节点上都会定时向其他节点发送心跳,以此来感知其他节点的变化,比如是否失效、或者角色发生了变化。
默认情况下,节点会每2秒向其他节点发出心跳,这其中包括了主节点。 如果备节点在10秒内没有收到主节点的响应就会主动发起选举。
此时新一轮选举开始,新的主节点会产生并接管原来主节点的业务。 整个过程对于上层是透明的,应用并不需要感知,因为 Mongos 会自动发现这些变化。
如果应用仅仅使用了单个副本集,那么就会由 Driver 层来自动完成处理。

分片

高数据量和吞吐量的数据库应用会对单机的性能造成较大压力,大的查询量会将单机的CPU耗尽,大的数据量对单机的存储压力较大,最终会耗尽系统的内存而将压力转移到磁盘IO上。
为了解决这些问题,有两个基本的方法: 垂直扩展和水平扩展。

  • 垂直扩展:增加更多的CPU和存储资源来扩展容量。
  • 水平扩展:将数据集分布在多个服务器上。水平扩展即分片。
分片架构
组件说明
Config Server存储集群所有节点、分片数据路由信息。默认需要配置3个Config Server节点。
Mongos提供对外应用访问,所有操作均通过mongos执行。一般有多个mongos节点。数据迁移和数据自动平衡。
Mongod存储应用数据记录。一般有多个Mongod节点,达到数据分片目的。

在这里插入图片描述

  • Mongos :数据路由,和客户端打交道的模块。mongos本身没有任何数据,他也不知道该怎么处理这数据,去找config server。
  • Config Server:所有存、取数据的方式,所有shard节点的信息,分片功能的一些配置信息。可以理解为真实数据的元数据。
  • Shard:真正的数据存储位置,以chunk为单位存数据。
分片机制

首先,基于分片切分后的数据块称为 chunk,一个分片后的集合会包含多个 chunk,每个 chunk 位于哪个分片(Shard) 则记录在 Config Server(配置服务器)上。
Mongos 在操作分片集合时,会自动根据分片键找到对应的 chunk,并向该 chunk 所在的分片发起操作请求。

数据是根据分片策略来进行切分的,而分片策略则由 分片键(ShardKey)+分片算法(ShardStrategy)组成。

调优

explain

使用 explain() 命令可以用于查询计划分析,进一步评估索引的效果,详细说明可参考官网
如下:

db.test.explain().find( { a : 5 } )

{
  "queryPlanner" : {
    ...
    "winningPlan" : {
      "stage" : "FETCH",
      "inputStage" : {
        "stage" : "IXSCAN",
        "keyPattern" : {
            "a" : 5
        },
        "indexName" : "a_1",
        "isMultiKey" : false,
        "direction" : "forward",
        "indexBounds" : {"a" : ["[5.0, 5.0]"]}
        }
    }},
   ...
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我叫小八

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值