FastDB 应用开发指南

 FastDB 应用开发指南(一)

FastDB 是一个高效率的内存数据库系统,具有实时性能和方便的 C++ 接口。

FastDB 并不支持客户端/服务器结构,所有使用FastDB数据库的应用程序都必须运行在同一台主机上。

FastDB 为具有主导读取访问模式的应用程序作了优化。

通过消除数据传输的开销和使用高性能的锁工具实现了查询执行的高速度。

数据库文件和使用该数据库的每一个应用程序占用的虚拟内存空间相映射。

所以查询在应用程序的任务中执行,不需要进行任务切换和数据传输。

在FastDB中,通过原子指令来实现对数据库并发访问的同步,对查询处理几乎不增加任 何开销。

FastDB假设整个数据库都在当前内存中,并且在这个假设的基础上优化查询算法和结构。

另外,数据库缓存管理几乎不会给FastDB 增加任何开销,同时FastDB也不需要在数据库文件和缓冲池中进行数据传送。

这就是为什么FastDB比将所有数据放在缓冲池中的传统数据库明显速度快的原因。

FastDB 支持事务、在线备份和系统崩溃之后的自动恢复。

事务提交协议基于一个影子根页算法,对数据库执行原子更新操作。

恢复操作执行起来非常快,给关键应用程序提供了高效率。

另外,它还取消了事务日志,提高了系统的整体性能,并且能够更加有效地使用系统资源。

FastDB是面向应用程序的数据库,使用应用程序的类信息来构建数据库的表。

FastDB 支持自动系统赋值,只允许你在一个地方——你的应用程序的类中,改变它们的值。

FastDB为从数据库中提取数据提供了一个灵活而方便的接口。

使用类似于SQL的语言来书写查询语句。

这些非原子字段、嵌套数组、用户自定义类型和方法、直接指向对象内部的指针等后关系性能,简化了数据库应用程序的设计,并且使得它们更加高效。

虽然FastDB的优化是基于整个数据库都存放在机器的物理内存的这个假设上的,我们依然可以将FastDB使用在那些大小超过系统物理内存的数据库上。

最后,标准操作系统的交换机制将会起作用。

但是所有的 FastDB 的算法和结构的优化都是基于数据存放在内存中这个假设上的,所以数据交换的效率不会很高。

查询语言FastDB支持一种语法和SQL非常类似的查询语言。

FastDB使用的符号更加流行于面向对象的编程中,而不是面向关系型数据库的编程中。

将表行看作是对象的实例,表是这些对象的类。

跟SQL不一样,FastDB 是面向对象的,而并不跟SQL一样是工作在一张二维表上的。

所以每一个查询的执行结果就是一个类的一组对象。

FastDB查询语言和标准的SQL的主要区别如下: 


1、 在多张表之间没有连接,也没有嵌套的子查询。

通常,查询是从一张表中返回一个对象集。

2、 表的最小的列单元是标准的C类型。

3、 FastDB只有空指针,而没有NULL值。

我完全同意C.J Date关于三值逻辑的评论,也赞同他使用默认值的建议。

4、 结构和数组可以用作记录成员。

提供了一个特定的exists引用来定位数组中的元素, 

5、 不仅可以用无参用户方法来定义记录成员,也可以用它来定义 表记录(对象)。

6、 应用程序可以定义仅带一个简单字符型或数值型参数的用户函数。

7、 支持对象之间的指针,并且自动支持反向指针。

8、 start from follow by 的构造函数,实现了指针在递归记录之间的移动。

9、 由于查询语言和C++类结合得比较紧密,关键字和语言的标识符都区分大小写。

10、 不能进行整型和符点类型到字符串类型的隐式转换。

如果需要进行这种转换的话,必须显式地进行。


文章出处:飞诺网(www.firnow.com):http://dev.firnow.com/course/7_databases/database_other/2007106/77043.html


本文原创作品,如需转载请注明来源,作者:姜涛, towerjt@gmail.com

FastDB中不同的访问DataBase的模式在程序中能体现不一样的结果。
从试验可以知道,不同访问模式主要体现在对表的锁上。
(具体的试验,大家如果觉得有必要,可以自己做一下,用
subsql -access [read-only concurrent-read concurrent-update normal]
可以指定访问数据的模式,然后自己构建相关测试用例进行测试
dbDatabase::dbAllAccess,一旦某个进程使用该模式访问表,如果该进程使用了insert、update、delete等修改数据的操作,
其他访问该库的进程的所有操作(包括open、select)都会被阻塞,直到该操作提交或回滚
dbDatabase::dbConcurrentUpdate,使用该模式访问表,如果某个进程对数据进行修改性的操作,同时另外的进程使用
dbDatabase::dbReadOnly或者dbDatabase::dbConcurrentRead读取数据,不会出现阻塞的情况。
但是dbDatabase::dbReadOnly会把未提交的脏数据读出来;而dbDatabase::dbConcurrentRead则不会
如果多个进程都使用dbDatabase::dbConcurrentUpdate,实际效果和dbDatabase::dbAllAccess一样,一旦一个进程修改了数据,
其他进程所有的操作(包括open、select)都将阻塞,直到该操作提交或回滚
结论:
FastDB没有提供商用数据库的记录锁甚至是页级锁的机制,锁的范围是一个DataBase文件,
所以在日常的使用过程中:
1、仔细分析业务需求,如果你只需要访问数据,那么最好是使用dbDatabase::dbReadOnly或者是
dbDatabase::dbConcurrentRead。这样的话,你的访问操作不会因为这个表被其他进程修改数据而阻塞。
2、如果一个表有不止一个使用者,那么涉及修改数据的进程应该使用dbDatabase::dbConcurrentUpdate,
而只读的进程使用则使用dbDatabase::dbConcurrentRead。
3、如果表只有一个使用者,则可以根据需要,使用dbDatabase::dbAllAccess或者dbDatabase::dbReadOnly
4、所有的数据修改操作,如果有并发要求,一定要及时提交

fastdb 共享内存模式下的限制

最近项目中使用了fast db,为了提高访问效率,fast db采用diskless模式编译。
共享内存的最大尺寸受系统参数限制,
下面数据是在Linux im_monitor 2.6.9-42.ELsmp 下的缺省值:
cat /proc/sys/kernel/shmmax
33554432
默认大小都是32M;

同样fastdb里的代码也用32M作为容量上限,而且一旦越界,不再进行扩容;进程退出;
具体的代码行是:inc/database.h 文件里
#ifdef DISKLESS_CONFIGURATION
// In diskless confiuration database can not be reallocated
const size_t dbDefaultInitDatabaseSize = 32*1024*1024;
#else
const size_t dbDefaultInitDatabaseSize = 1024*1024;
#endif

为了支持fastdb在更大的共享内存下工作,需要做两点修改:
1、系统参数的修改
修改 /etc/sysctl.cfg,添加入下内容:
kernel.shmmni = 4096
kernel.shmall = 2097152
kernel.shmmax = 1073741824
sysctl -p 执行;
或者echo 1073741824 > /proc/sys/kernel/shmmax ,注意需要加到启动脚本里去;

2、修改fastdb源代码
const size_t dbDefaultInitDatabaseSize = 32*1024*1024;修改为合适的数值;比如32->1024

鉴于共享内存大小限制,以及不能动态扩容:当容量超过限制,进程不能进行再分配,直接退出,对于大容量的系统,对fastdb的数据使用超过2G, 或者总的虚拟内存使用量可能接近3G, 则不建议在32位操作系统中使用共享内存方式的fastdb,可以改用文件方式,实际上对于文件方式,也是通过mmap的方式来实现,对于io的写基本等同于内存效率,且可以在预分配的基础上扩容。

 

                                                                                        
FastDB内存数据库API
1.  查询语言
mdb支持类SQL句法的查询语言。mdb更接近oop而不是关系数据库。Table中的行被认为是对象实例,table则是这些对象的类。与SQL不同,mdb是面向对象的而不是SQL元组。因此,每次查询的结果是一个类的对象的集合。 
标识符大小写敏感,由 a-z, A-Z, '_' 或者 '$'字符开头,只能包含a-z, A-Z, '_' 或者 '$'字符,不能与SQL保留字重复
保留字列表 
abs and asc between by 
current desc escape exists false 
first follow from in integer 
is length like last lower 
not null or real start 
string true upper     

使用ANSI标准注释,在双连字符后直至行尾的字符都被忽略。
mdb支持位操作,and/or不仅可以用于布尔操作数,也可以用于整型操作数。对整型操作数进行and/or操作返回对这两个数进行 位-and/位-or操作的得到的一个整型操作数。
mdb也支持对整型和浮点型的幂运算(x^y)
Structures
结构可以作为记录的组成部分,结构的字段可以通过标准的点表达式访问:
company.address.city 
结构字段可以以指定的顺序索引和使用,可以嵌套并且嵌套的层次没有限制。
程序员可以定义结构的方法用于查询。该方法不能有参数并且只能返回原子类型(布尔值、数值、字符串和引用类型)。这些方法也不能改变对象句柄。如果该方法返回字符串,这个字符串应当使用new运算符分配,因为其值拷贝后串就会被删掉。
用户定义方法可以用来创建虚组件-就是不存储在数据库中由别的组件计算出来的组件。例如,mdb dbDateTime类型只包括一个整型的时间戳组件以及如dbDateTime::year(),dbDateTime::month()之类的方法,可以在应用中指定像这样的查询:"delivery.year = 1999",
Arrays
mdb可以使用动态数组作为记录的组成部分。不支持多维数组,但是可以定义数组的数组。
Strings
mdb中的string都是变长的。只支持ascii字符集以及逐字节的比较并忽略字符集的设定。
References
引用通过点表达式来解析,如:company.address.city = 'Chicago',可以用is null和is not null来检查引用是否为空。也可以与null关键字进行是否相等的比较。当解析一个null应用时,mdb会抛出异常。
mdb不支持连接操作,连接操作可以用引用来实现,如下面例子:
struct Detail { 
    char const* name;
    double      weight;
    
    TYPE_DESCRIPTOR((KEY(name, INDEXED), FIELD(weight)));
};
 
struct Supplier { 
    char const* company;
    char const* address;
 
    TYPE_DESCRIPTOR((KEY(company, INDEXED), FIELD(address)));
};
 
struct Shipment { 
    dbReference<Detail>   detail;
    dbReference<Supplier> supplier;
    int4                  price;
    int4                  quantity;
    dbDateTime            delivery;
 
    TYPE_DESCRIPTOR((KEY(detail, HASHED), KEY(supplier, HASHED), 
                    FIELD(price), FIELD(quantity), FIELD(delivery)));
};
我们需要得到来自于某个suppliers的delivery的details信息,在关系数据库中有如下查询:
     select from Supplier,Shipment,Detail where 
                 Supplier.SID = Shipment.SID and Shipment.DID = Detail.DID 
                and Supplier.company like ? and Supplier.address like ?
                and Detail.name like ? 
在mdb中就写成这样:
     dbQuery q = "detail.name like",name,"and supplier.company like",company,
                 "and supplier.address like",address,"order by price";
Functions
Predefined functions 
Name Argument type Return type Description 
abs integer integer absolute value of the argument  
abs real real absolute value of the argument  
integer real integer conversion of real to integer  
length array integer number of elements in array  
lower string string lowercase string  
real integer real conversion of integer to real  
string integer string conversion of integer to string  
string real string conversion of real to string  
upper string string uppercase string  

mdb允许用户自行定义函数和运算符,函数至少需要一个参数,但不能超过3个。参数类型必须为string、integer、boolean、reference或者用户定义类型(raw binary) 
用户自定义函数必须使用USER_FUNC(f)宏注册。
 
2.  C++接口
mdb中可以用c++写出如下的查询:         
    dbQuery q; 
    dbCursor<Contract> contracts;
    dbCursor<Supplier> suppliers;
    int price, quantity;
    q = "(price >=",price,"or quantity >=",quantity,
        ") and delivery.year=1999";
    // input price and quantity values
    if (contracts.select(q) != 0) { 
        do { 
            printf("%s/n", suppliers.at(contracts->supplier)->company);
        } while (contracts.next());
 
Table
mdb的数据存储在table中,这对应于C++类,表记录则对应于类实例。下面是可以做为mdb记录原子组件的C++类型: 
Type Description 
bool boolean type (true,false) 
int1 one byte signed integer (-128..127) 
int2 two bytes signed integer (-32768..32767) 
int4 four bytes signed integer (-2147483648..2147483647) 
int8 eight bytes signed integer (-2**63..2**63-1) 
real4 four bytes ANSI floating point type 
real8 eight bytes ANSI double precision floating point type 
char const* zero terminated string 
dbReference<T> reference to class T 
dbArray<T> dynamic array of elements of type T 

除此之外,mdb记录也包括包含这些componets的嵌套的结构。mdb不支持unsigned类型。
下面是定义一个table的例子,可以放在头文件中:
class dbDateTime { 
    int4 stamp;
  public:
 
    int year() { 
        return localtime((time_t*)&stamp)->tm_year + 1900;
    }
    ...
 
    CLASS_DESCRIPTOR(dbDateTime, 
                    (KEY(stamp,INDEXED|HASHED), 
                     METHOD(year), METHOD(month), METHOD(day),
                     METHOD(dayOfYear), METHOD(dayOfWeek),
                     METHOD(hour), METHOD(minute), METHOD(second)));
};    
 
class Detail { 
  public:
    char const* name;
    char const* material;
    char const* color;
    real4       weight;
 
    dbArray< dbReference<Contract> > contracts;
 
    TYPE_DESCRIPTOR((KEY(name, INDEXED|HASHED), 
                    KEY(material, HASHED), 
                    KEY(color, HASHED),
                    KEY(weight, INDEXED),
                    RELATION(contracts, detail)));
};
 
class Contract { 
  public:
    dbDateTime            delivery;
    int4                  quantity;
    int8                  price;
    dbReference<Detail>   detail;
    dbReference<Supplier> supplier;
 
    TYPE_DESCRIPTOR((KEY(delivery, HASHED|INDEXED), 
                    KEY(quantity, INDEXED), 
                    KEY(price, INDEXED),
                    RELATION(detail, contracts),
                    RELATION(supplier, contracts)));
};
定义了表之后,就需要用REGISTER(name)宏来注册,也就是把上面定义的类与表联系起来:
REGISTER(Detail);
REGISTER(Supplier);
REGISTER(Contract);
这个宏应该放在实现而不是头文件中。
当数据库新注册的表与库中原有的表同名时,mdb会比较两个表,如果定义不同,mdb会把原来的表更新成新的表,可以添加新字段,但是删除字段只有在原来的表为空的时候才可以进行。
有一个特殊的内部数据库表Metatable,保存了这个数据库中其他所有表的信息。
支持自动增加字段。
Query
The class query is used to serve two purposes: 
to construct a query and bind query parameters 
to cache compiled queries 
例子:
        dbQuery q;
        int price, quantity;
        q = "price >=",price,"or quantity >=",quantity;
给上面的参数price,quantity赋不同的值查询可以得到不同的结果,不能用数值常量作查询参数,但是可以用字符串常量作查询参数。
 
有两个方法来给字符串类型的参数赋值:
     dbQuery q;
     char* type;
     char name[256];
     q = "name=",name,"and type=",&type;
 
     scanf("%s", name);
     type = "A";     
     cursor.select(q);
     ...
     scanf("%s", name);
     type = "B";     
     cursor.select(q);
     ...
Cursor
Cursors用于访问查询语句返回的记录。mdb提供有类型的cursor,即与具体表关联的cursor.mdb有两种cursor:只读cursor和用于更新的cursor.mdb中cursor用c++模板类dbCursor<T>表示,T为一个与表关联的C++类的名字。cursor的类型必须在构造时指定,缺省时只读的。要创建一个用于更新的cursor,必须向constructor传递参数dbCursorForUpdate,
如:dbCursor<Contract> contract(dbCursorForUpdate);
 
查询通过执行cursor的select(dbQuery& q)或者select()方法,后者用来迭代表中的所有记录。两个方法都返回查询选择的记录数并且置cursor的当前位置为第一个记录(如果存在的话)。一个cursor可以向前向后滚动,next(),prev(),first(),last()方法用来改变cursor的当前位置,如果由于没有更多记录而使得这些操作进行下去,则这些操作返回NULL,并且当前的cursor位置不变。
 C++ API定义了null引用类型,可以将null与reference比较或者将其赋给reference:
        void update(dbReference<Contract> c) {
            if (c != null) { 
                dbCursor<Contract> contract(dbCursorForUpdate);
               contract.at(c);
               contract->supplier = null;
            }
        }
 
query参数通常与C++变量绑定,解决多线程同一query不同参数执行的问题 ,mdb采用了延迟参数绑定的方法:
dbQuery q;
struct QueryParams { 
    int         salary;
    int         age;
    int         rank;
};
void open()
{
    QueryParams* params = (QueryParams*)NULL;
    q = "salary > ", params->salary, "and age < ", params->age, "and rank =", params->rank;
}
void find(int salary, int age, int rank) 
    QueryParams params;
    params.salary = salary;
    params.age = age;
    params.rank = rank;
    dbCursor<Person> cusor;
    if (cursor.select(q, & params) > 0) { 
        do { 
        } while (cursor.next());
    }
}
So in this example function open binds query parameters just to offsets of fields in structure. Later in find functions, actual pointer to the structure with parameters is passed to the select structure. Function find can be concurrently executed by several threads and only one compiled version of the query is used by all these threads. This mechanism is available since version 2.25.
Database
dbDatabase类控制应用与数据库的交互。同步数据库的并发访问,事务管理,内存分配,错误处理,。。。
构造dbDatabase对象时可以指定数据库参数:
    dbDatabase(dbAccessType type = dbAllAccess,
               size_t dbInitSize = dbDefaultInitDatabaseSize,
               size_t dbExtensionQuantum = dbDefaultExtensionQuantum,
               size_t dbInitIndexSize = dbDefaultInitIndexSize,
               int nThreads = 1);
当database主要是以readonly模式访问并且更新不能长时间锁定读的时候应当同时使用dbConcurrentUpdate和dbConcurrentRead模式。在这种模式下,更新数据库可以与读访问并发进行(reader将看不到改变的数据,直到事务提交),只有在事务提交时才设置排它锁并且当更新了当前对象的索引后会马上释放。
Attension! Do not mix dbConcurrentUpdate and dbConcurrentRead mode with other modes and do not use them together in one process (so it is not possible to start two threads in one of which open database in dbConcurrentUpdate mode and in other - in dbConcurrentRead). Do not use dbDatabase::precommit method in dbConcurrentUpdate mode.
使用open(char const* databaseName, char const* fileName = NULL, unsigned waitLockTimeout = INFINITE)方法打开数据库。如果fileName为空,则自动在databaseName加上'.fdb'形成数据库文件名,waitLockTimeout用来设置一个进程被锁住的最大时间,当过期后,被锁住的活动进程就自动恢复执行。
使用dbDatabase::backup(char const* file)方法备份数据库。恢复只需要改文件名。
 
3.  SubSql
   select (*) from table-name select-condition ;
  | insert into table-name values values-list ;
  | create index on on table-name.field-name ;
  | create table table-name (field-descriptor {, field-descriptor}) ;
  | alter table table-name (field-descriptor {, field-descriptor}) ;
  | update table-name set field-name = expression {, field-name = expression} where condition ;
  | drop index table-name.field-name ;
  | drop table table-name
  | open database-name ( database-file-name ) ;
  | delete from table-name
  | backup file-name
  | start server server-URL number-of-threads
  | stop server server-URL
  | start http server server-URL 
  | stop http server server-URL
  | export xml-file-name
  | import xml-file-name
  | commit
  | rollback
  | autocommit (on | off)
  | exit
  | show
  | help
 
 
4.  Quick start
开发mdb应用时,首先要确定需要存储在数据库中的数据和类。通过REGISTER宏注册表标识符。如果想要重定义缺省的mdb出错处理,就要从dbDatabase继承定义自己的数据库类。应当创建一个该类的实例并且使之能够让所有的应用模块访问。
在操作数据库前,首先要打开。检查dbDatabase::open()的返回码确定数据库是否成功返回。在数据库打开过过程中出错并不会中止应用不过回报告。
确认数据库正常打开后,就可以开始工作。如果是多线程的应用并且多个线程会使用同一个数据库,就用dbDatabase::attach方法把每一个线程与数据库连接起来。在线程终止前,应该通过dbDatabase::detach()方法将其与数据库剥离。
为了访问数据库的数据,需要创建一些dbQuery和dbCursor对象,如果多个线程同时工作于一个数据库,每一个线程都要有自己query和cursor对象。通常一个表有一个cursor就够了(或者两个,如果应用需要更新数据库的话)。但在嵌套查询时也需要使用多个cursor 。每一种类型的查询都需要创建一个query对象,query对象也用来缓存编译好的查询。
数据库有4个主要操作:insert, select, update, remove.
第一个操作不需要cursor,而使用全局重载的模版函数insert。
选择,更新和删除记录需要使用cursor。要改变表就必须使用用于更新的cursor. 在mdb中cursor是类型化的,并且包含一个table类的对象实例。重载的cursor运算符‘->'能够用来访问当前记录的组件,也能够用来更新这些组件。update方法把当前cursor对象的数据复制到当前的表记录中。remove方法将移除当前cursor记录,removeAllSelected将移除所有中选的记录,removeAll将移除表中所有的记录。每一个事务由dbDatabase::commit()提交或者由dbDatabase::rollback()中止。在第一个select 、insert或者remove操作执行时就自动开始一个事务。
 在退出应用前要记住关闭数据库,同时也要记得dbDatabase::close()方法会自动提交最后一个事务。因此如果这不是你所想的操作,推出前显式调用dbDatabase::rollback
一个mdb应用的模版: 
//
// Header file
//
#include "mdb.h"
 
extern dbDatabase db; // create database object
 
class MyTable { 
    char const* someField;
    ...
  public:
    TYPE_DESCRIPTOR((FIELD(someField)));
};
 
//
// Implementation
//
REGISTER(MyTable);
 
int main() 
    if (db.open("mydatabase")) { 
        dbCursor<MyTable> cursor;
        dbQuery q;
 
  char value[bufSize];
 
  q = "someField=",value;
  gets(value);
  if (cursor.select(q) > 0) { 
      do { 
          printf("%s/n", cursor->someField);
      } while (cursor.next());
        }
  db.close();
  return EXIT_SUCCESS;
    } else { 
        return EXIT_FAILURE;
    }
}

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
概述FastDB是一个高效率的内存数据库系统,具有实时性能和方便的C++接口。 FastDB并不支持客户端/服务器结构,所有使用FastDB数据库的应用程序都必须运行在同一台主机上。FastDB为具有主导读取访问模式的应用程序作了优化。通过消除数据传输的开销和使用高性能的锁工具实现了查询执行的高速度。数据库文件和使用数据库的每一个应用程序占用的虚拟内存空间相映射。所以查询在应用程序的任务中执行,不需要进行任务切换和数据传输。在FastDB中,通过原子指令来实现对数据库并发访问的同步,对查询处理几乎不增加任何开销。FastDB假设整个数据库都在当前内存中,并且在这个假设的基础上优化查询算法和结构。另外,数据库缓存管理几乎不会给FastDB增加任何开销,同时FastDB也不需要在数据库文件和缓冲池中进行数据传送。这就是为什么FastDB比将所有数据放在缓冲池中的传统数据库明显速度快的原因。   FastDB支持事务、在线备份和系统崩溃之后的自动恢复。事务提交协议基于一个影子根页算法,对数据库执行原子更新操作。恢复操作执行起来非常快,给关键应用程序提供了高效率。另外,它还取消了事务日志,提高了系统的整体性能,并且能够更加有效地使用系统资源。   FastDB是面向应用程序的数据库使用应用程序的类信息来构建数据库的表。FastDB支持自动系统赋值,只允许你在一个地方——你的应用程序的类中,改变它们的值。FastDB为从数据库中提取数据提供了一个灵活而方便的接口。使用类似于SQL的语言来书写查询语句。这些非原子字段、嵌套数组、用户自定义类型和方法、直接指向对象内部的指针等后关系性能,简化了数据库应用程序的设计,并且使得它们更加高效。   虽然FastDB的优化是基于整个数据库都存放在机器的物理内存的这个假设上的,我们依然可以将FastDB使用在那些大小超过系统物理内存的数据库上。最后,标准操作系统的交换机制将会起作用。但是所有的FastDB的算法和结构的优化都是基于数据存放在内存中这个假设上的,所以数据交换的效率不会很高。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值