Redis面试题话术

1.介绍一下redis

Redis是一个非关系数据库,我们项目中主要用它来存储热点数据的,减轻数据库的压力,单线程纯内存操作,采用了非阻塞IO多路复用机制,就是单线程监听,我们项目中使用springdata-redis来操作redis

我们项目中使用redis的地方很多,比方说首页的热点数据,数据字典里的数据等都用热地说存储来提高访问速度

redis呢有5种数据类型,string、list、hash、set、zset,我们常用的有string、list和hash,一些简单的key-value类型的都存储在string类型中,比如一些系统开关之类的,是否开放注册等,还有一些存储在hash中,比如我们的首页的推荐数据和热门数据,都是用hash来存储的,一个固定的字符串作为key,每条数据的id作为field,对应的数据作为value存储

redis还有两种持久化方式,一个是RDB,这也是redis默认的持久化方式,这种方式是以快照的方式存储数据,在固定的时间段内如果有多少变化,那么就会生成快照存储到磁盘上,redis 在进行数据持久化的过程中,会先将数据写入到一个临时文件中,待持久化过程都结束了,才会用这个临时文件替换上次持久化好的文件。正是这种特性,让我们可以随时来进行备份,因为快照文件总是完整可用的。对于 RDB 方式,redis 会单独创建一个子进程来进行持久化,而主进程是不会进行任何 IO 操作的,这样就确保了 redis 极高的性能。 这种方式的优点呢就是快,但是如果没等到持久化开始redis宕机了,那么就会造成数据丢失

还有一种是AOF,是即时性的持久化方式,是将 redis 执行过的所有写指令记录下来,在下次 redis 重新启动时,只要把这些写指令从前到后再重复执行一遍,就可以实现数据恢复了。AOF的方式会导致性能下降

两种方式可以同时开启,当两种方式同时开启时,数据恢复Redis会优先选择AOF恢复。

我们项目中使用的持久化方式就是默认的RDB,因为我们存储的数据首先来说不是很重要的数据,如果丢失了,还可以从数据库加载到,主要用的就是性能这块

2、redis缓存雪崩和缓存穿透、缓存击穿、缓存预热、缓存降级

缓存雪崩
我们可以简单的理解为:由于原有缓存失效,新缓存还没有存入到redis的期间

比方说:我们设置缓存时采用了相同的过期时间,在同一时刻出现大面积的缓存过期,所有原本应该访问缓存的请求都去查询数据库了,而对数据库CPU和内存造成巨大压力,严重的会造成数据库宕机。从而形成一系列连锁反应,造成整个系统崩溃。

解决办法:

加最多的解决方案就是锁,或者队列的方式来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。还有一个简单方案就是缓存失效时间分散开,不设置固定的实效时间,采用随机失效的策略来解决,比如可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

缓存穿透
缓存穿透是指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库再查询一遍,然后返回空,这就相当于进行了两次无用的查询。像这样请求就绕过缓存直接查数据库,这也是经常提的缓存命中率问题

解决办法

最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
另外也有一个更为简单粗暴的方法,如果一个查询返回的数据为空,不管是数据不存在,还是系统故障,我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
将无效的key存放进redis中:
当出现redis查不到数据,数据库也查不到数据的情况,我们就把这个key保存到redis中,设置value=“null”,并设置其过期时间极短,后面再出现查询这个key的请求的时候,直接返回null,就不需要再查询数据库了。但是这种处理方式是有问题的,假如传进来的这个不存的key值每次都是随机的,那存在redis也没有意义。
缓存击穿
缓存击穿和缓存雪崩有些相似,缓存雪崩时大规模的key失效,而缓存击穿是某个热点key失效,大并发集中对其进行请求,就会造成大量请求读缓存没读到数据,从而导致高并发访问数据库,引起数据库压力剧增

解决办法

在缓存失效后,通过互斥锁或者队列来控制读数据写缓存的线程数量,比如某个key只允许一个线程查询数据和写缓存,其他线程等待。这种方式会阻塞其他的线程,此时系统的吞吐量会下降
热点数据永不过期,永不过期实际包含两层意思:物理不过期,针对热点key不设置过期时间;逻辑过期,把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建

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

操作方式:

1、直接写个缓存刷新页面,上线时手工操作下;
2、数据量不大,可以在项目启动的时候自动进行加载;

然后就是缓存更新

1、定时去清理过期的缓存;

2.、当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存

缓存降级
当访问量剧增、服务出现问题,比如响应时间慢或不响应,或者非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有问题的服务。redis可以帮助系统实现数据降级载体,系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。降级的最终目的是保证核心服务可用,即使是有损的。

3、redis分布式锁

这个分布式锁这里,我们原来传统的项目都在单台服务器上部署用java里的锁synchronized这个同步锁就行,但是他这个是针对对象的锁,但是我们分布式的项目需要把项目部署到多台服务器上,每台服务器的对象都不同,所以就得考虑用分布式锁,这块实现起来也比较简单,其实这个锁就是redis中的一个key-value的一对值,在使用的时候吧,首先使用setnx方法进行尝试加锁,并可以设置过期时间,如果返回1则代表加锁成功,然后立即对这个锁设置一个实效时间,防止服务宕机,锁一致存在,在处理完业务逻辑之后,删除锁就行了,其他线程就可以获取锁进行业务了

4、redis主从复制

通过持久化功能,Redis保证了即使在服务器重启的情况下也不会损失(或少量损失)数据,因为持久化会把内存中数据保存到硬盘上,重启会从硬盘上加载数据。但是由于数据是存储在一台服务器上的,如果这台服务器出现硬盘故障等问题,也会导致数据丢失。为了避免单点故障,通常的做法是将数据库复制多个副本以部署在不同的服务器上,这样即使有一台服务器出现故障,其他服务器依然可以继续提供服务。为此, Redis 提供了复制功能,可以实现当一台数据库中的数据更新后,自动将更新的数据同步到其他数据库上。

Redis的主从结构可以采用一主多从或者级联结构,Redis主从复制可以根据是否是全量分为全量同步增量同步,配置非常简单,只需要在从节点配置slave of主节点的ip即可,如果有密码,还需要配置上密码,从节点只能读数据,不能写数据

全量同步主要发生在初次同步的时候,大概的步骤是

从服务器连接主服务器,发送SYNC命令;
主服务器接收到SYNC命名后,开始执行BGSAVE命令生成RDB文件并使用缓冲区记录此后执行的所有写命令;
主服务器BGSAVE执行完后,向所有从服务器发送快照文件,并在发送期间继续记录被执行的写命令;
从服务器收到快照文件后丢弃所有旧数据,载入收到的快照;
主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令;
从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令;
还有就是增量同步,主要发生在redis的工作过程中,Slave初始化后开始正常工作时主服务器发生的写操作同步到从服务器的过程。 增量复制的过程主要是主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令。

5、redis集群

Redis本身就支持集群操作redis_cluster,集群至少需要3主3从,且每个实例使用不同的配置文件,主从不用配置,集群会自己选举主数据库和从数据库,为了保证选举过程最后能选出leader,就一定不能出现两台机器得票相同的僵局,所以一般的,要求集群的server数量一定要是奇数,也就是2n+1台,并且,如果集群出现问题,其中存活的机器必须大于n+1台,否则leader无法获得多数server的支持,系统就自动挂掉。所以一般是3个或者3个以上的奇数节点。

Redis 2.8中提供了哨兵工具来实现自动化的系统监控和故障恢复功能。哨兵的作用就是监控redis主、从数据库是否正常运行,主数据库出现故障自动将从数据库转换为主数据库

我们公司搭建的redis集群是用的ruby脚本配合搭建的,我们一共搭建了6台服务器,3主3备,他们之间通信的原理是有一个乒乓协议进行通信的,我再给你说下一他们往里存储数据的机制吧,其实这个redis搭建好集群以后每个节点都存放着一个hash槽,每次往里存储数据的时候,redis都会根据存储进来的key值算出一个hash值,通过这个hash值可以判断到底应该存储到哪一个哈希槽中,取的时候也是这么取的,这就是我了解的redis集群

6、除了redis,还了解哪些别的非关系型数据库

有memacache,MongoDB这些,以及redis这几个都是非关系型数据库

memacache是纯内存型的,只支持简单的字符串数据,并且value值最大只能是1MB,而且所有的数据都只能存储在内存中,如果服务宕机或者关机重启,数据就会丢失,没有持久化功能

MongoDB的话是存储的数据都在磁盘上,功能比较多,不过性能没有其他两种好

而redis呢,支持的数据类型比较多,而且速度也非常快,value最大可以支持到512MB,而且既可以把数据存储在内存里,也可以持久化到磁盘上,重启之后还可以把磁盘中的数据重新加载到内存里,从性能以及数据安全上来说,都比memacache和MongoDB好一些

7、redis数据同步

这一块主要是跟mysql数据同步吧,mysql数据可能会发生变动,那么redis就要跟数据库的数据保持一致我们实际去使用的时候,是在数据发生变动的地方,比如增删改的时候,新奇一个线程,然后将变动的数据更新到redis中,根据不同的场景需求,也可以在数据变动时,把redis里的数据删掉,下一次用户查询的时候,发现redis中没有数据,就会重新去数据库加载一遍,这样也可以实现同步的效果

8、介绍一下redis的pipeline

pipeline的话,就是可以批量执行请求的命令

我们都知道redis是单线程的,在执行命令的时候,其他客户端是阻塞状态的,如果在高并发的时候,其实是会影响一定效率的,所以redis提供了pipeline,可以让我们批量执行命令,大大的减少了IO阻塞以及访问效率

因为pipeline是批量执行命令,我们一般会结合redis的事物去使用,它也符合事务的ACID特性,MULTI开启事物,EXEC执行,DISCARD清除事物状态,回到非事物状态,在使用前,还可以结合WATCH监控来使用,如果我们在执行一组命令的过程中,不想让其中的某个值被其他客户端改动,就可以使用WATCH,使用了之后,如果被改动,事务会自动回滚,可以很好的保证我们批量执行命令的时候,数据的准确性,在使用完成之后,可以使用UNWATCH解除监控。

9、介绍下redis中key的过期策略

定时删除:在设置key的过期时间的同时,为该key创建一个定时器,让定时器在key的过期时间来临时,对key进行删除

惰性删除:key过期的时候不删除,每次从数据库获取key的时候去检查是否过期,若过期,则删除,返回null

定期删除:每隔一段时间执行一次删除过期key操作

redis 过期策略是:定期删除+惰性删除。
所谓定期删除,指的是 redis 默认是每隔 100ms 就随机抽取一些设置了过期时间的 key,检查其是否过期,如果过期就删除。

10、Redis和mysql保持数据的一致性

数据同步⼀致性的问题:⽐如Redis->Mysql Mysq->Es
1.直接操作
更改数据源的时候直接更改对应缓存数据
2.延迟同步(延时双删)
更改数据源(主库),基于Mq的延迟实现延迟同步操作
3.定时同步
任务调度框架实现定时数据同步
4.第三⽅软件,实现数据迁移(定时)
淘宝Cancel

  1. 先修改主库的数据,保证主库数据是正确的,再发送mq消息,用监听器监听,如果修改redis没有成功,就抛异常,触发MQ的重发机制。
    消息队列的思路是,先更新数据库,成功后往消息队列发消息,消费到消息后再删除缓存,借助消息队列的重试机制来实现,达到最终一致性的效果。
  2. 延时双删。先删除redis缓存数据,再更新mysql,延迟几百毫秒再删除redis缓存数据,这样就算在更新mysql时,有其他线程读了Mysql,把老数据读到了Redis中,那么也会被删除掉,从而保持数据一致性。
    延时双删的方案的思路是,为了避免更新数据库的时候,其他线程从缓存中读取不到数据,就在更新完数据库之后,再Sleep 一段时间,然后再次删除缓存。 对于Sleep 的时间要对业务读写缓存的时间做出评估,只要Sleep 时间大于读写缓存的时间即可
    3.mysql读写分离(主从架构模式)
    通过阿里巴巴canal和RabbitMq来订阅binlog,实现异步删除通过数据库的binlog(二进制日志binnary log 以事件形式记录了对MySQL数据库执行更改的所有操作。)来异步淘汰key,利用工具(canal)将binlog日志采集发送到MQ中,然后通过ACK机制确认处理删除缓存。
    2.异步更新缓存(基于Mysql binlog的同步机制)
    1、涉及到更新的数据操作,利用Mysql binlog 进行增量订阅消费
    2、将消息发送到消息队列
    3、通过消息队列消费将增量数据更新到Redis上
    4、操作情况
    读取Redis缓存:热数据都在Redis上
    写Mysql:增删改都是在Mysql进行操作
    更新Redis数据:Mysql的数据操作都记录到binlog,通过消息队列及时更新到Redis上

11、redis数据类型

在这里插入图片描述
1.String类型
值是字符串类型
2.List类型
值是List集合,特点:存储多个元素,可以重复,保证添加顺序
3.Set类型
值是Set集合,特点:存储多个元素,不重复,不保证添加顺序
4.SortSet(Zset)类型
值是Map<Object,Double>,⼀种特殊Set集合,特点:存储多个元素,每个元素有:元素的值(任意类型)和分数(Score,只能Double),其中元素的值不能重复,分数是可以重复,⽽且还能根据分数排序
5.Hash类型
值是Map<Field(Object),Value(Object),>,特点:存储多个元素,每个元素有字段(Field)和值(Value),Field唯⼀,Value可以重复
6.Bitmap类型
值是⼀个byte数组,特点:byte数组,值只能是0或1,默认0
⼩身材⼤容量,应⽤场景:2种状态,海量数据,⽐如1亿⽤户,今⽇登录的⼈数、今⽇的签到⼈数、今⽇的抽奖⼈数
1Byte=8byte
1GB=1024MB=1024 * 1024KB=1024 * 1024 * 1024 B=1024 * 1024 * 1024 * 8b
7.Geo类型
值是⼀个地理位置数组,特点:元素是地理位置信息(经度、纬度、地点名称),可以进⾏计算距离,半径搜索
8.Hyperloglog类型
值是基数,特点:类似函数,计算集合中的基数

12、redis底层数据结构

SDS(simple dynamic string)简单动态字符串,Redis是基于C语⾔,但是字符串类型,重构了SDS
在这里插入图片描述
SDS就是String的底层,内部有三个属性

  1. len : 保存的字符串⻓度。获取字符串的⻓度就是O(1)
  2. free:剩余可⽤存储字符串的⻓度
  3. buf:保存字符串
    SDS的优点
    当⽤户修改字符串时sds api会先检查空间是否满⾜需求,如果满⾜,直接执⾏修改操作,如果不满⾜,将空间修改⾄满⾜需求的⼤⼩,然后再执⾏修改操作
    空间预分配
    如果修改后的sds的字符串⼩于1MB时(也就是len的⻓度⼩于1MB),那么程序会分配与len属性相同⼤⼩的未使⽤空间(就是再给未使⽤空间free也分配与len相同的空间) 例:字符串⼤⼩为600k,那么
    会分配600k给这个字符串使⽤,再分配600k的free空间在那。
    惰性空间释放,当缩短sds的存储内容时,并不会⽴即使⽤内存重分配来回收字符串缩短后的空间,⽽是通过free将空闲的空间记录起来,等待将来使⽤。真正需要释放内存的时候,通过调⽤api来释放内存
    通过空间预分配操作,redis有效的减少了执⾏字符串增⻓所需要的内存分配次数
    如果修改后sds⼤于1MB时(也就是len的⻓度⼤于等于1MB),那么程序会分配1MB的未使⽤空间
    例:字符串⼤⼩为3MB,那么会分配3MB给这个字符串使⽤,再分配1MB的free空间在那

Hash表数据结构,其实hash表本身就是⼀个数组,将key通过hash算法计算得出hash值对数组⻓度取模,⽤得到的值作为数组下标,然后把value保存在数组下标的位置。由于存储结构是数组,所以hash表
的读复杂度是O(1)。
应⽤场景
  1.Redis键值对(Hash类型)的底层实现。
  2.当⼀个哈希键包含的键值对⽐较多,⼜或者键值对总的元素都是⽐较⻓的字符串时,Redis会使⽤hash表作为哈希键的底层实现。
  3.集合键的底层实现之⼀
哈希冲突问题
  当hash表的负载因⼦过⼤时,会频繁出现hash冲突的问题,不同的key经过hash计算和取模得到相同的数组下标。redis使⽤链表法,将数组下标相同的key使⽤链表串联起来,以解决哈希冲突的问题。
但是当哈希冲突过多,链表⻓度过⻓时(需遍历链表,查找key完全匹配的数据),那么它的查询效率也将会降低到O(n),这个时候就要考虑增⼤数组的⻓度,执⾏rehash操作。
渐进式rehash
rehash操作需要创建⼀个新的⻓度*2的数组,将所有的key进⾏hash计算并对新的数组⻓度取模,然后把数据放在新的数组下标中,这样可以⼤幅度减少哈希冲突的⽐例,防⽌查询效率的退化。但是在⽣产系统中的数据量是很⼤的,如果⼀下⼦对所有的key进⾏rehash计算并迁移,那么会导致cpu负载过重,响应正常的对外提供服务。redis采⽤了渐进式的rehash⽅式,分批次的将所有key完成rehash,然后将指针引⽤到新的数组地址。

ZipList:压缩表,列表键(List)和哈希键(Hash类型和SortSet)底层实现,Redis会在列表键包含少量列表项且短字符串,使⽤压缩表作为底层结构。为了节约内存⽽开发
在这里插入图片描述
SkipList跳表,有序的链表,构建索引,提⾼查询效率,空间换时间,查找⽅式从上链表的顶层往下查找
时间复杂度是O(logn)
在这里插入图片描述

13、redis实现分布式锁

锁在程序中的作用就是同步工具,保证共享资源在同一时刻只能被一个线程访问,Java中的锁我们都很熟悉了,synchronized、lock都是我们经常用的,但是Java的锁只能保证单机的时候有效,分布式集群环境就无能为力了,这个时候我们就需要用到分布式锁。
分布式锁就是分布式项目开发中用到的锁,可以用来控制分布式系统之间同步访问共享资源。
思路是:在整个系统提供一个全局、唯一的获取锁的“东西”,然后每个系统在需要加锁时,都去问这个“东西”拿到一把锁,这个不同的系统拿到的就可以认为是同一把锁。至于这个东西,可以是redis,zookeeper,也可以是数据库。
一般来说,分布式锁需要满足的特性:
互斥性:在任何时刻,对于同一条数据,只有一台应用可以获取到分布式锁;
高可用性:在分布式场景下,一小部分服务器宕机不影响使用,这种情况就需要提供分布式锁以集群的方式部署;
防止锁超时:如果客户端没有主动释放锁,服务器会在一段时间之后自动释放锁,防止客户端宕机或者网络不可达时产生死锁;
独占性:加锁解锁必须由同一台服务器进行,也就是锁的持有者才可以释放锁,不能出现你加的锁,别人给你解锁了。

通过Redis的SET命令结合NX和EX选项来保证锁的唯一性和自动过期。具体步骤如下:

1.	加锁:使用SET key value NX EX time命令。当键不存在时,设置键并指定过期时间。
2.	释放锁:为了确保只有持有锁的客户端能够释放锁,我们使用Lua脚本来实现。Lua脚本可以保证获取锁值和删除锁的操作是原子性的。

我们使用UUID生成唯一的锁值,防止不同客户端之间锁值冲突。此外,我们设定了适当的过期时间来防止死锁问题。

使用 Redisson 来实现分布式锁,这是一个功能强大的 Redis 客户端,简化了分布式锁的实现。具体步骤如下:

1.	配置 Redisson 客户端:我们在 Spring 配置类中配置了 Redisson 客户端,使其连接到 Redis 服务器。可以通过配置文件或者直接在代码中进行配置。
2.	获取锁:使用 RedissonClient 提供的 RLock 对象,并调用 tryLock 方法尝试获取锁。我们可以设置最长等待时间和锁的自动过期时间,避免死锁问题。
3.	业务逻辑处理:在成功获取锁后,我们执行需要加锁的业务逻辑。
4.	释放锁:在 finally 块中调用 unlock 方法释放锁,确保即使在出现异常时也能正确释放锁。

通过这种方式,我们能够确保在分布式环境下的并发访问问题,保证数据的一致性和安全性。”
redlock一种算法,redis distributed lock,可用实现多接点redis的分布式锁,redisson完成了对redlock算法封装。
特性:互斥访问,永远只有一个client能拿到锁,避免死锁:最终client都可能拿到锁,不会出现死锁的情况,即使锁定资源的服务崩溃或者分区,仍然能释放锁。容错性:只要大部分redis节点存活(一半以上),就可以正常服务。
原理:
在这里插入图片描述
在这里插入图片描述

14、大key问题

在 Redis 中,“大key” 是指在 Redis 数据库中占用大量存储空间的键。大key 会影响 Redis 的性能,因为大key 的读取、写入、删除操作可能需要较长的时间,从而导致延迟问题。此外,某些操作(如备份、恢复和迁移)也会受到大key 的影响,导致性能下降。

识别大key

识别大key 可以通过以下方法进行:

  1. Redis 提供的命令

    • 使用 SCAN 命令遍历所有键并使用 MEMORY USAGE 命令获取每个键的内存使用情况。示例如下:
      redis-cli --bigkeys
      
      --bigkeys 是 Redis 自带的一个工具,可以自动扫描 Redis 数据库并报告大key。
  2. Redis 内存统计工具

    • Redis 提供了 INFO memory 命令来获取 Redis 的内存使用情况,但它不会显示具体键的内存使用。你需要自己写脚本遍历所有键并记录内存使用情况。
  3. 自定义脚本

    • 可以编写自定义脚本,使用 Redis 的 SCAN 命令遍历数据库中的键,使用 MEMORY USAGE 命令获取每个键的内存使用情况,并将其记录下来。例如:
      import redis
      
      r = redis.Redis(host='localhost', port=6379, db=0)
      cursor = '0'
      while cursor != 0:
          cursor, keys = r.scan(cursor=cursor, count=100)
          for key in keys:
              size = r.memory_usage(key)
              print(f"Key: {key}, Size: {size} bytes")
      

解决大key

解决大key 的问题可以通过以下方法:

  1. 拆分大key

    • 如果一个大key 是由多个元素组成的,可以将其拆分为多个较小的键。例如,一个包含大量元素的列表可以拆分为多个列表。
  2. 使用合适的数据结构

    • 选择合适的数据结构来存储数据。例如,将一个包含大量字段的哈希表拆分为多个较小的哈希表,或者将大字符串拆分为多个较小的字符串。
  3. 使用压缩

    • 对数据进行压缩以减少存储空间。Redis 自身并不直接支持数据压缩,但你可以在客户端进行数据压缩和解压缩操作。
  4. 定期清理

    • 定期清理过期或不再使用的数据。使用 Redis 的过期策略(如 EXPIRE 命令)可以自动删除过期数据。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值