Redis 面试题

Redis面试题

Redis是什么

Redis作为一个内存数据库。 性能优秀,数据在内存中,读写速度非常快 , 单进程单线程,是线程安全的,采用IO多路复用机制;

丰富的数据类型,支持字符串(strings)、散列(map)、列表(list)、集合(set)、有序集合(zset),还有三种特殊的数据类型 Geosptatial 地理位置 Hyperloglog 不重复元素(一个人不管访问多少次网站都算一次),Bitmap 打卡,只有0和1,还有发布订阅 subscribe和publish 还有布隆过滤器(这个是加分项);

支持数据持久化。可以将内存中数据保存在磁盘中,重启时加载;

支持 主从复制,哨兵,高可用;

可以用作分布式锁; 可以作为消息中间件使用,支持发布订阅

应用场景⭐

  1. 缓存
  2. 共享Session
  3. 消息队列系统
  4. 分布式锁

Redis 和 memcached 的区别

  1. redis支持更丰富的数据类型(支持更复杂的应用场景):Redis不仅仅支持简单的k/v类型的数据,同时还提供list,set,zset,hash等数据结构的存储。memcache支持简单的数据类型,String。
  2. Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而Memecache把数据全部存在内存之中。
  3. 集群模式:memcached没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是 redis 目前是原生支持 cluster 模式的.
  4. Memcached是多线程,非阻塞IO复用的网络模型;Redis使用单线程的多路 IO 复用模型。

为什么Redis这么快?⭐

完全基于内存,绝大部分请求是纯粹的内存操作,执行效率高
采用单线程,单线程也能处理高并发请求,想多核也可启动多实例

单线程反而避免了多线程的频繁上下文切换问题,预防了多线程可能产生的竞争问题。

核心是基于非阻塞的 IO 多路复用机制。

Redis 支持的数据类型有哪些?应用?⭐

  1. String字符串:字符串类型是 Redis 最基础的数据结构,首先键都是字符串类型, Value 不仅是 String,也可以是数字。常用在缓存、计数、共享Session、限速等。
  2. Hash哈希:在Redis中,哈希类型是指键值本身又是一个键值对结构,哈希可以用来存放用户信息,比如实现购物车。
  3. List列表(双向链表):列表(list)类型是用来存储多个有序的字符串。可以做简单的消息队列的功能。 数据结构:List 就是链表,可以用来当消息队列用。Redis 提供了 List 的 Push 和 Pop 操作,还提供了操作某一段的 API,可以直接查询或者删除某一段的元素。 实现方式:Redis List 的是实现是一个双向链表,既可以支持反向查找和遍历,更方便操作,不过带来了额外的内存开销。
  4. Set集合:集合(set)类型也是用来保存多个的字符串元素,集合是通过 hashtable 实现的。 但和列表类型不一样的是,集合中不允许有重复元素,并且集合中的元素是无序的,不能通过索引下标获取元素。利用 Set 的交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的喜好等功能。
  5. Sorted Set有序集合(跳表实现):Sorted Set 多了一个权重参数 Score,集合中的元素能够按 Score 进行排列。实现方式:Redis Sorted Set 的内部使用 HashMap 和跳跃表(skipList)来保证数据的存储和有序,HashMap 里放的是成员到 Score 的映射。

String 在你们项目怎么用的?

常用命令: set,get,decr,incr,mget 等。

在显示某个人的基本数据的时候,比如名字,粉丝数,关注数,使用 String 保存:

eg:  user:id:3506728370  
{"id":3506728370,"name":"春晚","fans":12210862,"blogs":6164, "focus":83}

设置一个定时刷新的操作,这样用户不需要直接读取数据库。怎么设置?setx key value,一定时间循环判断key是否失效,到期后再去数据库读取。

List 在你们项目怎么用的?

常用命令: lpush,rpush,lpop,rpop,lrange等

  1. 朋友圈点赞,要求按照点赞顺序显示点赞好友信息
    如果取消点赞,移除对应好友信息,但是不能使用pop了,怎么办呢?

    解决方案

    lrem key count value 移除指定数据
    count:移除的数目
    value:具体要移除的内容

  2. 个人用户的关注列表需要按照用户的关注顺序展示。

Set 在你们项目怎么用的?

每位用户首次使用今日头条时会设置3项爱好的内容,但是后期为了增加用户的活跃度、兴趣点,必须让用户
对其他信息类别逐渐产生兴趣,增加客户留存度,如何实现?

分析
系统分析出各个分类的最新或最热点信息条目并组织成set集合
随机挑选其中部分信息
配合用户关注信息分类中的热点信息组织成展示的全信息集合

解决方案

  • 随机获取集合中指定数量的数据

    srandmember key [count]

  • 随机获取集合中的某个数据并将该数据移出集合

    spop key [count]

zset

在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维度的消息排行榜)等信息,适合使用 Redis 中的 Sorted Set 结构进行存储。

zset跳表的数据结构⭐

增加了向前指针的链表叫作跳表跳表是一个随机化的数据结构,实质就是一种可以进行二分查找的有序链表。跳表在原有的有序链表上面增加了多级索引,通过索引来实现快速查找。跳表不仅能提高搜索性能,同时也可以提高插入和删除操作的性能。

原理:

跳表在原有的有序链表上面增加了多级索引,通过索引来实现快速查找。首先在最高级索引上查找最后一个小于当前查找元素的位置,然后再跳到次高级索引继续查找,直到跳到最底层为止,这时候以及十分接近要查找的元素的位置了(如果查找元素存在的话)。由于根据索引可以一次跳过多个元素,所以跳查找的查找速度也就变快了。

为什么使用跳跃表

首先,因为 zset 要支持随机的插入和删除,所以它 不宜使用数组来实现,关于排序问题,我们也很容易就想到 红黑树/ 平衡树 这样的树形结构,为什么 Redis 不使用这样一些结构呢?

  1. 性能考虑: 在高并发的情况下,树形结构需要执行一些类似于 rebalance 这样的可能涉及整棵树的操作,相对来说跳跃表的变化只涉及局部 (下面详细说)
  2. 实现考虑: 在复杂度与红黑树相同的情况下,跳跃表实现起来更简单,看起来也更加直观;

redis 事务

开启事务(mult)。。。。。。执行事务(exec)
放弃事务 DISCARD

监控watch 相当于乐观锁,加在multi前面,防止执行事务时,另外一个线程修改数据

redis 设置过期时间

Redis中有个设置时间过期的功能,即对存储在 redis 数据库中的值可以设置一个过期时间。作为一个缓存数据库,这是非常实用的。如我们一般项目中的 token 或者一些登录信息,尤其是短信验证码都是有时间限制的,按照传统的数据库处理方式,一般都是自己判断过期,这样无疑会严重影响项目性能。

我们 set key 的时候,都可以给一个 expire time,就是过期时间,通过过期时间我们可以指定这个 key 可以存活的时间。EXPIRE name 10 设置 ttl name 查询过期时间

如果假设你设置了一批 key 只能存活1个小时,那么接下来1小时后,redis是怎么对这批key进行删除的?

redis springboot整合⭐

可用Jedis中间件进行操作,但是一般不用这个,一般用的是Lettuce(Netty),不存在线程不安全的情况,可以减少线程的数据

数据过期策略⭐

定期删除+惰性删除。

通过名字大概就能猜出这两个删除方式的意思了。

  • 定期删除:redis默认是每隔 100ms 就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除。注意这里是随机抽取的。为什么要随机呢?你想一想假如 redis 存了几十万个 key ,每隔100ms就遍历所有的设置过期时间的 key 的话,就会给 CPU 带来很大的负载!
  • 惰性删除 :定期删除可能会导致很多过期 key 到了时间并没有被删除掉。所以就有了惰性删除。假如你的过期 key,靠定期删除没有被删除掉,还停留在内存里,除非你的系统去查一下那个 key,才会被redis给删除掉。

但是仅仅通过设置过期时间还是有问题的。我们想一下:如果定期删除漏掉了很多过期 key,然后你也没及时去查,也就没走惰性删除,此时会怎么样?如果大量过期key堆积在内存里,导致redis内存块耗尽了。怎么解决这个问题呢? redis 内存淘汰机制。

数据淘汰机制⭐

内存达到上限的六种策略
1、volatile-lru:只对设置了过期时间的key进行LRU(默认值)
2、allkeys-lru : 删除lru算法的key
3、volatile-random:随机删除即将过期key
4、allkeys-random:随机删除
5、volatile-ttl : 删除即将过期的
6、noeviction : 永不过期,返回错误

当内存到达最大内存限制时进行的数据淘汰策略

  1. 新写入操作会报错。(Redis 默认策略)
  2. 在键空间中,移除最近最少使用的 Key。(LRU推荐使用)
  3. 在键空间中,随机移除某个 Key。
  4. 在设置了过期时间的键空间中,移除最近最少使用的 Key。这种情况一般是把 Redis 既当缓存,又做持久化存储的时候才用。
  5. 在设置了过期时间的键空间中,随机移除某个 Key。
  6. 在设置了过期时间的键空间中,有更早过期时间的 Key 优先移除。

LRU 算法实现:1.通过双向链表来实现,新数据插入到链表头部;2.每当缓存命中(即缓存
数据被访问),则将数据移到链表头部;3.当链表满的时候,将链表尾部的数据丢弃。
LinkedHashMap:HashMap 和双向链表合二为一即是 LinkedHashMap。HashMap 是无序
的,LinkedHashMap 通过维护一个额外的双向链表保证了迭代顺序。该迭代顺序可以是插
入顺序(默认),也可以是访问顺序。

Redis的LRU具体实现:

传统的LRU是使用栈的形式,每次都将最新使用的移入栈顶,但是用栈的形式会导致执行select *的时候大量非热点数据占领头部数据,所以需要改进。Redis每次按key获取一个值的时候,都会更新value中的lru字段为当前秒级别的时间戳。Redis初始的实现算法很简单,随机从dict中取出五个key,淘汰一个lru字段值最小的。在3.0的时候,又改进了一版算法,首先第一次随机选取的key都会放入一个pool中(pool的大小为16),pool中的key是按lru大小顺序排列的。接下来每次随机选取的keylru值必须小于pool中最小的lru才会继续放入,直到将pool放满。放满之后,每次如果有新的key需要放入,需要将pool中lru最大的一个key取出。淘汰的时候,直接从pool中选取一个lru最小的值然后将其淘汰。

Redis 持久化的两种方式⭐

  • RDB:快照形式是直接把内存中的数据保存到一个dump的文件中,定时保存,保存策略。 当Redis需要做持久化时,Redis会fork一个子进程,子进程将数据写到磁盘上一个临时RDB文件中。当子进程完成写临时文件后,将原来的RDB替换掉。
  • AOF:把所有的对Redis的服务器进行修改的命令都存到一个文件里,命令的集合。 使用AOF做持久化,每一个写命令都通过write函数追加到appendonly.aof中。aof的默认策略是每秒钟fsync一次,在这种配置下,就算发生故障停机,也最多丢失一秒钟的数据。 缺点是对于相同的数据集来说,AOF的文件体积通常要大于RDB文件的体积。根据所使用的fsync策略,AOF的速度可能会慢于RDB。对于主从同步来说,主从刚刚连接的时候,进行全量同步(RDB);全同步结束后,进行增量同步(AOF)。

如果同时使用 RDB 和 AOF 两种持久化机制,那么在 redis 重启的时候,会使用 AOF 来重新构建数据,因为 AOF 中的数据更加完整。

RDB 持久化优点
RDB是一个紧凑压缩的二进制文件,存储效率高
RDB恢复数据速度比AOF快

RDB持久化缺点
无法做到实时持久化,具有较大可能丢失数据
存储数量较大时,效率较低,I/O性能较低
基于fork创建子进程,内存产生额外消耗
宕机带来的数据丢失风险

AOF 优点

  • AOF 可以更好的保护 数据不丢失,一般 AOF 会每隔 1 秒,最多丢失 1 秒钟的数据。
  • 写入性能非常高,而且文件不容易破损
  • 适合做灾难性的误删除的紧急恢复

AOF 缺点

对于同一份数据来说,AOF 日志文件通常比 RDB 数据快照文件更大。

恢复速度较慢

RDB 与 AOF 如何选择

对数据非常敏感,建议使用默认的AOF持久化方案
AOF策略使用everysec,每秒fsync一次,该策略仍可保持很好性能,出现问题最多丢失一秒内的数据
数据可以做到阶段内无丢失,且恢复较快,阶段点数据恢复通常使用RDB方案

综合:
如果不能承受分钟内的数据丢失,对业务数据非常敏感,选用AOF
如果能承受分钟内的数据丢失,且追求大数据集的恢复速度选用RDB,RDB 非常适合灾难恢复。
双保险策略,同时开启RDB和AOF,重启后Redis优先使用AOF来恢复数据,降低丢失数据量

为什么要用缓存?

用缓存,主要有两个用途:高性能高并发

怎么保证缓存和数据库数据的一致性?⭐

分布式环境下非常容易出现缓存和数据库间数据一致性问题,针对这一点,如果项目对缓存的要求是强一致性的,那么就不要使用缓存。

我们只能采取合适的策略来降低缓存和数据库间数据不一致的概率,而无法保证两者间的强一致性。

  • 合理设置缓存的过期时间。
  • 新增、更改、删除数据库操作时同步更新 Redis,可以使用事务机制来保证数据的一致性。
  • 缓存失败时增加重试机制。

redis 怎么实现分布式锁?

Redis 分布式锁其实就是在系统里面占一个“坑”,其他程序也要占“坑”的时候,占用成功了就可以继续执行,失败了就只能放弃或稍后重试。

占坑一般使用 setnx(set if not exists)指令,只允许被一个程序占有,使用完调用 del 释放锁

也可以配合EXPIRE key seconds自动释放锁
设置key的生存时间,当key过期时(生存时间为0) ,会被自动删除
风险/ 缺陷 :原子性没有得到满足,所以不建议。

缓存雪崩

在一个较短的时间内,缓存中较多的key集中过期或者缓存挂了,导致了数据库服务器崩溃

缓存雪崩的事前事中事后的解决方案如下:

在批量往Redis存数据的时候,把每个Key的失效时间都加个随机值就好了,这样可以保证数据不会再同一时间大面积失效。如果 Redis 是集群部署,将热点数据均匀分布在不同的 Redis 库中也能避免全部失效。或者设置热点数据永不过期,有更新操作就更新缓存就好了

缓存穿透

原因:

  1. Redis中大面积出现未命中
  2. 出现非正常URL访问

解决方案:最简单粗暴的方法如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们就把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。

**布隆过滤器(Bloom Filter)**这个也能很好的预防缓存穿透的发生,就是利用高效的数据结构和算法快速判断出你这个Key是否在数据库中存在,不存在你return就好了,存在你就去查DB刷新KV再return

缓存击穿

缓存击穿是指一个Key非常热点,在不停地扛着大量的请求,大并发集中对这一个点进行访问,当这个Key在失效的瞬间,持续的大并发直接落到了数据库上,就在这个Key的点上击穿了缓存。

解决:设置热点数据永不过期,或者加上个锁就搞定了。

假如 Redis 里面有 1 亿个 key ,其中有 10w 个 个 key 是以某个固定的已知的前缀开头的,如
果将它们全部找出来?
使用 keys 指令可以扫出指定模式的 key 列表。
对方接着追问:如果这个 redis 正在给线上的业务提供服务,那使用 keys 指令会有什么问
题?
这个时候你要回答 redis 关键的一个特性:redis 的单线程的。keys 指令会导致线程阻塞一
段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用 scan 指
令,scan 指令可以无阻塞的提取出指定模式的 key 列表,但是会有一定的重复概率,在客
户端做一次去重就可以了,但是整体所花费的时间会比直接用 keys 指令长。

实际项目中使用缓存有遇到什么问题或者会遇到什么问题你知道吗?

缓存和数据库数据一致性问题。

  1. 并发的场景下,导致读取老的 DB 数据,更新到缓存中。

  2. 缓存和 DB 的操作,不在一个事务中,可能只有一个操作成功,而另一个操作失败,导致不一致。

    比如缓存了钱包的余额,在前端读到的是老的DB缓存数据 解决办法在上面

主从复制

作用:
读写分离:master写、slave读,提高服务器的读写负载能力
负载均衡:基于主从结构,配合读写分离,由slave分担master负载,并根据需求的变化,改变slave的数量,通过多个从节点分担数据读取负载,大大提高Redis服务器并发量与数据吞吐量
故障恢复:当master出现问题时,由slave提供服务,实现快速的故障恢复
数据冗余:实现数据热备份,是持久化之外的一种数据冗余方式
高可用基石:基于主从复制,构建哨兵模式与集群,实现Redis的高可用方案

过程:

  • 从节点执行 slaveof IP,port 发送指令
  • 主节点响应
  • 从节点保存主节点信息(IP,port),建立和主节点的 Socket 连接。
  • 从节点发送 Ping 信号,主节点返回 Pong,确定两边能互相通信。
  • 连接建立后,主节点将所有数据发送给从节点(数据同步)。
  • 主节点把当前的数据同步给从节点后,便完成了复制的建立过程。接下来,主节点就会持续的把写命令发送给从节点,保证主从数据一致性。

复制/数据同步过程分为两个阶段

  1. 全量复制:
    slave接收到master生成的RDB文件,先清空自身的旧数据,然后执行RDB恢复过程,然后告知master已经恢复完毕。
  2. 部分复制(增量复制)
    主节点发送数据给从节点过程中,主节点还会进行一些写操作,这时候的数据存储在复制缓冲区中。master把自己之前创建的复制缓冲区的数据发送到slave,slave接收到aof指令后执行重写操作,恢复数据。

主从复制会存在以下问题:

  • 一旦主节点宕机,从节点晋升为主节点,同时需要修改应用方的主节点地址,还需要命令所有从节点去复制新的主节点,整个过程需要人工干预。
  • 主节点的写能力受到单机的限制。
  • 主节点的存储能力受到单机的限制。

哨兵:

哨兵(sentinel) 是一个分布式系统,用于对主从结构中的每台服务器进行监控,当出现故障时通过投票机制选择新的master并将所有slave连接到新的master。

作用:

监控
不断的检查master和slave是否正常运行。
master存活检测、master与slave运行情况检测

通知(提醒)
当被监控的服务器出现问题时,向其他(哨兵间,客户端)发送通知。

自动故障转移
断开master与slave连接,选取一个slave作为master,将其他slave连接到新的master,并告知客户端新的服务器地址

你项目里面用到了Redis,你们为啥用Redi s?

因为传统的关系型数据库如Mysql已经不能适用所有的场景了,比如秒杀的库存扣减,APP首页的访问流量高峰等等,都很容易把数据库打崩,所以引入了缓存中间件,目前市面上比较常用的缓存中间件有Redis 和 Memcached 不过中和考虑了他们的优缺点,最后选择了Redis。

Redis 和 Memcached 的区别,前面有说

Redis有哪些数据结构呀?

字符串String、字典Hash、列表List、集合Set、有序集合SortedSet。

这里我相信99%的读者都能回答上来Redis的5个基本数据类型。如果回答不出来的小伙伴我们就要加油补课哟,大家知道五种类型最适合的场景更好。

但是,如果你是Redis中高级用户,而且你要在这次面试中突出你和其他候选人的不同,还需要加上下面几种数据结构HyperLogLog、Geo、Pub/Sub。

如果你还想加分,那你说还玩过Redis Module,像BloomFilter,RedisSearch,Redis-ML,这个时候面试官得眼睛就开始发亮了,心想这个小伙子有点东西啊

****注:本人在面试回答到Redis相关的问题的时候,经常提到BloomFilter(布隆过滤器)这玩意的使用场景是真的多,而且用起来是真的香,原理也好理解,前面有说

如果有大量的key需要设置同一时间过期,一般需要注意什么?⭐

如果大量的key过期时间设置的过于集中,到过期的那个时间点,redis可能会出现短暂的卡顿现象。严重的话会出现缓存雪崩,我们一般需要在时间上加一个随机值,使得过期时间分散一些。

电商首页经常会使用定时任务刷新缓存,可能大量的数据失效时间都十分集中,如果失效时间一样,又刚好在失效的时间点大量用户涌入,就有可能造成缓存雪崩

如果在setnx之后执行expire之前进程意外crash或者要重启维护了,那会怎么样?

set指令有非常复杂的参数,这个应该是可以同时把setnx和expire合成一条指令来用的!

假如Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,如何将它们全部找出来?

使用keys指令可以扫出指定模式的key列表。

对方接着追问:如果这个redis正在给线上的业务提供服务,那使用keys指令会有什么问题?

这个时候你要回答redis关键的一个特性:redis的单线程的。keys指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用scan指令,scan指令可以无阻塞的提取出指定模式的key列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用keys指令长。

使用过Redis做异步队列么,你是怎么用的?

一般使用list结构作为队列,rpush生产消息,lpop消费消息。当lpop没有消息的时候,要适当sleep一会再重试。

如果对方追问可不可以不用sleep呢?

list还有个指令叫blpop,在没有消息的时候,它会阻塞住直到消息到来。

如果对方接着追问能不能生产一次消费多次呢?

使用pub/sub主题订阅者模式,可以实现 1:N 的消息队列。

如果对方继续追问 pub/su b有什么缺点?

在消费者下线的情况下,生产的消息会丢失,得使用专业的消息队列如RocketMQ等。

Redis如何实现延时队列?(难点)

使用sortedset,拿时间戳作为score,消息内容作为key调用zadd来生产消息,消费者用zrangebyscore指令获取N秒之前的数据轮询进行处理。

Redis是怎么持久化的?服务主从数据怎么交互的?

答RDB和AOF 以及他们的区别 再就是两种场景怎么交互 一种是重启redis 再一种就是主从复制

对方追问那如果突然机器掉电会怎样?

取决于AOF日志sync属性的配置,如果不要求性能,在每条写指令时都sync一下磁盘,就不会丢失数据。但是在高性能的要求下每次都sync是不现实的,一般都使用定时sync,比如1s1次,这个时候最多就会丢失1s的数据。

对方追问RDB的原理是什么?

前面有介绍 主要是fork和cow 一个子类 一个父类

******注:回答这个问题的时候,如果你还能说出AOF和RDB的优缺点,我觉得我是面试官在这个问题上我会给你点赞,两者其实区别还是很大的,而且涉及到Redis集群的数据同步问题等等

Pipeline有什么好处,为什么要用pipeline?

可以将多次IO往返的时间缩减为一次,前提是pipeline执行的指令之间没有因果相关性。使用redis-benchmark进行压测的时候可以发现影响redis的QPS峰值的一个重要因素是pipeline批次指令的数目。

Redis的同步机制了解么?

Redis可以使用主从同步,从从同步。第一次同步时,主节点做一次bgsave,并同时将后续修改操作记录到内存buffer,待完成后将RDB文件全量同步到复制节点,复制节点接受完成后将RDB镜像加载到内存。加载完成后,再通知主节点将期间修改的操作记录同步到复制节点进行重放就完成了同步过程。后续的增量数据通过AOF日志同步即可,有点类似数据库的binlog。

是否使用过Redis集群,集群的高可用怎么保证,集群的原理是什么?

Redis Sentinal着眼于高可用,在master宕机时会自动将slave提升为master,继续提供服务。

Redis Cluster着眼于扩展性,在单个redis内存不足时,使用Cluster进行分片存储。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值