InnoDB引擎底层事务的原理

事务ACID特性

原子性

一个事务中的操作要么全部成功,要么全部失败。

通过  undo log 来实现

隔离性

一个事务的修改在最终提交前,对其他事务是不可见的。

通过 读写锁+MVCC来实现

持久性

一旦事务提交,所做的修改就会永久保存到数据库中。

通过  redo log 来实现

一致性

通过 原子性、隔离性、持久性来保证
数据库必须要实现AID三大特性,才有可能实现一致性。同时 一致性也需要应用程序的支持

事务的具体实现机制

MySQL采用的是 WAL(Write-ahead logging, 预写式日志)机制来实现
在使用WAL的系统中, 所有的修改都先被写入到日志中,然后 再被应用到系统中。通常包含 redo和undo两部分信息。
重启之后系统可以通过比较日志和系统状态来决定是 继续完成操作还是撤销操作。

redo log

重做日志,每当有操作时,在 数据变更之前将操作写入redo log,这样当发生掉电之类的情况时系统可以在 重启后继续操作

undo log

撤销日志当一些 变更执行到一半无法完成时,可以 根据撤销日志恢复到变更之前的状态。
MySQL中用 redo log来在系统Crash重启之类的情况时 修复数据(事务的持久性),而 undo log来保证事务的原子性

redo日志

使用redo日志的原因

如果我们只在内存的Buffer Pool中修改了页面,假设在事务提交后突然发生了某个故障,导致内存中的数据都失效了,这个已经提交了的事务对数据库中所做的更改也就跟着丢失了,如何保证这个持久性呢?一个很简单的做法就是在事务提交完成之前把该事务所修改的所有页面都刷新到磁盘,但是这个简单粗暴的做法有些问题:

刷新一个完整的数据页太浪费了

随机IO刷起来比较慢

redo日志如何保证持久性

没有必要在每次事务提交时就把该事务在内存中修改过的全部页面刷新到磁盘,只需要 把修改了哪些东西记录一下就好
这样我们在 事务提交时,把上述内容刷新到磁盘中,即使之后系统崩溃了,重启之后只要按照上述内容所记录的步骤重新更新一下数据页,那么该事务对数据库中所做的修改又可以被恢复出来,也就意味着满足持久性的要求。
因为在系统崩溃重启时需要按照上述内容所记录的步骤重新更新数据页,所以上述内容也被称之为 重做日志

好处:

redo日志 占用的空间非常小
存储 表空间ID、页号、偏移量以及 需要更新的值所需的 存储空间是很小的。
redo日志 是顺序写入磁盘的
在执行事务的过程中,每 执行一条语句,就可能 产生若干条redo日志,这些日志是 按照产生的顺序写入磁盘的,也就是 使用顺序IO

redo日志格式

type:该条redo日志的类型,redo日志设计大约有53种不同的类型日志。
space ID:表空间ID。
page number:页号。
data:该条redo日志的具体内容

简单的redo日志类型

Max Row ID属性实际占用8个字节的存储空间,所以在修改页面中的该属性时,会记录一条类型为MLOG_8BYTE的redo日志

offset代表在页面中的偏移量
其余MLOG_1BYTE、MLOG_2BYTE、MLOG_4BYTE类型的redo日志结构和MLOG_8BYTE的类似,只不过具体数据中包含对应个字节的数据罢了。 MLOG_WRITE_STRING类型的redo日志表示写入一串数据,但是因为 不能确定写入的具体数据占用多少字节,所以需要在日志结构中还会 多一个len字段

复杂一些的redo日志类型

InnoDB中就有 非常多的redo日志类型来做记录。这些类型的redo日志既包含物理层面的意思,也包含逻辑层面的意思,
物理层面看,这些日志都指明了 对哪个表空间的哪个页进行了修改
逻辑层面看,在系统崩溃重启时,并 不能直接根据这些日志里的记载,将页面内的某个偏移量处恢复成某个数据,而是需要 调用一些事先准备好的函数执行完这些函数后才可以将页面恢复成系统崩溃前的样子。
只要记住: redo日志会把事务在执行过程中对数据库所做的所有修改都记录下来,在之后 系统崩溃重启后可以把事务所做的任何修改都恢复出来。

redo日志的写入过程

redo log block和日志缓冲区

InnoDB为了更好的进行系统崩溃恢复,把redo日志都放在了大小为 512字节的块(block)中。
写入redo日志时也 不能直接直接写到磁盘上,实际上在 服务器启动时就向操作系统申请了一大片称之为 redo log buffer 的连续内存空间,就是redo日志缓冲区,简称为 log buffer
这片内存空间被 划分成若干个连续的redo log block,我们可以通过启动参数 innodb_log_buffer_size指定log buffer的大小,该启动参数的默认值为 16MB
向log buffer中 写入redo日志的过程是顺序的,也就是 先往前边的block中写,当该block的空闲空间 用完之后再往下一个block中写

redo日志刷盘时机

  1. InnoDB认为如果当前写入log buffer的redo日志量已经占满了log buffer总容量的大约一半左右,就需要把这些日志刷新到磁盘上
  2. 在事务提交时可以不把修改过的Buffer Pool页面刷新到磁盘,但是为了保证持久性,必须要把修改这些页面对应的redo日志刷新到磁盘
  3. 后台有一个线程,大约每秒都会刷新一次log buffer中的redo日志到磁盘
  4. 正常关闭服务器时等等

redo日志文件组

MySQL的数据目录(使用 SHOW VARIABLES LIKE 'datadir'查看)下默认有两个名为 ib_logfile0和ib_logfile1的文件,log buffer中的日志默认情况下就是 刷新到这两个磁盘文件中。如果我们对默认的redo日志文件不满意,可以通过下边几个启动参数来调节:
  • innodb_log_group_home_dir 指定了redo日志文件所在的目录,默认值就是当前的数据目录。
  • innodb_log_file_size 指定了每个redo日志文件的大小,默认值为48MB
  • innodb_log_files_in_group 指定redo日志文件的个数,默认值为2,最大值为100
磁盘上的redo日志文件以一个日志文件组的形式出现,以ib_logfile[数字](数字可以是0、1、2...)的形式进行命名,是 从ib_logfile0开始写,到最后一个文件,都满了就转到ib_logfile0继续写。
非常大的负载下,为 避免错误的覆盖,InnoDB  会强制的flush脏页
将log buffer中的redo日志 刷新到磁盘的本质就是 把block的镜像写入日志文件中,所以 redo日志文件其实也是 由若干个512字节大小的block组成。
redo日志文件组中的每个文件大小都一样,格式也一样,都是由两部分组成:
  • 前2048个字节,也就是前4个block是用来存储一些管理信息的。
  • 从第2048字节往后是用来存储log buffer中的block镜像的。

Log Sequence Number

记录已经写入的redo日志量,设计了一个称之为 Log Sequence Number的全局变量,翻译过来就是: 日志序列号,简称 LSN。规定 初始的lsn值为8704
redo日志都有一个 唯一的LSN值与其对应LSN值越小,说明redo日志产生的越早

buf_next_to_write

标记当前log buffer中 已经有哪些日志被刷新到磁盘中了。redo日志是 首先写到log buffer中,之后才会 被刷新到磁盘上的redo日志文件

flushed_to_disk_lsn

表示 刷新到磁盘中的redo日志量的全局变量,随着系统的运行,redo日志被 不断写入log buffer,但是 并不会立即刷新到磁盘,lsn的值就和flushed_to_disk_lsn的值 拉开了差距,如果 两者的值相同时,说明 log buffer中的所有redo日志都已经刷新到磁盘中了。
使用 SHOW ENGINE INNODB STATUS命令 查看当前InnoDB存储引擎中的各种LSN值的情况
  • Log sequence number:代表系统中的lsn值,也就是当前系统已经写入的redo日志量包括写入log buffer中的日志
  • Log flushed up to:代表flushed_to_disk_lsn的值,也就是当前系统已经写入磁盘的redo日志量
  • Pages flushed up to:代表flush链表中被最早修改的那个页面对应的oldest_modification属性值。
  • Last checkpoint at:当前系统的checkpoint_lsn值。

innodb_flush_log_at_trx_commit

当该系统变量值为0时,表示在事务提交时 不立即向磁盘中同步redo日志,这个任务是 交给后台线程做的。
这样很明显 会加快请求处理速度,但是如果事务提交后服务器挂了,后台线程没有及时将redo日志刷新到磁盘,那么该事务 对页面的修改会丢失
当该系统变量值为1时,表示在 事务提交时需要将redo日志同步到磁盘,可以 保证事务的持久性。1也是innodb_flush_log_at_trx_commit的 默认值

当该系统变量值为2时,表示在 事务提交时需要将redo日志写到操作系统的缓冲区中,但并 不需要保证将日志真正的刷新到磁盘
这种情况下如果数据库挂了, 操作系统没挂的话,事务的持久性还是可以保证的,但是操作系统也挂了的话,那就不能保证持久性了。

undo日志

事务需要 保证原子性,也就是事务中的操作 要么全部完成,要么什么也不做,事务执行过程中可能已经修改了很多东西,为了 保证事务的原子性,我们 需要把东西改回原先的样子,这个过程就称之为 回滚。
对一条记录做改动时(这里的改动可以指INSERT、DELETE、UPDATE),都需要 把回滚时所需的东西都给记下来为了回滚而记录的这些东西称之为 撤销日志,英文名为 undo log/undo日志
SELECT 并 不会修改任何用户记录,所以在查询操作执行时,并 不需要记录相应的undo日志

事务id

只读事务

通过START TRANSACTION READ ONLY语句开启一个只读事务,不可以对普通的表其他事务也能访问到的表进行增、删、改操作,但可以对用户临时表做增、删、改操作。

读写事务

通过 START TRANSACTION READ WRITE语句 开启一个读写事务,或者使用 BEGIN、START TRANSACTION语句开启的事务 默认也算是读写事务可以对表执行增删改查操作。

事务id分配方式

如果某个事务执行过程中对某个表执行了增、删、改操作,那么InnoDB存储引擎就会给它分配一个独一无二的事务id,分配方式如下:

  • 对于只读事务来说,只有在它第一次对某个用户创建的临时表执行增、删、改操作时才会为这个事务分配一个事务id,否则的话是不分配事务id的。
  • 对某个查询语句执行EXPLAIN分析它的查询计划时,有时候在Extra列会看到Using temporary的提示,这个表明在执行该查询语句时会用到内部临时表。这个所谓的内部临时表和我们手动用CREATE TEMPORARY TABLE创建的用户临时表并不一样,在事务回滚时并不需要把执行SELECT语句过程中用到的内部临时表也回滚,在执行SELECT语句用到内部临时表时并不会为它分配事务id
  • 对于读写事务来说,只有在它第一次对某个表(包括用户创建的临时表)执行增、删、改操作时才会为这个事务分配一个事务id,否则的话也是不分配事务id的。
  • 有的时候虽然我们开启了一个读写事务,但是在这个事务中全是查询语句,并没有执行增、删、改的语句,那也就意味着这个事务并不会被分配一个事务id。

事务id生成机制

本质上就是一个数字

  • 服务器会在内存中维护一个全局变量,每当需要为某个事务分配一个事务id时,就会把该变量的值当作事务id分配给该事务,并且把该变量自增1
  • 每当这个变量的值为256的倍数时,就会将该变量的值刷新到系统表空间的页号为5的页面中一个称之为Max Trx ID的属性处,这个属性占用8个字节的存储空间。
  • 当系统下一次重新启动时,会将上边提到的Max Trx ID属性加载到内存中,将该值加上256之后赋值给我们前边提到的全局变量(因为在上次关机时该全局变量的值可能大于Max Trx ID属性值)。
这样就可以保证 整个系统中分配的事务id值是一个递增的数字。先被分配id的事务得到的是较小的事务id,后被分配id的事务得到的是较大的事务id。

trx_id隐藏列

聚簇索引的记录除了会 保存完整的用户数据以外,而且还会 自动添加名为trx_id、roll_pointer的隐藏列,如果用户 没有在表中定义主键以及UNIQUE键,还会 自动添加一个名为row_id的隐藏列

trx_id列就是某个 对这个聚簇索引记录做改动的语句所在的事务对应的 事务id而已(此处的改动可以是 INSERT、DELETE、UPDATE操作)

undo日志的格式

一般 每对一条记录做一次改动,就 对应着一条undo日志,但在 某些更新记录的操作中,也 可能会对应着2条undo日志
这些 undo日志会被从0开始编号,这个 编号也被称 undo NO

FIL_PAGE_UNDO_LOG

FIL_PAGE_UNDO_LOG类型的 页面是专门用来 存储undo日志的。可以 从系统表空间中分配也可以从一种专门存放undo日志的表空间,也就是所谓的 undo tablespace中分配

INSERT操作对应的undo日志

TRX_UNDO_INSERT_REC 类型undo日志
当我们向某个表中插入一条记录时,实际上需要 向聚簇索引和所有的二级索引都插入一条记录。不过记录undo日志时,我们只需要考虑向聚簇索引插入记录时的情况就好了,因为其实 聚簇索引记录和二级索引记录是一一对应的,我们在回滚插入操作时, 只需要知道这条记录的主键信息,然后根据主键信息做对应的删除操作,做删除操作时就会 顺带着把所有二级索引中相应的记录也删除掉
roll_pointer
一个 指向记录对应的undo日志的一个指针, 记录被存储到了类型为FIL_PAGE_INDEX的页面中(就是我们前边一直所说的 数据页), undo日志被存放到了类型为FIL_PAGE_UNDO_LOG的页面中。roll_pointer本质就是一个指针, 指向记录对应的undo日志

DELETE操作对应的undo日志

一种称之为 TRX_UNDO_DEL_MARK_REC 类型的undo日志
被删除的记录其实也会根据记录头信息中的 next_record属性组成一个链表,只不过这个链表中的 记录占用的存储空间可以被重新利用,所以也称这个链表为 垃圾链表
Page Header部分有一个称之为 PAGE_FREE的属性,它 指向由被删除记录组成的 垃圾链表中的头节点

使用DELETE语句把正常记录链表中的最后一条记录给删除掉,其实这个删除的过程需要经历两个阶段:
阶段一: 将记录的delete_mask标识位设置为1,这个阶段称之为 delete mark
正常记录链表中的 最后一条记录的delete_mask值被设置为1,但是并没有被加入到垃圾链表。也就是此时 记录处于一个中间状态。在 删除语句所在的事务提交之前被删除的记录一直都处于这种所谓的中间状态。主要是为了实现MVCC的功能。 版本链

阶段二:当该 删除语句所在的事务提交之后,会有 专门的线程后来真正的把记录删除掉。所谓真正的删除就是把 该记录从正常记录链表中移除,并且 加入到垃圾链表的头节点中,然后还要 调整一些页面的其他信息,比如页面中的 用户记录数量PAGE_N_RECS上次插入记录的位置PAGE_LAST_INSERT垃圾链表头节点的指针PAGE_FREE页面中可重用的字节数量PAGE_GARBAGE、还有 页目录的一些信息等等。这个阶段称之为 purge
阶段二执行完了,这条记录就算是 真正的被删除掉了。这条已删除记录 占用的存储空间也可以被重新利用了。

在删除语句所在的 事务提交之前,只会经历阶段一,也就是delete mark阶段(提交之后我们就不用回滚了,所以只需考虑对删除操作的阶段一做的影响进行回滚)

版本链

同时,在对一条记录进行delete mark操作前,需要把该记录的旧的trx_id和roll_pointer隐藏列的值都给记到对应的undo日志中来,就是我们图中显示的old trx_id和old roll_pointer属性。这样有一个好处,那就是可以通过undo日志的old roll_pointer找到记录在修改之前对应的undo日志。比方说在一个事务中,我们先插入了一条记录,然后又执行对该记录的删除操作,这个过程的示意图就是这样:

从图中可以看出来,执行完delete mark操作后,它对应的undo日志和INSERT操作对应的 undo日志就串成了一个链表。这个链表就称之为 版本链

UPDATE操作对应的undo日志

不更新主键的情况
一种类型为 TRX_UNDO_UPD_EXIST_RECundo日志
就地更新:如果更新后的列和更新前的列 占用的存储空间都一样大,那么就可以进行就地更新,也就是 直接在原记录的基础上修改对应列的值。更新前比更新后占用的存储空间不一样就不能进行就地更新。
先删除掉旧记录,再插入新记录:如果有任何一个被更新的列更新前和更新后占用的 存储空间大小不一致,那么就需要先把这条 旧的记录从聚簇索引页面中删除掉,然后再根据更新后列的值 创建一条新的记录插入到页面中。
这里所说的删除并不是delete mark操作,而是 真正的删除掉,也就是把这条记录 从正常记录链表中移除并加入到垃圾链表中,并且 修改页面中相应的统计信息(比如PAGE_FREE、PAGE_GARBAGE等这些信息)。由用户线程同步执行真正的删除操作,真正删除之后紧接着就要 根据各个列更新后的值创建的新记录插入。
这里如果新创建的记录占用的存储空间大小 不超过旧记录占用的空间,那么可以 直接重用被加入到垃圾链表中的 旧记录所占用的存储空间,否则的话需要在页面中 新申请一段空间以供新记录使用,如果本页面内已经 没有可用的空间的话,那就需要 进行页面分裂操作,然后 再插入新记录
更新主键的情况
在对该记录进行 delete mark操作前,会记录一条类型为 TRX_UNDO_DEL_MARK_REC的undo日志;
之后 插入新记录时,记录一条类型为 TRX_UNDO_INSERT_RECundo日志。
每对一条记录的主键值做改动时,会 记录2条undo日志
将旧记录进行delete mark操作
在UPDATE语句所在的 事务提交前,对旧记录 只做一个delete mark操作,在事务提交后才 由专门的线程做purge操作,把它加入到垃圾链表中。这里 一定要和我们上边所说的在不更新记录主键值时,先真正删除旧记录,再插入新记录的方式区分开!
之所以只对旧记录做delete mark操作,是因为 别的事务同时也可能访问这条记录如果把它真正的删除加入到垃圾链表后, 别的事务就访问不到了。这个功能就是所谓的MVCC。

创建一条新记录

根据更新后各列的值 创建一条新记录,并将其 插入到聚簇索引中(需 重新定位插入的位置)。
由于更新后的记录 主键值发生了改变,所以需要 重新从聚簇索引中定位这条记录所在的位置,然后把它插进去。

事务执行

MySQL的事务主要主要是通过 Redo Log和 Undo Log实现的。

MySQL在事务执行的过程中,会记录相应SQL语句的UndoLog 和 Redo Log,然后在内存中更新数据并形成数据脏页。接下来RedoLog会根据一定规则触发刷盘操作,Undo Log 和数据脏页则通过刷盘机制刷盘事务提交时,会将当前事务相关的所有Redo Log刷盘,只有当前事务相关的所有Redo Log 刷盘成功事务才算提交成功

事务恢复

如果事务在执行第8步,即事务提交之前,MySQL 崩溃或者宕机,此时会先使用Redo Log恢复数据,然后使用Undo Log回滚数据

如果在执行第8步之后MySQL崩溃或者宕机,此时会使用Redo Log恢复数据,大体流程如下图所示。        

MySQL崩溃恢复后,首先会 获取日志检查点信息,随后 根据日志检查点信息 使用Redo Log进行恢复。MySQL崩溃或者宕机时 事务未提交,则接下来使用 Undo Log回滚数据。如果在MySQL崩溃或者宕机时 事务已经提交,则 用Redo Log恢复数据即可。

恢复机制

在服务器不挂的情况下,redo日志简直就是个大累赘,不仅没用,反而让性能变得更差。但是万一数据库挂了,就可以在重启时根据redo日志中的记录就可以将页面恢复到系统崩溃前的状态。

MySQL可以 根据redo日志中的各种LSN值,来 确定恢复的起点和终点。然后 将redo日志中的数据,以 哈希表的形式,将 一个页面下的放到哈希表的一个槽中。之后就可以遍历哈希表,因为对同一个页面进行修改的redo日志都放在了一个槽里,所以可以 一次性将一个页面修复好避免了很多读取页面的随机IO)。并且通过各种机制,避免无谓的页面修复,比如已经刷新的页面,进而提升崩溃恢复的速度。

崩溃后的恢复为什么不用binlog

  • 这两者使用方式不一样binlog 是用作人工恢复数据redo log 是 MySQL 自己使用,用于保证在数据库崩溃时的事务持久性
  • redo log 是 InnoDB 引擎特有的,binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用
  • redo log是物理日志,记录的是“在某个数据页上做了什么修改”,恢复的速度更快;binlog是逻辑日志记录的是这个语句的原始逻辑,比如“给ID=2这的c字段加1 ” ;
  • redo log是“循环写”的日志文件,redo log 只会记录未刷盘的日志,已经刷入磁盘的数据都会从 redo log 这个有限大小的日志文件里删除。binlog 是追加日志保存的是全量的日志
  • 当数据库 crash 后,想要恢复未刷盘但已经写入 redo log 和 binlog 的数据到内存时,binlog 是无法恢复的。虽然 binlog 拥有全量的日志,但没有一个标志让 innoDB 判断哪些数据已经入表(写入磁盘),哪些数据还没有

Redo日志和Undo日志的关系

  1. 数据库崩溃重启后,需要先从redo log中把未落盘的脏页数据恢复回来,重新写入磁盘保证用户的数据不丢失。当然,在崩溃恢复中还需要把未提交的事务进行回滚操作。由于回滚操作需要undo log日志支持undo log日志的完整性和可靠性需要redo log日志来保证,所以数据库崩溃需要先做redo log数据恢复,然后undo log回滚
  2. 事务执行过程中,除了记录redo一些记录,还会记录undo log日志。Undo log记录了数据每个操作前的状态,如果事务执行过程中需要回滚,就可以根据undo log进行回滚操作
  3. 因为redo log是物理日志,记录的是数据库页的物理修改操作。所以undo log(可以看成数据库的数据)的写入也会伴随着redo log的产生,这是因为undo log也需要持久化的保护
  4. 事务进行过程中,每次sql语句执行,都会记录undo log和redo log,然后更新数据形成脏页事务执行COMMIT操作时,会将本事务相关的所有redo log进行落盘,只有所有的redo log落盘成功,才算COMMIT成功。然后内存中的undo log和脏页按照同样的规则进行落盘。如果此时发生崩溃,则只使用redo log恢复数据

同时写Redo和Binlog怎么保持一致

1)当事务提交时InnoDB存储引擎 进行prepare操作
2) MySQL上层会将数据库、数据表和数据表中的数据的更新操作写入BinLog文件
3) InnoDB存储引擎将事务日志写入Redo Log文件中

MySQL8中的新增特性

索引

隐藏索引

被隐藏的索引不会被优化器使用,但 依然真实存在,主要 用于软删除,可以 根据需要后续真正删除或者重新可视化。我们可以隐藏一个索引,然后观察对数据库的影响。如果数据库性能有所下降,就说明这个索引是有用的,于是将其“恢复显示”即可;如果数据库性能看不出变化,说明这个索引是多余的,可以考虑去除了。当索引被隐藏时,它的内容仍然是和正常索引一样实时更新的,这个特性本身是专门为优化调试使用。如果你长期隐藏一个索引,那还不如干脆删掉,因为毕竟索引的存在会影响插入、更新和删除的性能。

降序索引

以往的MySQL虽然支持降序索引,但是写盘的时候依然是升序保存。MySQL8.0中则是真正的按降序保存。

不再对group by操作进行隐式排序

索引中可以使用函数表达式

创建表时创建一个函数索引,查询的时候使用同样的函数就可以利用索引了。

默认字符集

默认 字符集由latin1变为utf8mb4

InnoDB性能提升

行缓存

MySQL8.0的优化器可以估算将要读取的行数,因此可以提供给存储引擎一个合适大小的row buffer来存储需要的数据。大批量的连续数据扫描的性能将受益于更大的record buffer

改进扫描性能

改进InnoDB范围查询的性能,可提升全表查询和范围查询  5-20%的性能

改进成本模型

InnoDB缓冲区可以估算缓存区中的有多少表和索引,这可以让优化器选择访问方式时知道数据是否可以存储在内存中还是必须存储到磁盘上。

移除了一些功能,例如query cache

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值