Redis面试题总结

1.什么是Redis

Redis 是一种基于内存的数据库对数据的读写操作都是在内存中完成,因此读写速度非常快,常用于缓存,消息队列、分布式锁等场景。

Redis 提供了多种数据类型来支持不同的业务场景,比如 String(字符串)、Hash(哈希)、 List(列表)Set(集合)、Zset(有序集合)、Bitmaps (位图) 、HyperLogLog (基数统计) 、GEO (地理信息)Stream (流),并且对数据类型的操作都是原子性的,因为执行命令由单线程负责的,不存在并发竞争的问题。

除此之外,Redis 还支持事务 、持久化、Lua 脚本、多种集群方案 (主从复制模式、哨兵模式、切片机群模式)、发布/订阅模式,内存淘汰机制、过期删除机制等等。

Redis官方简介

Redis 是一个开源 (BSD 许可)内存数据结构存储用作数据库、缓存、消息代理和流引警。Redis 提供数据结构,例如 字符串、散列、列表集合、带范围查询的排序集合、位图、超日志、地理空间索引和流。Redis 内置了复制、Lua 脚本、LRU 驱逐、事务和不同级别的磁盘持久性,并通过以下方式提供高可用性Redis Sentinel和Redis Cluster的自动分区。

您可以对这些类型运行原子操作例如附加到字符串; 增加哈希值;将元素推入列表,计算集交、并 、差;或获取排序集中排名最高的成员。

为了达到最佳性能,Redis 使用 内存中的数据集。根据您的用例,Redis 可以通过定期将数据集转储到磁盘 或将每个命令附加到基于磁盘的日志来持久化您的数据。如果您只需要一个功能丰富的网络内存缓存,您也可以禁用持久性。

Redis 支持异步复制,具有快速非阻塞同步和自动重新连接以及网络拆分上的部分重新同步

2.Redis和Memcached有什么区别

很多人都说用 Redis 作为缓存,但是 Memcached 也是基于内存的数据库,为什么不选择它作为缓存呢? 要解答这个问题,我们就要弄清楚 Redis 和 Memcached 的区别。

Redis 与 Memcached 共同点:

1、都是基于内存的数据库,一般都用来当做缓存使用;

2、都有过期策略;

3、两者的性能都非常高;

Redis 与 Memcached 区别:

1、Redis 支持的数据类型更丰富 (String、Hash、List、Set、ZSet),而 Memcached 只支持最简单的 key-value 数据类型;

2、Redis 支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用而 Memcached 没有持久化功能,数据全部存在内存之中,Memcached 重启或挂掉后,数据会丢失;

3、Redis 原生文持集群模式,Memcached 没有原生的集模需要依靠客尸端来实现往集群中分片写入数据;

4、Redis 支持发布订阅模型、Lua 脚本、事务等功能,而 Memcached 不支持。

3.为什么用Redis作为MySQL的缓存

主要是因为Redis具备高性能和高并发两种特性。

1、Redis具备高性能

假如用户第一次访问 MySQL 中的某些数据,这个过程会比较慢,因为是从硬盘上读取的;将该用户访问的数据缓存在 Redis 中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了,操作 Redis缓存就是直接操作内存,所以速度相当快。

如果 MySQL 中的对应数据改变的之后,同步改变 Redis 缓存中相应的数据即可,不过这里会有Redis 和 MySQL 双写一致性的问题,后面我们会提到。

2、Redis具备高并发

单台设备的 Redis 的 QPS (Query Per Second,每秒钟处理完请求的次数) 是 MySQL 的 10 倍Redis 单机的 QPS 能轻松破 10w,而 MySQL 单机的 QPS 很难破 1w。

所以,直接访问 Redis 能够承受的请求是远远大于直接访问 MySQL 的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。

4.Redis数据结构

Redis数据类型以及使用场景分别是什么

Redis 提供了丰富的数据类型,常见的有五种数据类型: String (字符串) ,Hash (哈希),List (列表),Set (集合) 、Zset (有序集合)。

结构类型结构存储的值结构的读写能力
String字符串可以是字符串、整数或浮点数对整个字符串或字符串的一部分进行操作;对整数或浮点数进行自增或自减操作;
List列表一个链表,链表上的每个节点都包含一个字符串对链表的两端进行push和pop操作,读取单个或多个元素;根据值查找或删除元素;
Set集合包含字符串的无序集合字符串的集合,包含基础的方法有看是否存在添加、获取、删除;还包含计算交集、并集、差集等
Hash散列包含键值对的无序散列表包含方法有添加、获取、删除单个元素
Zset有序集合和散列一样,用于存储键值对字符串成员与浮点数分数之间的有序映射;元素的排列顺序由分数的大小决定,包含方法有添加、获取、删除单个元素以及根据分值范围或成员来获取元素

随着 Redis 版本的更新,后面又支持了四种数据类型:BitMap (2.2 版新增) 、HyperLogLog (2.8 版 新增)、Redis 五种数据类型的应用场景:GEO (3.2 版新增) 、Stream (5.0 版新增) 。

String类型的应用场景: 缓存对象、常规计数、分布式锁、共享 session 信息等。

List 类型的应用场景:消息队列(但是有两个问题: 1.生产者需要自行实现全局唯一 ID;2.不能以消费组形式消费数据)等。

Hash 类型: 缓存对象、购物车等。

Set 类型: 聚合计算(并集、交集、差集)场景,比如点赞、共同关注、抽奖活动等.

Zset 类型: 排序场景,比如排行榜、电话和姓名排序等

Redis 后续版本又支持四种数据类型,它们的应用场景如下:

BitMap (2.2 版新增): 二值状态统计的场景,比如签到、判断用户登陆状态、连续签到用户总数 等;

HyperLogLog (2.8 版新增) : 海量数据基数统计的场景,比如百万级网页 UV 计数等;

GEO (3.2 版新增) : 存储地理位置信息的场景,比如滴滴叫车;

Stream (5.0 版新增): 消息队列,相比于基于 List 类型实现的消息队列,有这两个特有的特性:自动生成全局唯一消息ID,支持以消费组形式消费数据。

五种常见的 Redis 数据类型是怎么实现

String类型的内部实现

String 类型的底层的数据结构实现主要是 SDS(简单动态字符串)。 SDS 和我们认识的 C 字符串不太一样,之所以没有使用 C 语言的字符串表示,因为 SDS 相比于C的原生字符串:

1、SDS 不仅可以保存文本数据,还可以保存二进制数据。因为 SDS 使用len 属性的值而不是空字符来判断字符串是否结束,并且SDS的所有API都会以处理二进制的方式来处理SDS存放在buf 数组的数据。所以SDS不光能存放文本数据,而且能保存图片、音频、视频、压缩文件这样的二进制数据;

2、SDS 获取字符串长度的时间复杂度是 O(1)。因为 C语言的字符串并不记录自身长度,所以获取长度的复杂度为 O(n);而SDS结构里用len 属性记录了字符串长度,所以复杂度为 O(1);

3、Redis 的 SDS API 是安全的,拼接字符串不会造成缓冲区溢出。因为 SDS 在拼接字符串之前会检查SDS空间是否满足要求,如果空间不够会自动扩容,所以不会导致缓冲区溢出的问题。

List类型内部实现

List 类型的底层数据结构是由双向链表压缩列表实现的:

1、如果列表的元素个数小于 512个(默认值,可由 list-max-ziplist-entries 配置),列表每个素的值都小于 64 字节(默认值,可由 list-max-ziplist-value 配置),Redis 会使用压缩列表作为 List 类型的底层数据结构;

2、如果列表的元素不满足上面的条件,Redis 会使用双向链表作为 List 类型的底层数据结构;

但是在 Redis 3.2 版本之后,List 数据类型底层数据结构就只由 quicklist 实现了,替代了双向链表和压缩列表。

Hash类型内部实现

Hash 类型的底层数据结构是由压缩列表哈希表实现的:

1、如果哈希类型元素个数小于512个(默认值,可由 hash-max-ziplist-entries 配置),所有值小于64字节(默认值,可由hash-max-ziplist-value 配置)的话,Redis 会使用压缩列表作为 Hash 类型的底层数据结构;

2、如果哈希类型元素不满足上面条件,Redis 会使用哈希表作为 Hash 类型的底层数据结构。

在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了。

Set内部实现

Set 类型的底层数据结构是由哈希表整数集合实现的:

1、如果集合中的元素都是整数且元素个数小于 512 (默认值,set-maxintset-entries配置)个,Redis会使用整数集合作为 Set 类型的底层数据结构;

2、如果集合中的元素不满足上面条件,则 Redis 使用哈希表作为 Set 类型的底层数据结构。

ZSet类型内部实现

Zset 类型的底层数据结构是由压缩列表或跳表实现的:

1、如果有序集合的元素个数小于 128 个,并且每个元素的值小于 64 字节时,Redis 会使用压缩列表作为 Zset 类型的底层数据结构;

2、如果有序集合的元素不满足上面的条件,Redis 会使用跳表作为 Zset 类型的底层数据结构。

5.Redis持久化

Redis 是内存数据库,如果不将内存中的数据库状态保存到磁盘,那么一旦服务器进程退出,服务器中的数据库状态也会消失。所以 Redis 提供了持久化功能!

持久化过程保存什么

1、将当前数据状态进行保存,快照形式,存储数据结果,存储格式简单,关注点在数据 (RDB)

2、将数据的操作过程进行保存,日志形式,存储操作过程,关注点在数据的操作过程(AOF)

一、RDB方式

在指定的时间间隔内将内存中的数据集快照写入磁盘, 也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里。

1、RDB手动

使用save指令

2、RDB自动

配置 :save second changes

作用 : 满足限定时间范围内key的变化数量达到指定数量即进行持久化

参数 :

second:监控时间范围

changes:监控key的变化量

位置 : 在conf文件中进行配置

注意:

1、save配置要根据实际业务情况进行设置,频度过高或过低都会出现性能问题,结果可能是灾难性的

2、save配置中对于second与changes设置通常具有互补对应关系,尽量不要设置成包含性关系

3、save配置启动后执行的是bgsave操作。

RDB优点

RDB是一个紧凑压缩的二进制文件,存储效率较高

RDB内部存储的是redis在某个时间点的数据快照,非常适合用于数据备份,全量复制等场景

RDB恢复数据的速度要比AOF快很多

RDB节省磁盘空间

RDB缺点

Fork的时候,内存中的数据被克隆了一份,大致2倍的膨胀性需要考虑

虽然Redis在fork时使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能

RDB方式无论是执行指令还是利用配置,无法做到实时持久化,具有较大的可能性丢失数据Redis的众多版本中未进行RDB文件格式的版本统一,有可能出现各版本服务之间数据格式无法兼容现象

二、AOF方式

AOF(append only fifile)持久化:以独立日志的方式记录每次写命令,重启时再重新执行AOF文件中命令达到恢复数据的目的;与RDB相比可以简单描述为改记录数据为记录数据产生的过程AOF的主要作用是解决了数据持久化的实时性,目前已经是Redis持久化的主流方式。

AOF执行过程

客户端的请求写命令会被append追加到AOF缓冲区内;

AOF缓冲区根据AOF持久化策略[always,everysec,no]将操作sync同步到磁盘的AOF文件中;

AOF文件大小超过重写策略或手动重写时,会对AOF文件rewrite重写,压缩AOF文件容量;

Redis服务重启时,会重新load加载AOF文件中的写操作达到数据恢复的目的;

AOF写数据三种策略(appendfsync)

always(每次):每次写入操作均同步到AOF文件中,数据零误差,性能较低

everysec(每秒):每秒将缓冲区中的指令同步到AOF文件中,数据准确性较高,性能较高 在系统突然宕机的情况下丢失1秒内的数据

no(系统控制):由操作系统控制每次同步到AOF文件的周期,整体过程不可控

6.Redis删除策略

1.过期数据

Redis是一种内存级数据库,所有数据均存放在内存中,内存中的数据可以通过TTL指令获取其状态

xx:具有时效性的数据,-1:永久有效的数据,-2:已经过期的数据或被删除的数据或未定义的数据

2.数据删除策略

数据删除策略的目标

在内存占用与CPU占用之间寻找一种平衡,顾此失彼都会造成整体redis性能的下降,甚至引发服务器宕机或内存泄露

2.1.定时删除

创建一个定时器,当key设置有过期时间,且过期时间到达时,由定时器任务立即执行对键的删除操作:

优点:节约内存,到时就删除,快速释放掉不必要的内存占用

缺点:CPU压力很大,无论CPU此时负载量多高,均占用CPU,会影响redis服务器响应时间和指令吞吐量

总结:用处理器性能换取存储空间(拿时间换空间)

2.2.惰性删除

数据到达过期时间,不做处理。等下次访问该数据时

1、如果未过期,返回数据;

2、发现已过期,删除,返回不存在

优点:节约CPU性能,发现必须删除的时候才删除

缺点:内存压力很大,出现长期占用内存的数据

总结:用存储空间换取处理器性能(拿空间换时间)

2.3.定期删除

两种方案都走极端,有没有折中方案

Redis启动服务器初始化时,读取配置server.hz的值,默认为10

每秒钟执行server.hz次serverCron()中的方法---databasesCron()---activeExpireCycle()

activeExpireCycle()对每个expires[*]逐一进行检测,每次执行250ms/server.hz

对某个expires[*]检测时,随机挑选W个key检测:

1、如果key超时,删除key

2、如果一轮中删除的key的数量>W * 25%,循环该过程

3、如果一轮中删除的key的数量≤W * 25%,检查下一个expires[*],0-15循环

4、W取值=ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP属性值

参数current_db用于记录activeExpireCycle() 进入哪个expires[*] 执行

如果activeExpireCycle()执行时间到期,下次从current_db继续向下执行

定期删除:周期性轮询redis库中的时效性数据,采用随机抽取的策略,利用过期数据占比的方式控制删除频度:

优点1:CPU性能占用设置有峰值,检测频度可自定义设置;

优点2:内存压力不是很大,长期占用内存的冷数据会被持续清理

总结:周期性抽查存储空间 (随机抽查,重点抽查)

2.4.删除策略比对

  1. 定时删除 节约内存,无占用 不分时段占用CPU资源,频度高 拿时间换空间

  2. 惰性删除 内存占用严重 延时执行,CPU利用率高 拿空间换时间

  3. 定期删除 内存定期随机清理 每秒花费固定的CPU资源维护内存 随机抽查,重点抽查

3.逐出算法

当新数据进入redis时,如果内存不足怎么办

1、Redis使用内存存储数据,在执行每一个命令前,会调用freeMemoryIfNeeded()检测内存是否充足。如果内存不满足新加入数据的最低存储要求,redis要临时删除一些数据为当前指令清理存储空间。清理数据的策略称为逐出算法

2、注意:逐出数据的过程不是100%能够清理出足够的可使用的内存空间,如果不成功则反复执行。当对所有数据尝试完毕后,如果不能达到内存清理的要求,将出现错误信息。

抛出异常:(error) OOM command not allowed when used memory >'maxmemory'

影响数据逐出的相关配置

1、maxmemory最大可使用内存

占用物理内存的比例,默认值为0,表示不限制,生产环境中根据需求设定,通常设置在50%以上。

2、maxmemory-samples每次选取待删除数据的个数

选取数据时并不会全库扫描,导致严重的性能消耗,降低读写性能。因此采用随机获取数据的方式作为待检测删除数据。

3、maxmemory-policy删除策略

检测易失数据(可能会过期的数据集server.db[i].expires)

1、volatile-lru:挑选最近最少使用的数据淘汰;

2、 volatile-lfu:挑选最近使用次数最少的数据淘汰;

3、volatile-ttl:挑选将要过期的数据淘汰;

4、volatile-random:任意选择数据淘汰。

检测全库数据(所有数据集server.db[i].dict

5、allkeys-lru:挑选最近最少使用的数据淘汰;

6、allkeys-lfu:挑选最近使用次数最少的数据淘汰;

7、allkeys-random:任意选择数据淘汰。

放弃数据驱逐

8、 no-enviction(驱逐):禁止驱逐数据(redis4.0中默认策略),会引发错误OOM(Out Of Memory)达到最大内存后的,对被挑选出来的数据进行删除的策略

7.企业级解决方案

1、缓存预热

宕机:服务器启动后迅速宕机

问题排查:1、请求数量较高

2、主从之间数据吞吐量较大,数据同步操作频度较高,因为刚刚启动时,缓存中没任何数据

解决方案:

1、日常例行统计数据访问记录,统计访问频度较高的热点数据

2、将统计结果中的数据分类,根据级别,redis优先加载级别较高的热点数据

实施:

1、使用脚本程序固定触发数据预热过程

2、 如果条件允许,使用了CDN(内容分发网络),效果会更好

缓存预热就是系统启动前,提前将相关的缓存数据直接加载到缓存系统。避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据

2、缓存雪崩

缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。

解决方案:

1、给不同的Key的TTL添加随机值;

2、利用Redis集群提高服务的可用性;

3、给缓存业务添加降级限流策略;

4、给业务添加多级缓存。

3、缓存击穿

缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。

解决方案:

1、互斥锁;

逻辑分析:假设线程1在查询缓存之后,本来应该去查询数据库,然后把这个数据重新加载到缓存的,此时只要线程1走完这个逻辑,其他线程就都能从缓存中加载这些数据了,但是假设在线程1没有走完的时候,后续的线程2,线程3,线程4同时过来访问当前这个方法, 那么这些线程都不能从缓存中查询到数据,那么他们就会同一时刻来访问查询缓存,都没查到,接着同一时间去访问数据库,同时的去执行数据库代码,对数据库访问压力过大

解决方案一、使用锁来解决:因为锁能实现互斥性。假设线程过来,只能一个人一个人的来访问数据库,从而避免对于数据库访问压力过大,但这也会影响查询的性能,因为此时会让查询的性能从并行变成了串行,我们可以采用tryLock方法 + double check来解决这样的问题

假设现在线程1过来访问,他查询缓存没有命中,但是此时他获得到了锁的资源,那么线程1就会一个人去执行逻辑,假设现在线程2过来,线程2在执行过程中,并没有获得到锁,那么线程2就可以进行到休眠,直到线程1把锁释放后,线程2获得到锁,然后再来执行逻辑,此时就能够从缓存中拿到数据了。

2、逻辑过期

解决方案二、逻辑过期方案

方案分析:我们之所以会出现这个缓存击穿问题,主要原因是在于我们对key设置了过期时间,假设我们不设置过期时间,其实就不会有缓存击穿的问题,但是不设置过期时间,这样数据不就一直占用我们内存了吗,我们可以采用逻辑过期方案。

我们把过期时间设置在 redis的value中,注意:这个过期时间并不会直接作用于redis,而是我们后续通过逻辑去处理。假设线程1去查询缓存,然后从value中判断出来当前的数据已经过期了,此时线程1去获得互斥锁,那么其他线程会进行阻塞,获得了锁的线程他会开启一个 线程去进行 以前的重构数据的逻辑,直到新开的线程完成这个逻辑后,才释放锁, 而线程1直接进行返回,假设现在线程3过来访问,由于线程线程2持有着锁,所以线程3无法获得锁,线程3也直接返回数据,只有等到新开的线程2把重建数据构建完后,其他线程才能走返回正确的数据

这种方案巧妙在于,异步的构建缓存,缺点在于在构建完缓存之前,返回的都是脏数据。

进行对比

互斥锁方案:由于保证了互斥性,所以数据一致,且实现简单,因为仅仅只需要加一把锁而已,也没其他的事情需要操心,所以没有额外的内存消耗,缺点在于有锁就有死锁问题的发生,且只能串行执行性能肯定受到影响

逻辑过期方案: 线程读取过程中不需要等待,性能好,有一个额外的线程持有锁去进行重构数据,但是在重构数据完成前,其他的线程只能返回之前的数据,且实现起来麻烦

4、缓存穿透

缓存穿透 :缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。

常见的解决方案有两种:

1、缓存空对象:优点:实现简单,方便维护。缺点:额外的内存消耗,可能造成短期的不一致;

2、布隆过滤:优点:内存占有较少。没有多余的key。缺点:实现复杂,存在误判可能。

缓存空对象思路分析:当我们客户端访问不存在的数据时,先请求redis,但是此时redis中没有数据,此时会访问到数据库,但是数据库中也没有数据,这个数据穿透了缓存,直击数据库,我们都知道数据库能够承载的并发不如redis这么高,如果大量的请求同时过来访问这种不存在的数据,这些请求就都会访问到数据库,简单的解决方案就是哪怕这个数据在数据库中也不存在,我们也把这个数据存入到redis中去,这样,下次用户过来访问这个不存在的数据,那么在redis中也能找到这个数据就不会进入到缓存了

布隆过滤:布隆过滤器其实采用的是哈希思想来解决这个问题,通过一个庞大的二进制数组,走哈希思想去判断当前这个要查询的这个数据是否存在,如果布隆过滤器判断存在,则放行,这个请求会去访问redis,哪怕此时redis中的数据过期了,但是数据库中一定存在这个数据,在数据库中查询出来这个数据后,再将其放入到redis中,假设布隆过滤器判断这个数据不存在,则直接返回;

这种方式优点在于节约内存空间,存在误判,误判原因在于:布隆过滤器走的是哈希思想,只要哈希思想,就可能存在哈希冲突

8.事务

8.1 概念

Redis事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。Redis事务的主要作用就是串联多个命令防止别的命令插队;

8.2 特点

1、Redis事务没有隔离级别的概念;

2、所有的命令在事务中,并没有直接被执行,只有发起执行命令的时候才会执行;

3、Redis单条命令是保存原子性的,但是事务不保证原子性

常用命令

命令描述
multi标记一个事务的开始
exec执行所有事务块内的命令
discard取消事务,放弃执行事务块内的所有命令
watch key [key]监视一个或多个类,如果在事务执行之前这个或这些key 被其他命令所改动,那么事务将被打断。类似乐观锁
unwatch取消watch命令对所有 key 的监视。

8.3 为什么添加事务

悲观锁

悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁;

乐观锁

乐观锁(Optimistic Lock) 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

码农叮叮车

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

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

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

打赏作者

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

抵扣说明:

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

余额充值