关于redis的面试题准备(上)

学习Java,准备面试,并记录下来,redis上篇
  1. redis的定义(是什么)

百度百科是下面这样定义的:

Redis(Remote Dictionary Server ),即远程字典服务,是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、高性能的Key-Value数据库,并提供多种语言的API。

在网上也翻了好多帖子,总结如下:

Redis是基于C语言开发的,单进程单线程的非关系型数据库,具有高性能,高可用,高并发的特点,Redis的数据是存在内存中的。它的读写效率非常高,每秒可以处理超过10万次读写操作。因此redis被广泛应用于缓存。另外,Redis也经常用来做分布式锁。除此之外,Redis支持事务、持久化、LUA 脚本、LRU 驱动事件、多种集群方案。

  1. Redis的数据类型有哪些,底层怎么实现?

  1. 五种基本数据类型

redis的这些数据类型在底层都是使用核心对象redisObject对象表示的。

redisObject对象有以下属性:

type、encoding、ptr、其它信息。

type数据类型,对应的是value五种数据类型。

encoding编码类型,表示的value保存的编码,type和encoding不是一一对应的,是组合!

ptr指针指向了实际保存value的数据结构。

  • 字符串(string):

  • 简介:

String是Redis最基础的数据结构类型,它是二进制安全的,可以存储图片或者序列化的对象, 值 最大存储为512M

  • 常用命令:

set key value 设置值、

get key 获取值、

incr key 递增

decr key 递减

setnx key value 不存在就插入(not exists)

setex key time value 过期时间(expire)

  • 应用场景:

共享session

分布式锁(setnx)

计数器(incr)文章的阅读量,微博点赞数,允许一定的延迟,先写入 Redis 再定时同步到数据库

限流(incr)以访问者的 IP 和其他信息作为 key,访问一次增加一次计数,超过次数则返回 false。

全局ID: int 类型,incrby,利用该方法的增加并返回结果的原子性操作,方便我们实现获取ID

  • 内部编码:

int:8 个字节的长整型(long,2^63-1)(整数集合)

embstr:小于等于44个字节的SDS(简单动态字符串)

embstr 存储形式是这样一种存储形式,它将 RedisObject 对象头和 SDS 对象连续存在一起,使用 malloc 方法一次分配。embstr 最小占用空间为 19(16+3),而 64-19-1(结尾的\0)=44,所以 embstr 只能容纳 44 字节。

raw:大于 44 个字节的SDS(简单动态字符串)

C语言的字符串是char[]实现的,而Redis使用SDS(simple dynamic string) 封装

  • SDS的源码
struct sdshdr{
     //记录buf数组中已使用字节的数量
     //等于 SDS 保存字符串的长度
     int len;
     //记录 buf 数组中未使用字节的数量
     int free;
     //字节数组,用于保存字符串
     char buf[];
}
  • Redis选择SDS结构的原因

①、常数复杂度获取字符串长度

  由于 len 属性的存在,我们获取 SDS 字符串的长度只需要读取 len 属性,时间复杂度为 O(1)。而对于 C 语言,获取字符串的长度通常是经过遍历计数来实现的,时间复杂度为 O(n)。通过 strlen key 命令可以获取 key 的字符串长度。

  ②、杜绝缓冲区溢出

  我们知道在 C 语言中使用 strcat 函数来进行两个字符串的拼接,一旦没有分配足够长度的内存空间,就会造成缓冲区溢出。而对于 SDS 数据类型,在进行字符修改的时候,会首先根据记录的 len 属性检查内存空间是否满足需求,如果不满足,会进行相应的空间扩展,然后在进行修改操作,所以不会出现缓冲区溢出。

  ③、减少修改字符串的内存重新分配次数

  C语言由于不记录字符串的长度,所以如果要修改字符串,必须要重新分配内存(先释放再申请),因为如果没有重新分配,字符串长度增大时会造成内存缓冲区溢出,字符串长度减小时会造成内存泄露。

  而对于SDS,由于len属性和free属性的存在,对于修改字符串SDS实现了空间预分配和惰性空间释放两种策略:

  1、空间预分配:对字符串进行空间扩展的时候,扩展的内存比实际需要的多,这样可以减少连续执行字符串增长操作所需的内存重分配次数。

  2、惰性空间释放:对字符串进行缩短操作时,程序不立即使用内存重新分配来回收缩短后多余的字节,而是使用 free 属性将这些字节的数量记录下来,等待后续使用。(当然SDS也提供了相应的API,当我们有需要时,也可以手动释放这些未使用的空间。)

  ④、二进制安全

  因为C字符串以空字符作为字符串结束的标识,而对于一些二进制文件(如图片等),内容可能包括空字符串,因此C字符串无法正确存取;而所有 SDS 的API 都是以处理二进制的方式来处理 buf 里面的元素,并且 SDS 不是以空字符串来判断是否结束,而是以 len 属性表示的长度来判断字符串是否结束。

  ⑤、兼容部分 C 字符串函数

  虽然 SDS 是二进制安全的,但是一样遵从每个字符串都是以空字符串结尾的惯例,这样可以重用 C 语言库<string.h> 中的一部分函数。

  • 列表(list):有序可重复

  • 简介:

列表(list)类型是用来存储多个有序的字符串,一个列表最多可以存储2^32-1个元素。

  • 常用命令:

lpush mylist a b c 左插入

rpush mylist x y z 右插入

lrange mylist 0 -1 数据集合

lpop mylist 从左边开始弹出元素

rpop mylist 从右边开始弹出元素

  • 应用场景:

app的关注列表,粉丝列表等都可以用 Redis 的 list 结构来实现可以利用lrange命令,做基于Redis 的分页功能,性能极佳,用户体验好。

消息队列 :列表类型可以使用 rpush 实现先进先出的功能,同时又可以使用 lpop 轻松的弹出(查询 并删除)第一个元素,所以列表类型可以用来实现消息队列

  • 内部编码:

redis3.2之前,列表类型使用ziplist、linkedlist两种底层数据结构存储,

ziplist底层实现为压缩列表,当元素数量小于2且所有元素长度都小于64字节时,使用这种结构来存储;

redis3.2以后,则统一使用quicklist。

  • 哈希(hash):存储键值对

  • 简介:

在Redis中,哈希类型是指v(值)本身又是一个键值对(k-v)结构

  • 常用命令:

hset myhash name cxx 用于为哈希表中的字段赋值

hget myhash name 获取哈希表中字段的值

hmset myhash name cxx age 25 note "i am notes" 批量插入

hmget myhash name age note 批量获取

hgetall myhash 获取所有的

hexists myhash name 是否存在

hsetnx myhash score 100 设置不存在的

hincrby myhash id 1 递增

hdel myhash name 删除

hkeys myhash 只取key

hvals myhash 只取value

hlen myhash 长度

  • 应用场景:

缓存用户信息:Key 是用户 ID,value 是一个 Map,这个 Map 的 key 是成员的属性名,value 是属性值

缓存购物车信息:

  • 内部编码:

ziplist(压缩列表):当哈希类型中元素个数小于 hash-max-ziplist-entries 配置(默认 512 个),同时所有值都小于 hash-max-ziplist-value 配置(默认 64 字节)时,Redis 会使用 ziplist 作为哈希的内部实现。

hashtable(哈希表):当上述条件不满足时,Redis 则会采用 hashtable 作为哈希的内部实现。

  • 集合(set):无序不可重复

  • 简介:

集合(set)类型也是用来保存多个的字符串元素,但是不允许重复元素

  • 常用命令:
  • sadd key value [value2 value3] 添加一个或者多个成员

  • smembers key 返回集合中的所有成员

  • srem key value [value2 value3] 移除集合中一个或多个成员

  • scard key 获取当前key下的元素个数

  • spop 从集合中随机弹出一个元素 就从集合中删掉了

  • sismember myset set1 判断元素是否在集合中

  • sdiff key1 key2 …… | sinter | sunion 操作:集合间运算:差集 | 交集 | 并集

  • srandmember key count 随机获取集合中的元素

  • 应用场景:
  • 点赞,收藏:

创建一个set用来维护点赞或收藏的用户,商品或者帖子的ID作为key,用户ID作为value

点赞的用户 添加进set sadd 帖子的ID 帖子的ID

取消点赞 移除set srem 帖子的ID 帖子的ID

是否点赞 sismember 帖子的ID 帖子的ID

点赞的所有人员:smembers 帖子的ID

点赞人数:scard 帖子的ID

  • 购物平台的商品筛选:

通过商品不同属性集合的交集来完成 sinter

  • 贴吧,抖音等用户之间相互关注:

每个用户都创建一个set, sismember判断用户是否关注,

  • 内部编码:
  • intset(整数集合):当集合中的元素都是整数,并且集合中的元素个数小于set-max-intset-entries 参数时,默认512,Redis 会选用 intset 作为底层内部实现。

  • hashtable(哈希表):当上述条件不满足时,Redis 会采用 hashtable 作为底层实现。

  • 有序集合(zset):有序不可重复

  • 简介:

有序集合是一个类似于set但是更复杂的数据类型,单词sorted意为着这种集合中的每个元素都有一个可用于排序的权重,并且我们可以按顺序从集合中得到元素在某些需要一个保持数据有序的场景中,使用这种原生的序的特性是很方便的。

  • 常用命令:
  • zadd key score value 添加元素key 并且设置分数

  • zrange key 0 -1 withscores 返回集合中所有的元素按照分数从小到大排序

  • zrevrange key 0 -1 withscores 返回集合中所有的元素按照分数从大到小排序

  • zrangebyscore key 10 25 withscores 取指定分数范围的值

  • zcard key 元素数量

  • zrem key value1 value2 删除一个或多个元素

  • zincrby zset 1 one 增长分数

  • zscore zset two 获取分数

  • zrangebyscore key 10 25 withscores 指定范围的值

  • zrangebyscore zset 10 25 withscores limit 1 2 分页

  • zrevrangebyscore zset 10 25 withscores 指定范围的值

  • zcount zset 获得指定分数范围内的元素个数

  • 应用场景:
  • 商品的评价标签,可以记录商品的标签,统计标签次数,增加标签次数,按标签的分值进行排序

  • 百度搜索热点

  • 电商网站的防止刷好评,刷销量,刷恶评

  • 譬如:1分钟评论不得超过2次、5分钟评论少于5次等

  • 内部编码:
  • ziplist(压缩列表):当有序集合的元素个数小于 128 个(默认设置),同时每个元素的值都小于 64 字节(默认设置),Redis 会采用 ziplist 作为有序集合的内部实现。

  • skiplist(跳跃表):当上述条件不满足时,Redis 会采用 skiplist 作为内部编码。

  1. 四种特殊数据类型(了解)

  • Geo(地理坐标):

Redis3.2推出的,地理位置定位,用于存储地理位置信息,并对存储的信息进行操作。

  • HyperLogLog(基数统计):

用来做基数统计算法的数据结构,如统计网站的UV。

  • Bitmaps (位图):

用一个比特位来映射某个元素的状态,在Redis中,它的底层是基于字符串类型实现的,可以把bitmaps成作一个以比特位为单位的数组

  • Streams 流

这是Redis5.0引入的全新数据结构,用一句话概括Streams就是Redis实现的内存版kafka。支持多播的可持久化的消息队列,用于实现发布订阅功能,借鉴了 kafka 的设计。Redis Stream的结构有一个消息链表,将所有加入的消息都串起来,每个消息都有一个唯一的ID和对应的内容。消息是持久化的,Redis重启后,内容还在。

  1. 缓存三大问题是什么以及解决方案

  1. 缓存的工作原理

当一个读请求来了,会先去查下缓存,缓存如果有值,就直接返回查询到的结果;缓存如果查询不到,就去查数据库,然后把数据库查询到的结果更新到缓存,再返回结果。

  1. 缓存穿透

  • 发生原因

读请求访问时,缓存和数据库都没有某个值,这样就会导致每次对这个值的查询请求都会穿透到数据库,这就是缓存穿透。

  • 解决方法
  1. 接口层增加校验

用户鉴权、参数校验(请求参数是否合法、请求字段是否不存在等等)

  1. 缓存空值/缺省值

如果查询数据库为空,我们可以给该次请求的结果设置个空值并保存到缓存中,同时,最后给缓存设置适当的过期时间,防止长期的数据不一致。但是如果有写请求进来的话,需要更新缓存,防止数据不一致。(业务上比较常用,简单有效)

  1. 布隆过滤器

使用布隆过滤器快速判断数据是否存在。即一个查询请求过来时,先通过布隆过滤器判断值是否存在,存在才继续往下查。

  1. 什么是布隆过滤器

布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。

  1. 解决什么样的问题
  • 垃圾邮件过滤

  • 文字处理中的错误单词检测

  • 网络爬虫重复URL检测

  • 会员抽奖

  • 判断一个元素在亿级数据中是否存在

  • 缓存穿透

  1. 布隆过滤器的工作原理

当一个元素被加入集合时,通过 K 个 Hash 函数将这个元素映射成一个位阵列(Bit array)中的 K 个点,把它们置为 1。检索时,我们只要看看这些点是不是都是 1 就(大约)知道集合中有没有它了:

  • 添加元素的原理

  1. 将要添加的元素给k个hash函数

  1. 得到对应于位数组上的k个位置

  1. 将这k个位置设置成 1

  • 查询元素原理1 将要查询的元素给k个hash函数

  1. 得到对应数组的k个元素

  1. 如果k个位置中有一个为0,则肯定不在集合中

  1. 如果k个位置全部为1,则有可能在集合中

  1. 缓存击穿

  1. 发生原因
  1. key对应的数据存在,但在缓存中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。

  1. 解决方法
  1. 使用互斥锁方案
  1. 缓存失效时,不是立即去加载db数据,而是先使用某些带成功返回的原子操作命令,

  1. 1.1、如(Redis的setnx)去操作,成功的时候,再去加载db数据库数据和设置缓存。否则就去重试获取缓存。

  1. 1.2、ReentrantLock.tryLock(),缓存没有,尝试加锁,抢不到就睡一会,抢到的那一个查数据库;

  1. “永不过期”
  1. 是指没有设置过期时间,但是热点数据快要过期时,异步线程去更新和设置过期时间。

  1. 2.1、这个可以借助Redisson,它是一个 Java 语言实现的 Redis SDK 客户端,在使用分布式锁时,它就采用了自动续期的方案来避免锁过期,这个守护线程我们一般也把它叫做看门狗线程

  1. 缓存雪崩

  • 发生原因

当缓存服务器重启或者大量缓存集中在某一个时间段失效,而查询数据量巨大,请求都直接访问数据库,引起数据库压力过大甚至down机。

  • 解决方法
  • 解决大量缓存集中在某一个时间段失效
  1. 均匀过期:

给热点数据设置不同的过期时间,给每个key的失效时间加一个随机值;

  1. 设置热点数据永不过期:

不设置失效时间,有更新的话,需要更新缓存;

  1. 服务降级:

指服务针对不同的数据采用不同的处理方式:

  • 业务访问的是非核心数据,直接返回预定义信息、空值或者报错;

  • 业务访问核心数据,则允许访问缓存,如果缓存缺失,可以读取数据库。

  • 解决Redis实例宕机问题
  • 方案一: 实现服务熔断或者请求限流机制

我们通过监测Redis以及数据库实例所在服务器负载指标,如果发现Redis服务宕机,导致数据库的负载压力增大,我们可以启动服务熔断机制,暂停对缓存服务的访问。

但是这种方法对业务应用的影响比较大,我们也可以通过限流的方式降低这种影响。

举个例子:比如业务系统正常运行时,请求入口每秒最大允许进入的请求数是1万个,其中9000请求个可以被缓存处理,余下1000个会发送给数据库处理。

  • 方案二: 事前预防

通过主从节点的方式构建 Redis 缓存高可靠集群。 如果 Redis 缓存的主节点故障宕机了,从节点还可以切换成为主节点,继续提供缓存服务,避免了由于缓存实例宕机而导致的缓存雪崩问题。

  1. 缓存击穿和缓存雪崩的区别

缓雪崩看着有点像缓存击穿,其实它两区别是,缓存雪奔是指数据库压力过大甚至down机,缓存击穿只是大量并发请求到了DB数据库层面。可以认为击穿是缓存雪奔的一个子集吧。有些文章认为它俩区别,是区别在于击穿针对某一热点key缓存,雪奔则是很多key。

  1. redis是单线程,为什么还那么快

一般来说,单线程的处理能力应该比多线程差,但 Redis 还能达到每秒数万级的处理能力有如下几个原因:

  1. 单线程模式

  • 正因为Redis是单线程,所以也就避免了CPU不必要的上下文切换和竞争锁的消耗。

  • 如果某个命令执行过长(如hgetall命令),会造成阻塞。Redis是面向快速执行场景的数据库,所以要慎用如smembers和lrange、hgetall等命令。

  • 严格来讲从 Redis 4 之后并不是单线程,除了主线程外,它也有后台线程在处理一些较为缓慢的操作,例如清理脏数据、无用连接的释放、大 key 的删除等等。

  • Redis 6.0 引入了多线程提速,但它的执行命令操作内存的仍然是个单线程。

  1. 基于内存储存

Redis是基于内存存储实现的非关系型数据库,我们都知道内存读写是比在磁盘快很多的,相对于数据存在磁盘的数据库,redis省去了磁盘I/O的消耗。

  1. 高效的数据结构、合理的数据编码格式

redis给我提供了多种数据类型,每个数据类型都支持多个编码,而每个编码格式的底层数据结构都不相同。

  • 通过不同类型的对象,Redis 可以在执行命令之前,根据对象的类型来判断一个对象是否可以执行该的命令。

  • 通过存入数据的类型,字节长度,元素个数来选择不同的编码格式进行储存,从而达到优化内存空间,提高查询速度。

  • 五种数据类型对应的编码格式

string:; list:linkedlist底层实现为双端链表,当数据不符合ziplist条件时,使用这种结构存储;3.2版本之后list一般采用quicklist的快速列表结构来代替前两种。 hash:编码分为ziplist、hashtable两种,其中ziplist底层实现为压缩列表,当键值对数量小于2,并且所有的键值长度都小于64字节时使用这种结构进行存储;hashtable底层实现为字典,当不符合压缩列表存储条件时,使用字典进行存储。 set:编码分为inset和hashtable,intset底层实现为整数集合,当所有元素都是整数值且数量不超过2个时使用该结构存储,否则使用字典结构存储。 zset:编码分为ziplist和skiplist,当元素数量小于128,并且每个元素长度都小于64字节时,使用ziplist压缩列表结构存储,否则使用skiplist的字典+跳表的结构存储。

  1. String类型的编码方式,即encoding有三种:int、embstr、raw;
  • int:value的值是整数,encoding为int,没有对应底层数据结构

  • embstr:底层实现为占一块内存的SDS结构,当数据为长度不超过44字节的字符串时,选择以此结构连续存储元数据和值;

  • raw:底层实现为占两块内存的SDS,用于存储长度超过44字节的字符串数据,此时会在两块内存中分别存储元数据和值。

  1. List:编码分为ziplist、linkedlist和quicklist(3.2以前版本没有quicklist)。
  • ziplist:底层数据结构为压缩列表,当元素数量小于2且所有元素长度都小于64字节时,使用这种结构来存储,否则使用linkedlist编码

  • redis3.2以后,则统一使用quicklist

  • linkedlist、quicklist:底层数据结构为双向链表

  1. Hash: 编码方式有ziplist、HT两种,分别对应ziplist和hashtable两种底层数据结构

哈希类型元素个数小于512个,所有值小于64字节的话,使用ziplist编码,否则使用ht编码。

  1. Set:编码方式有intset、HT两种,分别对应intset和hashtable两种底层数据结构

如果集合中的所有元素都可以转换成整数值且元素个数小于512个,使用intset编码,否则使用ht编码。

  1. Zset:编码方式有ziplist、skiplist两种,分别对应ziplist和skiplist两种底层数据结构

当有序集合的元素个数小于128个,每个元素的值小于64字节时,使用ziplist编码,否则使用skiplist(跳跃表)编码

  1. 合理的I/O模型

Redis采用了IO多路复用技术,多路I/O复用技术可以让单个线程高效的处理多个连接请求,而Redis使用用epoll作为I/O多路复用技术的实现。并且,Redis自身的事件处理模型将epoll中的连接、读写、关闭都转换为事件,不在网络I/O上浪费过多的时间。

  1. redis的高性能,高可用

  1. 高性能,指的是查询快

redis是c语言实现,与其他语言相比,在实现语言层面性能高;redis是内存数据库,而传统的关系型数据库是磁盘文件读写,所以redis读写快;单线程,无上下文切换损耗,也不需要线程间同步,在单核cpu上,性能高,如果服务器是多核cpu,可以开启多个进程的单线程redis实例

  1. 高可用(High Availability)

高可用指的是在节点故障时,服务仍然能正常运行或者进行降级后提供部分服务,redis可以从单节点,多节点来体现高可用。

  • 单节点

单节点的高可用主要体现为redis的数据持久化,Redis提供了RDB和AOF两种文件格式进行持久化:全量数据、增量请求。

  • RDB(Redis Database):全量数据
  • RDB是什么

RDB持久化,是指在指定的时间间隔内,执行指定次数的写操作,将内存中的数据集以快照的方式写入磁盘中,它是Redis默认的持久化方式。执行完操作后,在指定目录下通过rdb.c/rdbSave函数会生成一个dump.rdb文件,Redis 重启的时候,通过rdb.c/rdbLoad函数加载dump.rdb文件来恢复数据。

  • RDB触发机制
  • 手动触发
  • save(同步):该命令会开始持久化操作,但是save会使Redis服务阻塞,直到持久化完成。当数据量较大,会造成长时间阻塞,不建议使用

  • bgsave(后台异步):该命令会执行fork(调用OS函数复制主进程)方法创建一条子进程,由子进程进行持久化操作,Redis服务一般只会在创建子进程时阻塞,后续持久化操作由子进程完成,Redis服务不会阻塞。

  • 自动触发:在以下4种情况时会自动触发
  • redis.conf中配置save m n,即在m秒内有n次修改时,自动触发bgsave生成rdb文件;

  • 主从复制时,从节点要从主节点进行全量复制时也会触发bgsave操作,生成当时的快照发送到从节点;

  • 执行debug reload命令重新加载redis时也会触发bgsave操作;

  • 默认情况下执行shutdown命令时,如果没有开启aof持久化,那么也会触发bgsave操作;

  • RDB 执行流程(bgsave)
  1. Redis 主线程首先判断:当前是否在执行save,或 bgsave / bgrewriteaof (AOF 文件重写命令)的子进程,如果在执行则bgsave命令直接返回。

  1. Redis 主线程执行 fork(调用OS函数复制主进程)操作创建子进程,这个过程中 Redis 主线程是阻塞的,Redis 不能执行来自客户端的任何命令。

  1. Redis 主线程 fork 后,bgsave 命令返回“Background saving started”信息并不再阻塞 Redis 主线程,并可以响应其他命令。

  1. 子进程创建 RDB 文件,根据 Redis 主线程内存快照生成临时快照文件,完成后对原有文件进行原子替换。 (RDB 始终完整)

  1. 子进程发送信号给 Redis 主线程表示完成,Redis 主线程更新统计信息。

  1. Redis 主线程 fork 子进程后,继续工作。

  • RDB的优点
  • 适合备份:RDB 生成的文件是一个的紧凑压缩的二进制文件,体积小占内存也少,适合用于做备份。比如我们可以设定一个时间点对RDB文件进行归档,这样就能在需要的时候很轻易的把数据恢复到不同的版本。

  • 适合灾难恢复:基于上一点描述,RDB非常适用于灾难恢复,一个体积小的文件,可以方便快捷的传送到另一个远端数据中心或者亚马逊(Amazon S3 (可能加密)),

  • 性能更高:生成 RDB 文件时支持异步处理,主进程不需要进行任何磁盘IO操作,保证了 Redis的高性能

  • 恢复更快:因为rdb是数据的快照,基本上就是数据的复制,不用重新读取再写入内存,所以使用该文件恢复数据的速度非常快

  • RDB的缺点
  • 故障丢失数据:因为rdb是全量的,我们一般是使用shell脚本实现30分钟或者1小时或者每天对redis进行rdb备份,(注,也可以是用自带的策略),但是最少也要5分钟进行一次的备份,所以当服务死掉后,最少也要丢失5分钟的数据。

  • 备份时间长耐久性差:相对aof的异步策略来说,因为rdb的复制是全量的,即使是fork的子进程来进行备份,当数据量很大的时候对磁盘的消耗也是不可忽视的,尤其在访问量很高的时候,fork的时间也会延长,导致cpu吃紧,耐久性相对较差。

  • AOF(Append Only File):增量请求
  • AOF是什么

AOF 持久化,是指采用日志的形式来记录每个写操作,把被执行的写命令 以协议文本的方式通过write函数以追加的方式写到日志文件(appendonly.aof)的末尾,在 redis 重启的时候,可以通过回放 AOF 日志中的写入指令来重新构建整个数据集。(Redis默认情况是不开启AOF的)重启时再重新执行AOF文件中的命令来恢复数据。它主要解决数据持久化的实时性问题。

  • Redis底层协议

RESP(Redis Serialization Protocol)序列化协议

RESP主要有实现简单、解析速度快、可读性好等优点

  • AOF为什么是写后日志
  • 写后的含义
  • 写前日志指的是,在实际写数据前,先把修改的数据记录到日志文件,以便故障时进行恢复。

  • 写后的意思是,Redis先执行命令,把数据写入内存,然后再记录日志到磁盘。

  • 使用写后日志的原因
  • 防止记录错误的操作命令

redis为了避免额外的性能的开销,向日志文件存入操作命令时,不会检查命令语法的正确性,而是先让系统执行,只要执行成功才会追加到日志文件中,否则系统报错。

  • 不会阻塞写操作

基于先执行,在记录这个特性,保障了正确写操作的顺利执行

  • 写后日志存在的风险
  • 执行完命令还没记录日志时,宕机了会导致数据丢失

  • 前面说道,由于写操作命令执行成功后才记录到 AOF 日志,所以不会阻塞当前写操作命令的执行,但是可能会给下一个命令带来阻塞风险(因为将命令写入到日志的这个操作也是在主进程完成的,执行命令也是在主进程)。

  • 风险解决办法

其实这两个风险都有一个共性,都跟AOF 日志写回硬盘的时机有关。而redis也给我们提供了三个写回策略Always、Everysec、No,大家可以根据自己的业务开发需要来选择对应的策略,可以在 redis.conf 配置文件中的 appendfsync 配置项选择,这下面会对三种策略进行解释和对比

  • AOF持久化的实现步骤
  • 第一步是命令的实时写入(分2个过程)
  • 命令追加:当 AOF 持久化功能被打开时,服务器执行完一个写命令之后,会以命令的文本协议格式将被执行的命令追加到server.aof_buf缓冲区的末尾;

  • 数据写入文件:然后通过 write() 函数调用,将 server.aof_buf缓冲区的数据写入到 AOF 文件,此时数据并没有写入到硬盘,而是拷贝到了内核缓冲区page cache由内核决定什么时候将内核缓冲区的数据写入硬盘;具体的写回策略由appendfsync选项的值来确定;

  • 第二步是对AOF文件的重写。
  • 文件写入硬盘,同步文件数据
  • 三种写回策略的解释
  • Always(总是)同步:

它的意思是每次写操作命令执行完后,同步将 AOF 日志数据写回硬盘;

  • Everysec(每秒)异步:

它的意思是每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,然后每隔一秒就要在子线程中对AOF文件进行一次同步,创建一个异步任务执行fsync()函数将缓冲区里的内容写回到硬盘;

  • No(从不)

将缓冲区的内容写入AOF文件后,何时进行同步由操作系统控制,不执行fsync()函数性能好,可靠性低,宕机可能会丢失较多数据。

也就是每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,再由操作系统决定何时将缓冲区内容写回硬盘。

  • 回写策略的对比和选择

从上面对三种策略的解释就可以知道这 3 种写回策略都无法同时完美解决「主进程阻塞」和「减少数据丢失」的问题,因为两个问题是对立的,偏向于一边的话,就会要牺牲另外一边,下面可以根据这三种策略的侧重点来分析该怎么选择

  • Always(总是):

同步写回,可以很好的解决数据丢失问题,可靠性高,相对应的会影响主线程的性能

如果要高可靠,可以选择 Always 。

  • Everysec(每秒):

每秒写回,可能会造成少量数据的丢失,对主线程的性能影响也会降低

可靠性和性能都适中。

如果允许少量数据的丢失,但又想性能高,可以选择 Everysec

这个也是redis默认的写回策略。

  • No(从不)

这个策略相对于Always性能更高,但是它不能保证数据的丢失问题(宕机可能会丢失较多数据),是最不安全的一种策略。

性能好,可靠性低,

如果要高性能,可以选择 No。

  • AOF重写机制
  • 重写机制是什么

AOF日志是一个文件,随着执行命令的增加,文件会变得越来越大,当文件大到一定程度时,命令写入日志的效率就会变低,同时因为文件大的问题,在进行数据恢复时,将会降低速度。为了避免这个问题,redis增加了重写机制,当重写机制被触发时,会读取当前数据库中所有的键值对,记录下键值对的最终状态,就是将某个键值对的多次操作产生的多条命令压缩为一条命令并记录到 新的AOF文件,记录的命令少了,文件变小了。

  • 重写后AOF文件变小的原因
  • 进程内已经超时的数据不再写入文件。

  • 会删除旧的AOF文件中的无效命令如:

del key1、 hdel key2、 srem keys、 set a111、 set a222等。

重写使用进程内数据直接生成, 这样新的AOF文件只保留最终数据的写入命令。

  • 将多条写命令合并为一个如:

lpush list a、 lpush list b、 lpush list c可以转化为: lpush list a b c。

  • 为了防止单条命令过大造成客户端缓冲区溢出, 对于list、 set、 hash、 zset等类型操作, 以64个元素为界拆分为多条。

  • 重写机制的触发方式
  • 手动触发:手动发送bgrewriteaof指令(redis-cli bgrewriteaof)

  • 自动触发:

涉及到两个配置参数,只有AOF文件大小同时超出下面这两个配置项时,会触发AOF重写:

auto-aof-rewrite-min-size:AOF重写时文件的最小大小,默认为64MB;

auto-aof-rewrite-percentage:重写百分比,当前AOF文件比上一次重写后AOF文件的增加的值(当前AOF文件大小减去上一次重写后AOF文件大小),用这个值和上一次重写后AOF文件大小进行除法运算,最后得到的百分比。

举例理解第二个参数,比如参数配置的是50,即50%,当第一次重写后aof文件大小为60mb,随着操作进行aof文件继续被追加,当aof的文件大小超过60 + 60*50% = 90mb时,就会触发第二次重写,第二次重写的前提也是要文件大小大于auto-aof-rewrite-min-size配置的大小。

  • 执行重写的过程

和 AOF 日志由主线程写回不同,重写过程是由后台子进程 bgrewriteaof 来完成的,这也是为了避免阻塞主线程,导致数据库性能下降。

我们把重写的过程总结为"一个拷贝,两处日志”

  • 当触发重写时,首先会判断当前有没有 bgsave 命令(RDB 持久化)/ bgrewriteaof 在执行,倘若有,则这些命令执行完成以后在执行。(一个拷贝

  • 倘若可以执行重写,主进程 将fork 出一个后台的 重写子进程,在这一个短暂的时间内,redis是阻塞的。

  • 主进程 fork 完子进程后,返回继续接受客户端请求。子进程就可以在不影响主线程的情况下,逐一把拷贝的数据写成操作,记入重写日志(后台子线程创建的新日志文件)。有写命令依然写入现有的 AOF 文件缓冲区并根据 写回策略同步到磁盘,保证原有 AOF 文件完整和正确。此时,客户端的写请求不仅仅写入现有的 AOF 文件缓冲区,还会在重写日志缓冲区记录一份,以保证数据库的最新状态。

  • 子进程写完新的AOF文件后,向主进程发信号,主线程更新统计信息。

  • 主进程把重写日志缓冲区的数据写入到新的 AOF 文件(避免重写文件的数据丢失)。

  • 使用新的AOF文件覆盖旧的AOF文件,标志AOF重写完成。

  • AOF的优点
  • AOF 持久化保存的数据更加完整,一般 AOF 默认的写回策略是隔 1 秒通过一个后台线程执行一次写回 操作

  • AOF日志文件是一个纯追加的文件,文件不容易破损。就算服务器突然Crash,也不会出现日志的定位或者损坏问题。甚至如果因为某些原因(例如磁盘满了)命令只写了一半到日志文件里,我们也可以用redis-check-aof --fix这个命令进行修复

  • AOF 持久化文件,非常容易理解和解析,它是把所有 Redis 键值操作命令,以文件的方式存入了磁盘。非常适合做灾难性的误删除的紧急恢复

比如某人不小心用 flushall 命令清空了所有数据,只要这个时候后台rewrite还没有发生,那么就可以立即拷贝 AOF 文件,将最后一条 flushall 命令给删了,然后再将该 AOF 文件放回去,就可以通过恢复机制,自动恢复所有数据。

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

  • 在 Redis 负载比较高的情况下,RDB 比 AOF 性能更好;

  • RDB 使用快照的形式来持久化整个 Redis 数据,而 AOF 只是将每次执行的命令追加到 AOF 文件中,因此从理论上说,RDB 比 AOF 更健壮。

  • RDB-AOF混合持久化模式
  • 混合持久化模式的含义及保存文件是的流程

RDB-AOF混合持久化模式是Redis4.0开始引入的,这种模式是基于AOF持久化构建而来的。Redis服务器在执行AOF重写操作时,会像执行BGSAVE命令一样,根据数据库当前的状态生成相应的RDB数据,并将其写入新的AOF文件中,而从重写开始到重写结束期间所执行的Redis命令,则以协议文本的方式追加到新的AOF文件的末尾,即RDB数据之后,新的文件一开始不叫appendonly.aof,等到重写完新的AOF文件才会进行改名,覆盖原有的AOF文件,完成新旧两个AOF文件的替换。

  • 混合持久化模式优点

这个方法既能享受到 RDB 文件快速恢复的好处,又能享受到 AOF 只记录操作命令的简单优势, 实际环境中用的很多。

  • 开启混合持久化方式

用户可以通过配置文件中的“aof-use-rdb-preamble yes”配置项开启

注意: 开启它之前必须现开启AOF

  • redis重启加载从持久化文件中恢复数据
  • redis重启时判断是否开启aof,如果开启了aof,那么就优先加载aof文件;

  • 如果aof存在,那么就去加载aof文件,加载成功的话redis重启成功,如果aof文件加载失败,那么会打印日志表示启动失败,此时可以去修复aof文件后重新启动;

  • 若aof文件不存在,那么redis就会转而去加载rdb文件,如果rdb文件不存在,redis直接启动成功;

  • 如果rdb文件存在就会去加载rdb文件恢复数据,如加载失败则打印日志提示启动失败,如加载成功,那么redis重启成功,且使用rdb文件恢复数据;

  • 那么为什么会优先加载AOF呢?

因为AOF保存的数据更完整,通过上面的分析我们知道AOF基本上最多损失1s的数据。

  • 多节点

  • 多节点的引入

单机模式下,如果Redis服务器出现故障,内存中的数据将不复存在,而对应的客户端请求都将打到数据库服务器上,当访问量非常大的情况下,数据库服务器是会崩的。虽然Redis服务恢复了正常,可以通过RDB和AFO从磁盘中恢复数据,但是如果磁盘出现了故障,数据仍旧是不可用的,并且,在单机模式下,读写是不分离的,大量的请求也会出现IO性能瓶颈。

为了进一步提高可用性Redis 提供了三种部署模式:主从模式,哨兵模式,集群模式

  • 主从模式:
  • 概述

主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master),后者称为从节点(slave);数据的复制是单向的,只能由主节点到从节点。

  • 作用
  • 数据备份:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。

  • 故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。

  • 负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。

  • 主从库之间采用的是读写分离的方式。

  • 读操作:主库、从库都可以接收;

  • 写操作:首先到主库执行,然后,主库将写操作同步给从库。

  • 高可用基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。

  • 原理(复制过程)

主从复制过程大体可以分为3个阶段:连接建立阶段(即准备阶段)、数据同步阶段、命令传播阶段;

  • 从节点开启主从复制,有3种方式:

配置文件:

在从服务器的配置文件中加入:slaveof <masterip> <masterport>(常用)

启动命令:

redis-server启动命令后加入 --slaveof <masterip> <masterport>

客户端命令:

Redis服务器启动后,直接通过客户端执行命令:slaveof <masterip> <masterport>,则该Redis实例成为从节点。

先通过 redis-cli -h host -p port -a password 命令进入客户端,然后再执行

slaveof <masterip> <masterport>

  • 连接建立阶段(即准备阶段)

该阶段的主要作用是在主从节点之间建立连接,为数据同步做好准备。

  • 保存主节点信息

当客户端向从服务器发送 slaveof 命令时:

从服务器将主 机ip和端口分别用masterhost 和 masterport 这两个字段接受并保存。同时从服务器将向发送slaveof 命令的客户端返回 OK,表示复制指令已经被接收,而实际上复制工作是在 OK 返回之后进行。

  • 建立 socket 连接

从节点(slave)内部通过每秒运行的定时任务replicationCron()维护复制相关逻辑,当定时任务发现了有主节点可以连接时,便会根据主节点的ip和port建立socket连接。

如果创建成功,则:

从节点:为该socket建立一个专门处理复制工作的文件事件处理器,负责后续的复制工作,如接收RDB文件、接收命令传播等。

主节点:接收到从节点的socket连接后(即accept之后),为该socket创建相应的客户端状态,并将从节点看做是连接到主节点的一个客户端,后面的步骤会以从节点向主节点发送命令请求的形式来进行。

  • 发送 ping 命令

连接建立成功后从节点发送 ping 请求进行首次通信,ping 请求主要目的如下:

  1. 检测主从之间网络套接字是否可用。

  1. 检测主节点当前是否能够处理请求。

主节点的响应:

  1. 发送 pong :连接成功,继续复制流程

  1. 返回pong以外的信息:主节点 Master 不正常,从节点断开复制连接,下次定时任务会发起重连

  1. 未响应、超时,网络超时:从节点断开复制连接,下次定时任务会发起重连

  • 权限验证

主未设置密码(requirepass==“”) ,从也不用设置密码(masterauth=“”)

主设置密码(requirepass!=""),从需要设置密码(masterauth=主的requirepass的值) 或者从通过auth命令向主发送密码

  • 发送端口信息

在身份验证步骤之后,从服务器向主服务器发送从服务器的监听端口号,主节点将该信息保存到该从节点对应的客户端的slave_listening_port字段中

  • 数据同步阶段

主从节点之间的连接建立以后,便可以开始进行数据同步,该阶段可以理解为从节点数据的初始化。该阶段是主从复制最核心的阶段,根据主从节点当前状态的不同,可以分为全量复制和部分复制。

具体执行的方式是:

  1. 从节点会根据当前自身的状态来决定调用PSYNC [runid] [offset]命令发起同步请求

  • runid:上次复制的主节点的主服务器ID(实例启动时自动生成的一个随机 ID,用来唯一标记这个实例)用于定位去同步那个主服务器的

  • offset:上次复制截止时从节点保存的复制偏移量,用于定位具体的同步位置的

  • 主从节点第一次进行复制时,从节点发送命令为psync ? -1

  • 如果从节点之前未执行过slaveof或最近执行了slaveof no one,主从节点为第一次进行复制,请求全量复制。

  • 如果从节点之前执行了slaveof,向主节点发送 PSYNC [runid] [offset]

  1. 主节点接收到同步请求,根据对比 [runid] [offset]来决定复制类型。

  • 如果主节点版本低于Redis2.8,则返回-ERR回复,此时从节点重新发送sync命令执行全量复制;

  • 如果主节点版本够新,且主从节点是第一次进行复制时,向从服务器返回 +FULLRESYNC <runid> <offset>表示要进行全量复制

  • 如果主节点版本够新,且runid与从节点发送的runid相同,且从节点发送的offset之后的数据在复制积压缓冲区中都存在,则回复+CONTINUE,表示将进行部分复制,从节点等待主节点发送其缺少的数据即可

  • 如果主节点版本够新,但是runid与从节点发送的runid不同,或从节点发送的offset之后的数据已不在复制积压缓冲区中(在队列中被挤出了),则回复+FULLRESYNC <runid> <offset>,表示要进行全量复制

  • 全量复制的具体步骤
  1. 主节点接到全量复制同步请求之后,将在后台执行 BGSAVE 命令,在后台生成一个 RDB 文件,并使用一个复制积压缓冲区记录从现在开始执行的所有写命令。

  1. 主节点的bgsave执行完成后,将RDB文件发送给从节点,从节点首先清除自己的旧数据,然后载入接收的RDB文件,将数据库状态更新至主节点执行bgsave时的数据库状态

  1. 主节点发送完RDB文件后,开始将前述复制积压缓冲区中的所有写命令发送给从节点,从节点完成对快照的载入,开始接受主节点发来的命令请求,并执行这些写命令,将数据库状态更新至主节点的最新状态。

  1. 如果从节点开启了AOF,则会触发bgrewriteaof的执行,从而保证AOF文件更新至主节点的最新状态

在整个全量同步进行过程中,主节点并没有被阻塞,仍然可以接受请求,如果快照同步时间过长或者复制缓存区太小,都会导致同步期间早进来的请求被后面的请求挤出缓冲区,就会导致从节点完成快照同步后无法进行增量复制,然后再次发起快照同步,有可能会陷入快照同步的死循环。

所以配置一个大小合适的复制缓存区尤为重要。

  • 增量复制的具体步骤

增量复制主要是为了解决,主从库在命令传播时出现了网络闪断后,主从库重连进行全量复制同步,开销大的问题。从 Redis 2.8 开始,增加了一种增量复制的同步方式,增量复制的实现,依赖于三个重要的概念分别是,服务运行ID(runid)、复制偏移量(offset)、复制积压缓存区(repl_backlog_buffer

网络断了之后,主从库会尝试重连并根据这三个概念来选择使用什么同步方式。

  • 复制偏移量:

主节点和从节点分别维护一个复制偏移量(offset),代表的是主节点向从节点传递的字节数;主节点每次向从节点传播N个字节数据时,主节点的offset增加N;从节点每次收到主节点传来的N个字节数据时,从节点的offset增加N。

offset用于判断主从节点的数据库状态是否一致:如果二者offset相同,则一致;如果offset不同,则不一致,此时可以根据两个offset找出从节点缺少的那部分数据。

  • 复制积压缓存区:

复制积压缓冲区是由主节点维护的、固定长度的、先进先出(FIFO)队列,默认大小1MB,无论主节点有一个还是多个从节点,都只需要一个复制积压缓冲区。在命令传播阶段,主节点除了将写命令发送给从节点,还会发送一份给复制积压缓冲区,作为写命令的备份;除了存储写命令,复制积压缓冲区中还存储了其中的每个字节对应的复制偏移量(offset)。由于复制积压缓冲区定长且是先进先出,所以它保存的是主节点最近执行的写命令;时间较早的写命令会被挤出缓冲区。

当网络恢复,主从重连时从节点将自身维护的offset发送给主节点后,主节点根据offset和缓冲区大小决定能否执行增量复制。

  • 服务运行ID:

runid是一个服务启动时产生的一个随机ID,用来唯一识别一个Redis节点。当网络恢复,主从重连时从节点将断开网络之前保存的主节点runid发送给现在连接的主节点,主节点根据这个runid来决定是否执行增量复制。

根据这三个重要概念可以判断主从重连后同步数据的方式:

主从重连时,从节点保存的主节点runid与现在连接主节点的相同,且子节点复制偏移量之后的数据仍然都在复制积压缓存区里,这是主从节点以增量复制方式进行数据同步,两点只要有一个不符合,就执行全量复制

  • 命令传播阶段

当同步数据完成后,主从节点就会进入命令传播阶段,

当主节点发生数据增减时,就会触发replicationFeedSalves()函数,接下来在 主节点上调用的每一个命令会使用replicationFeedSlaves()来同步到子节点。执行这个函数的前提条件除了主节点数据增减,还要确保子节点个数不为空。

在命令传播阶段,除了发送写命令,主从节点还维持着心跳机制:PING和REPLCONF ACK。心跳机制对于主从复制的超时判断、数据安全等有作用。

PING:主向子,发送PING命令

每隔指定的时间,主节点会向从节点发送PING命令,这个PING命令的作用,主要是为了让从节点进行超时判断。

PING发送的频率由repl-ping-slave-period参数控制,单位是秒,默认值是10s。

REPLCONF ACK:子向主,发送REPLCONF ACK命令

在命令传播阶段,从节点会向主节点发送REPLCONF ACK命令,频率是每秒1次;命令格式为:REPLCONF ACK {offset},其中offset指从节点保存的复制偏移量。

作用包括:

  1. 实时监测主从节点网络状态:该命令会被主节点用于复制超时的判断。此外,在主节点中使用info Replication,可以看到其从节点的状态中的lag值,代表的是主节点上次收到该REPLCONF ACK命令的时间间隔,在正常情况下,该值应该是0或1

  1. 检测命令丢失:从节点发送了自身的offset,主节点会与自己的offset对比,如果从节点数据缺失,主节点会推送缺失的数据(可以用于处理命令丢失等情形)。

  1. 辅助保证从节点的数量和延迟:Redis主节点中使用min-slaves-to-write和min-slaves-max-lag参数,来保证主节点在不安全的情况下不会执行写命令;所谓不安全,是指从节点数量少于设置的min-slaves-to-write值,或延迟高于设置的min-slaves-max-lag值。

而这里从节点延迟值的获取,就是通过主节点接收到REPLCONF ACK命令的时间来判断的,即前面所说的info Replication中的lag值。

另外两种以主从复制模式为基石的高可用部署模式下篇记录,没想到redis知识点那么多

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
关于JavaRedis面试题,你可以参考以下资源: 1. "Java基础教程(入门篇)"这本书中可能包含与JavaRedis相关的基础知识点,例如如何连接和操作Redis以及在Java中使用Redis的常见场景。 2. "java面试大集合"这本书中可能包含JavaRedis面试题,涵盖了Java技术栈以及与Redis相关的问题。你可以浏览这本书中的相关章节以寻找你感兴趣的JavaRedis面试题。 3. "Java基础教程(进阶篇)"这本书可能包含更深入的JavaRedis面试题,例如Java高并发和如何在Java中使用Redis进行缓存。 这些资源可能会给你提供一些有关JavaRedis面试题的参考。你可以根据自己的需求和兴趣选择适合的资源进行学习。希望这些资源能帮助到你。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [redis面试题总结(附答案)](https://blog.csdn.net/guorui_java/article/details/117194603)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* [java面试大集合一共485页](https://download.csdn.net/download/wm9028/88268176)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值