阅读《Redis深度历险:核心原理和应用实践》的总结-02

续接上一篇:阅读《Redis深度历险:核心原理和应用实践》的总结-01_uuboke的博客-CSDN博客

5、布隆过滤器

  讲个使用场景,比如我们在使用新闻客户端看新闻时,它会给我们不停地推荐新的内容,它每次推荐时要去重,去掉那些已经看过的内容。问题来了,新闻客户端推荐系统如何实现推送去重的?
   你会想到服务器记录了用户看过的所有历史记录,当推荐系统推荐新闻时会从每个用户的历史记录里进行筛选,过滤掉那些已经存在的记录。问题是当用户量很大,每个用户看过的新闻又很多的情况下,这种方式,推荐系统的去重工作在性能上跟的上么?

实际上,如果历史记录存储在关系数据库里,去重就需要频繁地对数据库进行 exists 查 询,当系统并发量很高时,数据库是很难扛住压力的。你可能又想到了缓存,但是如此多的历史记录全部缓存起来,那得浪费多大存储空间 啊?而且这个存储空间是随着时间线性增长,你撑得住一个月,你能撑得住几年么?但是不 缓存的话,性能又跟不上,这该怎么办?

这时,布隆过滤器 (Bloom Filter) 闪亮登场了,它就是专门用来解决这种去重问题的。 它在起到去重的同时,在空间上还能节省 90% 以上,只是稍微有那么点不精确,也就是有 一定的误判概率。

布隆过滤器是什么?

布隆过滤器可以理解为一个不怎么精确的 set 结构,当你使用它的 contains 方法判断某 个对象是否存在时,它可能会误判但是布隆过滤器也不是特别不精确,只要参数设置的合理,它的精确度可以控制的相对足够精确,只会有小小的误判概率。

本质上布隆过滤器是一种数据结构,比较巧妙的概率型数据结构(probabilistic data structure),特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”。

当布隆过滤器说某个值存在时,这个值可能不存在;当它说不存在时,那就肯定不存 在。

套在上面的使用场景中,布隆过滤器能准确过滤掉那些已经看过的内容,那些没有看过 的新内容,它也会过滤掉极小一部分 (误判),但是绝大多数新内容它都能准确识别。这样就 可以完全保证推荐给用户的内容都是无重复的。

Redis 中的布隆过滤器

Redis 官方提供的布隆过滤器到了 Redis 4.0 提供了插件功能之后才正式登场。布隆过滤 器作为一个插件加载到 Redis Server 中,给 Redis 提供了强大的布隆去重功能。

布隆过滤器基本使用

布隆过滤器有二个基本指令,bf.add 添加元素,bf.exists 查询元素是否存在,它的用法 和 set 集合的 sadd 和 sismember 差不多。注意 bf.add 只能一次添加一个元素,如果想要 一次添加多个,就需要用到 bf.madd 指令。同样如果需要一次查询多个元素是否存在,就需 要用到 bf.mexists 指令。

⚠️注意事项

布隆过滤器只是默认参数的布隆过滤器,它在我们第一次 add 的时候自动创建。Redis 其实还提供了自定义参数的布隆过滤器,需要我们在 add 之前使用 bf.reserve 指令显式创建。如果对应的 key 已经存在,bf.reserve 会报错。bf.reserve 有三个参数,分别是 key, error_rate 和 initial_size。错误率越低,需要的空间越大。initial_size 参数表示预计放 入的元素数量,当实际数量超出这个数值时,误判率会上升。

所以需要提前设置一个较大的数值避免超出导致误判率升高。如果不使用 bf.reserve,默认的 error_rate 是 0.01,默认的 initial_size 是 100。

布隆过滤器的 initial_size 估计的过大,会浪费存储空间,估计的过小,就会影响准确率,用户在使用之前一定要尽可能地精确估计好元素数量,还需要加上一定的冗余空间以避 免实际元素可能会意外高出估计值很多。

布隆过滤器的 error_rate 越小,需要的存储空间就越大,对于不需要过于精确的场合, error_rate 设置稍大一点也无伤大雅。比如在新闻去重上而言,误判率高一点只会让小部分文 章不能让合适的人看到,文章的整体阅读量不会因为这点误判率就带来巨大的改变。

支持删除么,不能删除

目前我们知道布隆过滤器可以支持 add 和 isExist 操作,那么 delete 操作可以么,答案是不可以,例如加入一个值的 bit 位 4 被两个值共同覆盖的话,一旦你删除其中一个值例如 “tencent” 而将其置位 0,那么下次判断另一个值例如 “baidu” 是否存在的话,会直接返回 false,而实际上你并没有删除它。

如何解决这个问题,答案是计数删除。但是计数删除需要存储一个数值,而不是原先的 bit 位,会增大占用的内存大小。这样的话,增加一个值就是将对应索引槽上存储的值加一,删除则是减一,判断是否存在则是看值是否大于0。

布隆过滤器的原理

每个布隆过滤器对应到 Redis 的数据结构里面就是一个大型的位数组和几个不一样的无 偏 hash 函数。所谓无偏就是能够把元素的 hash 值算得比较均匀。

向布隆过滤器中添加 key 时,会使用多个 hash 函数对 key 进行 hash 算得一个整数索 引值然后对位数组长度进行取模运算得到一个位置,每个 hash 函数都会算得一个不同的位 置。再把位数组的这几个位置都置为 1 就完成了 add 操作。

向布隆过滤器询问 key 是否存在时,跟 add 一样,也会把 hash 的几个位置都算出 来,看看位数组中这几个位置是否都位 1,只要有一个位为 0,那么说明布隆过滤器中这个 key 不存在。如果都是 1,这并不能说明这个 key 就一定存在,只是极有可能存在,因为这 些位被置为 1 可能是因为其它的 key 存在所致。如果这个位数组比较稀疏,这个概率就会 很大,如果这个位数组比较拥挤,这个概率就会降低。具体的概率计算公式比较复杂,感兴 趣可以阅读扩展阅读,非常烧脑,不建议读者细看。

使用时不要让实际元素远大于初始化大小,当实际元素开始超出初始化大小时,应该对 布隆过滤器进行重建,重新分配一个 size 更大的过滤器,再将所有的历史元素批量 add 进 去 (这就要求我们在其它的存储器中记录所有的历史元素)。因为 error_rate 不会因为数量超 出就急剧增加,这就给我们重建过滤器提供了较为宽松的时间。

空间占用估计

布隆过滤器 :Bloom Filter Calculator

实际元素超出时,误判率会怎样变化

当实际元素超出预计元素时,错误率会有多大变化,它会急剧上升么,还是平缓地上升,这就需要另外一个公式,引入参数 t 表示实际元素和预计元素的倍数 t

f=(1-0.5^t)^k # 极限近似,k 是 hash 函数的最佳数量

当 t 增大时,错误率,f 也会跟着增大,分别选择错误率为 10%,1%,0.1% 的 k 值,画出它的曲线进行直观观察。

布隆过滤器的其它应用

在爬虫系统中,我们需要对 URL 进行去重,已经爬过的网页就可以不用爬了。但是 URL 太多了,几千万几个亿,如果用一个集合装下这些 URL 地址那是非常浪费空间的。这 时候就可以考虑使用布隆过滤器。它可以大幅降低去重存储消耗,只不过也会使得爬虫系统错过少量的页面。

布隆过滤器在 NoSQL 数据库领域使用非常广泛,我们平时用到的 HBase、Cassandra 还有 LevelDB、RocksDB 内部都有布隆过滤器结构,布隆过滤器可以显著降低数据库的 IO 请求数量。当用户来查询某个 row 时,可以先通过内存中的布隆过滤器过滤掉大量不存在的 row 请求,然后再去磁盘进行查询。邮箱系统的垃圾邮件过滤功能也普遍用到了布隆过滤器,因为用了这个过滤器,所以平 时也会遇到某些正常的邮件被放进了垃圾邮件目录中,这个就是误判所致,概率很低。

布隆过滤器的内部原理

【原】布隆过滤器 (Bloom Filter) 详解 - Allen Sun - 博客园


6、简单限流

       限流算法在分布式领域是一个经常被提起的话题,当系统的处理能力有限时,如何阻止计划外的请求继续对系统施压,这是一个需要重视的问题。

       除了控制流量,限流还有一个应用目的是用于控制用户行为,避免垃圾请求。比如在 UGC 社区,用户的发帖、回复、点赞等行为都要严格受控,一般要严格限定某行为在规定 时间内允许的次数,超过了次数那就是非法行为。对非法行为,业务必须规定适当的惩处策略。

6.1、如何使用 Redis 来实现简单限流策略?

首先我们来看一个常见 的简单的限流策略。系统要限定用户的某个行为在指定的时间里 只能允许发生 N 次,如何使用 Redis 的数据结构来实现这个限流的功能?

解决方案

这个限流需求中存在一个滑动时间窗口,想想 zset 数据结构的 score 值,是不是可以 通过 score 来圈出这个时间窗口来。而且我们只需要保留这个时间窗口,窗口之外的数据都 可以砍掉。那这个 zset 的 value 填什么比较合适呢?它只需要保证唯一性即可,用 uuid 会 比较浪费空间,那就改用毫秒时间戳吧。

如图所示,用一个 zset 结构记录用户的行为历史,每一个行为都会作为 zset 中的一个 key 保存下来。同一个用户同一种行为用一个 zset 记录。

为节省内存,我们只需要保留时间窗口内的行为记录,同时如果用户是冷用户,滑动时 间窗口内的行为是空记录,那么这个 zset 就可以从内存中移除,不再占用空间。

通过统计滑动窗口内的行为数量与阈值 max_count 进行比较就可以得出当前的行为是否 允许。用代码表示如下:

public class SimpleRateLimiter {
    private Jedis jedis;

    public SimpleRateLimiter(Jedis jedis) {
        this.jedis = jedis;
    }

    public boolean isActionAllowed(String userId, String actionKey, int period, int maxCount) {
        String key = String.format("hist:%s:%s", userId, actionKey);
        long nowTs = System.currentTimeMillis();
        Pipeline pipe = jedis.pipelined();
        pipe.multi();
        pipe.zadd(key, nowTs, "" + nowTs);
        pipe.zremrangeByScore(key, 0, nowTs - period * 1000);
        Response<Long> count = pipe.zcard(key);
        pipe.expire(key, period + 1);
        pipe.exec();
        pipe.close();
        return count.get() <= maxCount;
    }

    public static void main(String[] args) {
        Jedis jedis = new Jedis();
        SimpleRateLimiter limiter = new SimpleRateLimiter(jedis);
        for (int i = 0; i < 20; i++) {
            System.out.println(limiter.isActionAllowed("laoqian", "reply", 60, 5));
        }
    }
}

这段代码还是略显复杂,需要读者花一定的时间好好啃。它的整体思路就是:每一个行 为到来时,都维护一次时间窗口。将时间窗口外的记录全部清理掉,只保留窗口内的记录。 zset 集合中只有 score 值非常重要,value 值没有特别的意义,只需要保证它是唯一的就可 以了。因为这几个连续的 Redis 操作都是针对同一个 key 的,使用 pipeline 可以显著提升 Redis 存取效率。但这种方案也有缺点,因为它要记录时间窗口内所有的行为记录,如果这 个量很大,比如限定 60s 内操作不得超过 100w 次这样的参数,它是不适合做这样的限流 的,因为会消耗大量的存储空间。

限流策略的简单应用,它仍然有较大的提升空间,适用的场景也有限。为了解决简单限流的缺点,下一节我们将引入高级限流算法——漏斗限流。


7、漏斗限流

Redis-Cell

Redis 4.0 提供了一个限流 Redis 模块,它叫 redis-cell。该模块也使用了漏斗算法,并提供了原子的限流指令。有了这个模块,限流问题就非常简单了。

该模块只有 1 条指令 cl.throttle,它的参数和返回值都略显复杂,接下来让我们来看看这 个指令具体该如何使用。

上面这个指令的意思是允许的频率为每 60s 最多 30 次(漏水速 率),漏斗的初始容量为 15,也就是说一开始可以连续回复 15 个帖子,然后才开始受漏水 速率的影响。我们看到这个指令中漏水速率变成了 2 个参数,替代了之前的单个浮点数。用 两个参数相除的结果来表达漏水速率相对单个浮点数要更加直观一些。

在执行限流指令时,如果被拒绝了,就需要丢弃或重试。cl.throttle 指令考虑的非常周 到,连重试时间都帮你算好了,直接取返回结果数组的第四个值进行 sleep 即可,如果不想 阻塞线程,也可以异步定时任务来重试。

漏斗限流模块除了应用于 UGC,还能应用于哪些地方?


8、GeoHash

Redis 在 3.2 版本以后增加了地理位置 GEO 模块,意味着我们可以使用 Redis 来实现摩拜单车「附近的 Mobike」、美团和饿了么「附近的餐馆」这样的功能了。

Redis 的 Geo 指令基本使用
Redis 提供的 Geo 指令只有 6 个,读者们瞬间就可以掌握。使用时,读者务必再次想起,它只是一个普通的 zset 结构。

增加

geoadd 指令携带集合名称以及多个经纬度名称三元组,注意这里可以加入多个三元组

127.0.0.1:6379> geoadd company 116.48105 39.996794 juejin (integer) 1
127.0.0.1:6379> geoadd company 116.514203 39.905409 ireader (integer) 1

127.0.0.1:6379> geoadd company 116.489033 40.007669 meituan
(integer) 1
127.0.0.1:6379> geoadd company 116.562108 39.787602 jd 116.334255 40.027400 xiaomi (integer) 2

距离

geodist 指令可以用来计算两个元素之间的距离,携带集合名称、2 个名称和距离单位。

127.0.0.1:6379> geodist company juejin ireader km "10.5501"
127.0.0.1:6379> geodist company juejin meituan km "1.3878"

127.0.0.1:6379> geodist company juejin jd km "24.2739"
127.0.0.1:6379> geodist company juejin xiaomi km "12.9606"

127.0.0.1:6379> geodist company juejin juejin km "0.0000"

我们可以看到掘金离美团最近,因为它们都在望京。距离单位可以是 m、km、ml、ft, 分别代表米、千米、英里和尺。

获取元素位置

geopos 指令可以获取集合中任意元素的经纬度坐标,可以一次获取多个。

127.0.0.1:6379> geopos company juejin 1) 1) "116.48104995489120483"
2) "39.99679348858259686"

127.0.0.1:6379> geopos company ireader 1) 1) "116.5142020583152771"

2) "39.90540918662494363" 127.0.0.1:6379> geopos company juejin ireader 1) 1) "116.48104995489120483"

2) "39.99679348858259686" 2) 1) "116.5142020583152771"

2) "39.90540918662494363"

我们观察到获取的经纬度坐标和 geoadd 进去的坐标有轻微的误差,原因是 geohash 对 二维坐标进行的一维映射是有损的,通过映射再还原回来的值会出现较小的差别。对于「附 近的人」这种功能来说,这点误差根本不是事。

获取元素的 hash 值

geohash 可以获取元素的经纬度编码字符串,上面已经提到,它是 base32 编码。 你可 以使用这个编码值去 http://geohash.org/${hash}中进行直接定位,它是 geohash 的标准编码 值。
127.0.0.1:6379> geohash company ireader 1) "wx4g52e1ce0"
127.0.0.1:6379> geohash company juejin 1) "wx4gd94yjn0"
让我们打开地址 http://geohash.org/wx4g52e1ce0,观察地图指向的位置是否正确。

附近的公司

georadiusbymember 指令是最为关键的指令,它可以用来查询指定元素附近的其它元 素,它的参数非常复杂。

# 范围 20 公里以内最多 3 个元素按距离正排,它不会排除自身
127.0.0.1:6379> georadiusbymember company ireader 20 km count 3 asc 1) "ireader"
2) "juejin"
3) "meituan"
# 范围 20 公里以内最多 3 个元素按距离倒排
127.0.0.1:6379> georadiusbymember company ireader 20 km count 3 desc 1) "jd"
2) "meituan"
3) "juejin"
# 三个可选参数 withcoord withdist withhash 用来携带附加参数
# withdist 很有用,它可以用来显示距离
127.0.0.1:6379> georadiusbymember company ireader 20 km withcoord withdist withhash count 3 asc 1) 1) "ireader"
2) "0.0000"
3) (integer) 4069886008361398 4) 1) "116.5142020583152771"
2) "39.90540918662494363" 2) 1) "juejin"
2) "10.5501"
3) (integer) 4069887154388167 4) 1) "116.48104995489120483"
2) "39.99679348858259686" 3) 1) "meituan"
2) "11.5748"
3) (integer) 4069887179083478 4) 1) "116.48903220891952515"
2) "40.00766997707732031"
除了 georadiusbymember 指令根据元素查询附近的元素,Redis 还提供了根据坐标值来 查询附近的元素,这个指令更加有用,它可以根据用户的定位来计算「附近的车」,「附近 的餐馆」等。它的参数和 georadiusbymember 基本一致,除了将目标元素改成经纬度坐标 值。
127.0.0.1:6379> georadius company 116.514202 39.905409 20 km withdist count 3 asc 1) 1) "ireader"
2) "0.0000" 2) 1) "juejin"
2) "10.5501" 3) 1) "meituan"
2) "11.5748"

小结 & 注意事项

在一个地图应用中,车的数据、餐馆的数据、人的数据可能会有百万千万条,如果使用 Redis 的 Geo 数据结构,它们将全部放在一个 zset 集合中。在 Redis 的集群环境中,集合 可能会从一个节点迁移到另一个节点,如果单个 key 的数据过大,会对集群的迁移工作造成 较大的影响,在集群环境中单个 key 对应的数据量不宜超过 1M,否则会导致集群迁移出现 卡顿现象,影响线上服务的正常运行。

所以,这里建议 Geo 的数据使用单独的 Redis 实例部署,不使用集群环境。

如果数据量过亿甚至更大,就需要对 Geo 数据进行拆分,按国家拆分、按省拆分,按 市拆分,在人口特大城市甚至可以按区拆分。这样就可以显著降低单个 zset 集合的大小。


9、scan

平时线上 Redis 维护工作中,有时候需要从 Redis 实例成千上万的 key 中找出特定 前缀的 key 列表来手动处理数据,可能是修改它的值,也可能是删除 key。这里就有一个问 题,如何从海量的 key 中找出满足特定前缀的 key 列表来?

Redis 提供了一个简单暴力的指令 keys 用来列出所有满足特定正则字符串规则的 key。

127.0.0.1:6379> set codehole1 a OK
127.0.0.1:6379> set codehole2 b OK

127.0.0.1:6379> set codehole3 c OK
127.0.0.1:6379> set code1hole a OK

127.0.0.1:6379> set code2hole b OK
127.0.0.1:6379> set code3hole b OK

127.0.0.1:6379> keys * 1) "codehole1"
2) "code3hole"
3) "codehole3"

4) "code2hole"
5) "codehole2"
6) "code1hole"
127.0.0.1:6379> keys codehole* 1) "codehole1"

2) "codehole3"
3) "codehole2"
127.0.0.1:6379> keys code*hole 1) "code3hole"
2) "code2hole"
3) "code1hole"

这个指令使用非常简单,提供一个简单的正则字符串即可,但是有很明显的两个缺点。

keys的两个缺点:

  • 没有 offset、limit 参数,一次性吐出所有满足条件的 key,万一实例中有几百 w 个key 满足条件,当你看到满屏的字符串刷的没有尽头时,你就知道难受了。
  • keys 算法是遍历算法,复杂度是 O(n),如果实例中有千万级以上的 key,这个指令就会导致 Redis 服务卡顿,所有读写 Redis 的其它的指令都会被延后甚至会超时报错,因为Redis 是单线程程序,顺序执行所有指令,其它指令必须等到当前的 keys 指令执行完了才可以继续。

面对keys两个显著的缺点该怎么办呢?

Redis 为了解决这个问题,它在 2.8 版本中加入了大海捞针的指令——scan

scan 相比 keys 具备有以下特点:

  • 1、复杂度虽然也是 O(n),但是它是通过游标分步进行的,不会阻塞线程;
  • 2、提供 limit 参数,可以控制每次返回结果的最大条数,limit 只是一个 hint,返回的 结果可多可少;
  • 3、同 keys 一样,它也提供模式匹配功能;
  • 4、服务器不需要为游标保存状态,游标的唯一状态就是 scan 返回给客户端的游标整数;
  • 5、返回的结果可能会有重复,需要客户端去重复,这点非常重要;
  • 6、遍历的过程中如果有数据修改,改动后的数据能不能遍历到是不确定的;
  • 7、单次返回的结果是空的并不意味着遍历结束,而要看返回的游标值是否为零;

scan 基础使用

scan 参数提供了三个参数,第一个是 cursor 整数值,第二个是 key 的正则模式,第三个是遍历的 limit hint。第一次遍历时,cursor 值为 0,然后将返回结果中第一个整数值作为下一次遍历的 cursor。一直遍历到返回的 cursor 值为 0 时结束。

127.0.0.1:6379> scan 0 match key99* count 1000

字典的结构

在 Redis 中所有的 key 都存储在一个很大的字典中,这个字典的结构和 Java 中的 HashMap 一样,是一维数组 + 二维链表结构,第一维数组的大小总是 2^n(n>=0),扩容一 次数组大小空间加倍,也就是 n++。

scan 指令返回的游标就是第一维数组的位置索引,我们将这个位置索引称为槽 (slot)。 如果不考虑字典的扩容缩容,直接按数组下标挨个遍历就行了。limit 参数就表示需要遍历的 槽位数,之所以返回的结果可能多可能少,是因为不是所有的槽位上都会挂接链表,有些槽 位可能是空的,还有些槽位上挂接的链表上的元素可能会有多个。每一次遍历都会将 limit 数量的槽位上挂接的所有链表元素进行模式匹配过滤后,一次性返回给客户端。

scan 遍历顺序

scan 的遍历顺序非常特别。它不是从第一维数组的第 0 位一直遍历到末尾,而是采用 了高位进位加法来遍历。之所以使用这样特殊的方式进行遍历,是考虑到字典的扩容和缩容 时避免槽位的遍历重复和遗漏。

普通加法和高位进位加法的区别

高位进位法从左边加,进位往右边移动,同普通加法正好相反。但是最终它们都会遍历 所有的槽位并且没有重复。

更多的 scan 指令

scan 指令是一系列指令,除了可以遍历所有的 key 之外,还可以对指定的容器集合进 行遍历。比如 zscan 遍历 zset 集合元素,hscan 遍历 hash 字典的元素、sscan 遍历 set 集 合的元素。

它们的原理同 scan 都会类似的,因为 hash 底层就是字典,set 也是一个特殊的 hash(所有的 value 指向同一个元素),zset 内部也使用了字典来存储所有的元素内容,所以 这里不再赘述。

大 key 扫描

有时候会因为业务人员使用不当,在 Redis 实例中会形成很大的对象,比如一个很大的 hash,一个很大的 zset 这都是经常出现的。这样的对象对 Redis 的集群数据迁移带来了很 大的问题,因为在集群环境下,如果某个 key 太大,会数据导致迁移卡顿。另外在内存分配 上,如果一个 key 太大,那么当它需要扩容时,会一次性申请更大的一块内存,这也会导致 卡顿。如果这个大 key 被删除,内存会一次性回收,卡顿现象会再一次产生。

在平时的业务开发中,要尽量避免大 key 的产生。
如果你观察到 Redis 的内存大起大落,这极有可能是因为大 key 导致的,这时候你就需要定位出具体是那个 key,进一步定位出具体的业务来源,然后再改进相关业务代码设 计。

那如何定位大 key 呢?
为了避免对线上 Redis 带来卡顿,这就要用到 scan 指令,对于扫描出来的每一个

key,使用 type 指令获得 key 的类型,然后使用相应数据结构的 size 或者 len 方法来得到 它的大小,对于每一种类型,保留大小的前 N 名作为扫描结果展示出来。

上面这样的过程需要编写脚本,比较繁琐,不过 Redis 官方已经在 redis-cli 指令中提供 了这样的扫描功能,我们可以直接拿来即用。

redis-cli -h 127.0.0.1 -p 7001 –-bigkeys

如果你担心这个指令会大幅抬升 Redis 的 ops 导致线上报警,还可以增加一个休眠参 数。

redis-cli -h 127.0.0.1 -p 7001 –-bigkeys -i 0.1

上面这个指令每隔 100 条 scan 指令就会休眠 0.1s,ops 就不会剧烈抬升,但是扫描的 时间会变长。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值