likes
comments
collection
share

redis面试

作者站长头像
站长
· 阅读数 12

redis面试

数据结构

redis面试

String

String:String是redis最基本的类型,一个key对应一个value。redis的string类型可以包含任何数据,比如jpg图片或者序列化的对象。string类型的值最大能存储512M。String底层采用了SDS设计。 SDS 结构设计: redis面试

len:SDS 所保存的字符串长度。 alloc:分配给字符数组的空间长度。修改字符串的时候,可以通过 alloc - len 计算 观察是否需要对空间扩容。 flags:数据类型。 buf[]:保存实际数据。

SDS实际好处:

1、SDS保存了数据长度,所以时间复杂度就是O(1)。 2、空间预分配:SDS 被修改后,程序不仅会为 SDS 分配所需要的必须空间,还会分配额外的未使用空间。 3、惰性空间释放:当数据进行缩短操作,多余空间不会被回收,后面需要增长时,就不用额外去拓展空间。
List

List:是一个链表。一个key对应一个链表。先入先出,可以通过索引查询,方便新增和删除。List底层数据结构又两种:

ZipList(压缩表):列表对象保存的所有字符串元素的长度都小于 64 字节并且保存的元素数量小于 512 个。 QuickList(快速表):使用quicklist,它是一个双向链表,而且是一个基于ziplist的双向链表,quicklist的每个节点都是一个ziplist。

ZipList数据结构:redis面试

zlbytes,记录整个压缩列表占用对内存字节数; zltail,记录压缩列表「尾部」节点距离起始地址由多少字节,也就是列表尾的偏移量; zllen,记录压缩列表包含的节点数量; zlend,标记压缩列表的结束点,特殊值 OxFF(十进制255)。

Entry节点构成:

prevlen,记录了前一个节点的长度; encoding,记录了当前节点实际数据的类型以及长度; data,记录了当前节点的实际数据;

压缩表优点:将长度较短的数据用压缩链表,能够时内存更紧凑,节约内存。

为什么数据量较大和数据长度较长不适合使用ZipList?压缩表每个Entry的prevlen都记录了上一个节点的长度。如果前一个节点的长度小于 254 字节,那么 prevlen 属性需要用 1 字节的空间来保存这个长度值;如果前一个节点的长度大于等于 254 字节,那么 prevlen 属性需要用 5 字节的空间来保存这个长度值;假设出现了连续多个节点长度在250~253字节的数据,突然新增了也该字节长度超过了254数据,会导致后续节点的prevlen都从原来的1个字节拓展到4个字节,该节点就需要拓展空间。然后又导致了该节点的后续节点的prevlen长度变更......然后一直持续下去,这种就叫连锁更新。redis面试

quickList优点:quicklist 是 ziplist 和 linkedlist 的混合体,它将 linkedlist 按段切分,每一段使用 ziplist 来紧凑存储,多个 ziplist 之间使用双向指针串接起来。对于quickList已经将ZipList分成了很多小片区,在小片区内发生连锁更新也是可以接受的。redis面试

Hash

类似Java的HashMap,数组+链表。redis面试1:当Hash冲突时将具有相同哈希值的数据链接起来,以便这些数据在表中仍然可以被查询到。 2:当Hash冲突过多,导致数据链表太长就会发生rehash.

渐进式 Rehash: 为了避免 rehash 在数据迁移过程中,因拷贝数据的耗时,影响 Redis 性能的情况,所以 Redis 采用了渐进式 rehash,也就是将数据的迁移的工作不再是一次性迁移完成,而是分多次迁移。渐进式 rehash 步骤如下:

给「哈希表 2」 分配空间; 在 rehash 进行期间,每次哈希表元素进行新增、删除、查找或者更新操作时,Redis 除了会执行对应的操作之外,还会顺序将「哈希表 1 」中索引位置上的所有 key-value 迁移到「哈希表 2」 上; 随着处理客户端发起的哈希表操作请求数量越多,最终会把「哈希表 1 」的所有 key-value 迁移到「哈希表 2」,从而完成 rehash 操作。

rehash 触发条件:负载因子 = 保存节点数/哈希表大小

当负载因子大于等于 1 ,并且 Redis 没有在执行 bgsave 命令或者 bgrewiteaof 命令,也就是没有执行 RDB 快照或没有进行 AOF 重写的时候,就会进行 rehash 操作。 当负载因子大于等于 5 时,此时说明哈希冲突非常严重了,不管有没有有在执行 RDB 快照或 AOF 重写,都会强制进行 rehash 操作。

ZSet

ZSet 有两种不同的实现,分别是 ziplist 和 skiplist。具体使用哪种结构进行存储,规则如下:

ziplist:满足以下两个条件 : 1、[value,score] 键值对数量少于 128 个。 2、每个元素的长度小于 64 字节。 skiplist:不满足以上两个条件时使用跳表、组合了 hash 和 skiplist hash 用来存储 value 到 score 的映射,这样就可以在 O(1) 时间内找到 value 对应的分数 skiplist 按照从小到大的顺序存储分数 skiplist 每个元素的值都是 [value,score] 对

SkipList大致数据结构图:redis面试

Redis为什么用skipList来实现有序集合,而不是红黑树? 主要是因为实现简单,数据新增和删除比较简单,不会影响数据的大体结构。 新增和删除数据简单的原因是因为skipList的分层算法,它没有严格的分成,导致它数据结构只有大致规则,没有固定的结构。

int zslRandomLevel(void) {
    int level = 1;
    while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
        level += 1;
    return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}

直观上期望的目标是 50% 的概率被分配到 Level 1,25% 的概率被分配到 Level 2,12.5% 的概率被分配到 Level 3,以此类推...有 2-63 的概率被分配到最顶层,因为这里每一层的晋升率都是 50%。Redis 跳跃表默认允许最大的层数是 32,被源码中 ZSKIPLIST_MAXLEVEL 定义,当 Level[0] 有 264 个元素时,才能达到 32 层,所以定义 32 完全够用了。

布隆过滤器(BloomFilter)

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

BitMap

Bitmap 的底层数据结构用的是 String 类型的 SDS 数据结构来保存位数组,Redis 把每个字节数组的 8 个 bit 位利用起来,每个 bit 位 表示一个元素的二值状态(不是 0 就是 1)。可以将 Bitmap 看成是一个 bit 为单位的数组,数组的每个单元只能存储 0 或者 1,数组的下标在 Bitmap 中叫做 offset 偏移量。redis面试

实战: 1、40亿QQ号去重 将QQ号对应的偏移量的值设置为1,遍历完成所有数据,只需要统计bitMap上值为1的数据就行。

2、统计今日登陆用户将用户id作为偏移量

Redis为什么这么快(io多路复用)

1、redis基于内存2、优秀的数据结构3、io多路复用

缓存击穿,缓存穿透,缓存雪崩

问题:雪崩是大面积的key同时缓存失效;穿透是redis里不存在这个缓存key;击穿是redis某一个热点key突然失效。 解决方案: 1、雪崩:

1:随机设置key失效时间,避免大量key集体失效; 2:可将热点数据均匀分布在不同的Redis库中也能够避免key全部失效问题; 3:设置热点数据永不过期,同步去更新数据库和缓存数据;

2、穿透:

1:接口层添加校验; 2:布隆过滤器

3、击穿:

1:设置不过期 2:加分布式锁

Redis锁机制

先使用 setnx 来争抢锁,抢到之后利用 expire 设置一个过期时间防止未能释放锁。setnx 的意义是如果 key 不存在,则将key设置为 value,返回 1。如果已存在,则不做任何操作,返回 0。 eg: 加锁:如果key不存在,通过setex(setnx + expire合并方式)添加key,unpack是拼接value 和过期时间。value:一般是当前加锁的线程产生的随机数。

"if redis.call('exists', KEYS[1]) == 0 then \n"+ " return redis.call('setex', KEYS[1], unpack(ARGV)) \n" + " end ";

解锁:判定当前value和参数是否一致,一致去删除key

if redis.call('get', KEYS[1]) == ARGV[1] then \n"+ " return redis.call('del', KEYS[1]) or true \n" + "end";

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

1、先更新数据库,再更新缓存

这种是常规的做法,但是如果更新缓存失败,将会导致缓存是旧数据,数据库是新数据

2、先删除缓存,再写入数据库

这种方式能够解决方式1的问题,但是仅限于低并发的场景,不然如果有新的请求在删完缓存之后,写数据库之前进来,那么缓存就会马上更新数据库更新之前数据,造成数据不一致的问题

3、延时双删策略

这种方式是先删除缓存,然后更新数据库,最后延迟个几百毫秒再删除缓存

4、直接操作缓存,定期写入数据库

Redis持久化

1、RDB(Redis DataBase)持久化

RDB 是 Redis 中默认的持久化机制,按照一定的时间将内存中的数据以快照的方式保存到磁盘中,它会产生一个特殊类型的文件 .rdb 文件,同时可以通过配置文件中的 save 参数来定义快照的周期在 RDB 中有两个核心概念 fork 和 cow,在执行备份的流程如下:在执行bgsave的时候,Redis 会 fork 主进程得到一个新的子进程,子进程是共享主进程内存数据的,会将数据写到磁盘上的一个临时的 .rdb 文件中,当子进程写完临时文件后,会将原来的 .rdb 文件替换掉,这个就是 fork 的概念。那 cow 全称是 copy-on-write ,当主进程执行读操作的时候是访问共享内存的,而主进程执行写操作的时候,则会拷贝一份数据,执行写操作。

优点:

1、只有一个文件 dump.rdb ,方便持久化 2、容错性好,一个文件可以保存到安全的磁盘 3、实现了性能最大化,fork 单独子进程来完成持久化,让主进程继续处理命令,主进程不进行任何 I/O 操作,从而保证了Redis的高性能 4、RDB 是一个紧凑压缩的二进制文化,RDB重启时的加载效率比AOF持久化更高,在数据量大时更明显

缺点:

1、可能出现数据丢失,在两次RDB持久化的时间间隔中,如果出现宕机,则会丢失这段时间中的数据由于RDB是通过fork子进程来协助完成数据持久化,如果当数据集较大时,可能会导致整个服务器间歇性暂停服务。 2、AOF(Append Only File)持久化

AOF 全称是 Append Only File(追加文件)。当 Redis 处理每一个写命令都会记录在 AOF 文件中,可以看做是命令日志文件。该方式需要设置 AOF 的同步选项,因为对文件进行写入并不会马上将内容同步到磁盘上,而是先存储到缓冲区中,同步选项有三种配置项选择:

always:同步刷盘,可靠性高,但性能影响较大 everysec:每秒刷盘,性能适中,最多丢失 1 秒的数据 no:操作系统控制,性能最好,可靠性最差

为了解决 AOF 文件体检不断增大的问题,用户可以向 Redis 发送 bgrewriteaof 命令,可以将 AOF 文件进行压缩,也可以选择自动触发,在配置文件中配置

auto-aof-rewrite-precentage 100 auto-aof-rewrite-min-zise 64mb

优点:实现持久化,数据安全,AOF持久化可以配置 appendfsync 属性为 always,每进行一次命令操作就记录到AOF文件中一次,数据最多丢失一次通过 append 模式写文件,即使中途服务器宕机,可以通过 Redis-check-aof 工具解决数据一致性问题AOF 机制的 rewrite 模式。AOF 文件的文件大小触碰到临界点时,rewrite 模式会被运行,重写内存中的所有数据,从而缩小文件体积

缺点:

1、AOF 文件大,通常比 RDB 文件大很多 2、比 RDB 持久化启动效率低,数据集大的时候较为明显 3、AOF 文件体积可能迅速变大,需要定期执行重写操作来降低文件体积

Redis过期删除策略:

1:定时删除。

在设置 Key 的过期时间的同时,会创建一个定时器 timer,定时器在 Key 过期时间来临时,会立即执行对 Key 的删除操作 特点: 对内存友好,对 CPU 不友好。存在较多过期键时,利用定时器删除过期键会占用相当一部分CPU

2:惰性删除。

key 不使用时不管 key 过不过期,只会在每次使用的时候再检查 Key 是否过期,如果过期的话就会删除该 Key。 特点: 对 CPU 友好,对内存不友好。不会花费额外的CPU资源来检测Key是否过期,但如果存在较多未使用且过期的Key时,所占用的内存就不会得到释放

3:定期删除。

每隔一段时间就会对数据库进行一次检查,删除里面的过期Key,而检查多少个数据库,则由算法决定。 特点: 定期删除是对上面两种过期策略的折中,也就是对内存友好和CPU友好的折中方法。每隔一段时间执行一次删除过期键任务,并通过限制操作的时长和频率来减少对CPU时间的占用。

Redis内存淘汰机制

设有过期时间key:

1、volatile-lru:尝试回收最少使用的键 2、volatile-random:回收随机的键 3、volatile-ttl:优先回收存活时间较短的键

没有过期时间key:

1、allkey-lru:尝试回收最少使用的键 2、allkeys-random:回收随机的键 3、noeviction:当内存达到限制并且客户端尝试执行新增,会返回错误

Redis集群实现

Cluster:

这是Redis 自带的集群功能,它采用的分布式算法是哈希槽,而不是一致性Hash。支持主从结构,可以扩展多个从服务器,当主节点挂了可以很快切换到一个从节点作主节点,然后其他从节点都会切换读取最新的主节点。

Redis哨兵模式

哨兵主要作用:

1、监控:哨兵会不断检查用户的Master和Slave是否运作正常2、提醒:当被监控的某个Redis节点出现问题时,哨兵可以通过API向管理员或其他应用程序发送通知3、自动故障迁移:当一个Master不能正常工作时,哨兵会开始一次自动故障迁移操作,它会将集群中一个Slave提升为新的Master,并让其他Slave改为与新的Master进行同步。当客户端试图连接失败的Master时,集群也会想客户端返回新的Master地址。当主从服务器切换后,新Master的Redis.conf,Slave的Redis.conf和Sentinel的Redis.conf三者配置文件都会发生相应的改变。