Redis面试问题

Redis各数据类型常见应用场景?

String

String是简单的 key-value 键值对,value 不仅可以是String,也可以是数字。String在redis内部存储默认就是一个字符串,被redisObject所引用,当遇到incr,decr等操作时会转成数值型进行计算,此时redis object的encoding字段为int。
String是最常用的一种数据类型,一般用于存储简单的字符比如存个标志位,或者分布式锁,又比如存储一下当前优惠券的剩余数量。注意,其内部实现为SDS

SDS

SDS字符串在 Redis 中是很常用的,键值对中的键是字符串类型,值有时也是字符串类型。Redis 是用 C 语言实现的,但是它没有直接使用 C 语言的 char* 字符数组来实现字符串,而是自己封装了一个名为简单动态字符串(simple dynamic string,SDS) 的数据结构来表示字符串,也就是 Redis 的 String 数据类型的底层数据结构是 SDS。既然 Redis 设计了 SDS 结构来表示字符串,肯定是 C 语言的 char* 字符数组存在一些缺陷。要了解这一点,得先来看看 char* 字符数组的结构。

C 语言字符串的缺陷

C语言的字符串其实就是一个字符数组,即数组中每个元素是字符串中的一个字符。比如,下图就是字符串“xiaolin”的 char* 字符数组的结构:

这里注意对于C/C++ strlen和sizeof的返回值可能不同

C 语言标准库中的字符串操作函数就通过判断字符是不是 “\0” 来决定要不要停止操作,如果当前字符不是 “\0” ,说明字符串还没结束,可以继续操作,如果当前字符是 “\0” 是则说明字符串结束了,就要停止操作。举个例子,C 语言获取字符串长度的函数 strlen,就是通过字符数组中的每一个字符,并进行计数,等遇到字符为 “\0” 后,就会停止遍历,然后返回已经统计到的字符个数,即为字符串长度。下图显示了 strlen 函数的执行流程:
很明显,C 语言获取字符串长度的时间复杂度是$ O(N)(这是一个可以改进的地方)。
C 语言字符串用 “\0” 字符作为结尾标记还有第二个缺陷。
假设有个字符串中有个 “\0” 字符,这时在操作这个字符串时就会提早结束,比如 “xiao\0lin” 字符串,计算字符串长度的时候则会是 4,如下图:
因此,除了字符串的末尾之外,字符串里面不能含有 “\0” 字符,否则最先被程序读入的 “\0” 字符将被误认为是字符串结尾,这个限制
使得 C 语言的字符串只能保存文本数据,不能保存像图片、音频、视频文化这样的二进制数据
(这也是一个可以改进的地方)

SDS 结构设计

下图就是 Redis 5.0 的 SDS 的数据结构:

结构中的每个成员变量分别介绍下:
len,记录了字符串长度。这样获取字符串长度的时候,只需要返回这个成员变量值就行,时间复杂度只需要 O ( 1 ) O(1) O(1)
alloc分配给字符数组的空间长度。这样在修改字符串的时候,可以通过 alloc - len 计算出剩余的空间大小,可以用来判断空间是否满足修改需求,如果不满足的话,就会自动将 SDS 的空间扩展至执行修改所需的大小,然后才执行实际的修改操作,所以使用 SDS 既不需要手动修改 SDS 的空间大小,也不会出现前面所说的缓冲区溢出的问题。
flags用来表示不同类型的 SDS。一共设计了 5 种类型,分别是 sdshdr5、sdshdr8、sdshdr16、sdshdr32 和 sdshdr64,后面在说明区别之处。
buf[],字符数组,用来保存实际数据。不仅可以保存字符串,也可以保存二进制数据。
总的来说,Redis 的 SDS 结构在原本字符数组之上,增加了三个元数据:len、alloc、flags,用来解决 C 语言字符串的缺陷。

O ( 1 ) O(1) O(1)复杂度获取字符串长度

C 语言的字符串长度获取 strlen 函数,需要通过遍历的方式来统计字符串长度,时间复杂度是 O ( n ) O(n) O(n)。而 Redis 的 SDS 结构因为加入了 len 成员变量,那么获取字符串长度的时候,直接返回这个成员变量的值就行,所以复杂度只有 O ( 1 ) O(1) O(1)

二进制安全

因为 SDS 不需要用 “\0” 字符来标识字符串结尾了,而是有个专门的 len 成员变量来记录长度,所以可存储包含 “\0” 的数据。但是** SDS 为了兼容部分 C 语言标准库的函数, SDS 字符串结尾还是会加上 “\0” 字符**。
因此, SDS 的 API 都是以处理二进制的方式来处理 SDS 存放在 buf[] 里的数据,程序不会对其中的数据做任何限制,数据写入的时候时什么样的,它被读取时就是什么样的。
通过使用二进制安全的 SDS,而不是 C 字符串,使得 Redis 不仅可以保存文本数据,也可以保存任意格式的二进制数据。

不会发生缓冲区溢出

C 语言的字符串标准库提供的字符串操作函数,大多数(比如 strcat 追加字符串函数)都是不安全的,因为这些函数把缓冲区大小是否满足操作需求的工作交由开发者来保证,程序内部并不会判断缓冲区大小是否足够用,当发生了缓冲区溢出就有可能造成程序异常结束。
所以,Redis 的 SDS 结构里引入了 alloc 和 len 成员变量,这样 SDS API 通过 alloc - len 计算,可以算出剩余可用的空间大小,这样在对字符串做修改操作的时候,就可以由程序内部判断缓冲区大小是否足够用。
而且,当判断出缓冲区大小不够用时,Redis 会自动将扩大 SDS 的空间大小(小于 1MB 翻倍扩容,大于 1MB 按 1MB 扩容),以满足修改所需的大小。
在扩展 SDS 空间之前,SDS API 会优先检查未使用空间是否足够,如果不够的话,API 不仅会为 SDS 分配修改所必须要的空间,还会给 SDS 分配额外的「未使用空间」。
这样的好处是,下次在操作 SDS 时,如果 SDS 空间够的话,API 就会直接使用「未使用空间」,而无须执行内存分配,有效的减少内存分配次数。所以,使用 SDS 即不需要手动修改 SDS 的空间大小,也不会出现缓冲区溢出的问题。

节省内存空间

SDS 结构中有个 flags 成员变量,表示的是 SDS 类型。

Redis 一共设计了 5 种类型,分别是 sdshdr5、sdshdr8、sdshdr16、sdshdr32 和 sdshdr64。

这 5 种类型的主要区别就在于,它们数据结构中的 len 和 alloc 成员变量的数据类型不同。

比如 sdshdr16 和 sdshdr32 这两个类型,它们的定义分别如下:

struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len;
    uint16_t alloc; 
    unsigned char flags; 
    char buf[];
};


struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len;
    uint32_t alloc; 
    unsigned char flags;
    char buf[];
};

可以看到:

  • sdshdr16 类型的 len 和 alloc 的数据类型都是 uint16_t,表示字符数组长度和分配空间大小不能超过 2 的 16 次方。
  • sdshdr32 则都是 uint32_t,表示表示字符数组长度和分配空间大小不能超过 2 的 32 次方。

之所以 SDS 设计不同类型的结构体,是为了能灵活保存不同大小的字符串,从而有效节省内存空间。比如,在保存小字符串时,结构头占用空间也比较少。

除了设计不同类型的结构体,Redis 在编程上还使用了专门的编译优化来节省内存空间,即在 struct 声明了 __attribute__ ((packed)) ,它的作用是:告诉编译器取消结构体在编译过程中的优化对齐,按照实际占用字节数进行对齐

比如,sdshdr16 类型的 SDS,默认情况下,编译器会按照 2 字节对齐的方式给变量分配内存,这意味着,即使一个变量的大小不到 2 个字节,编译器也会给它分配 2 个字节。

举个例子,假设下面这个结构体,它有两个成员变量,类型分别是 char 和 int,如下所示:

#include <stdio.h>

struct test1 {
    char a;
    int b;
 } test1;
 
int main() {
     printf("%lu\n", sizeof(test1));
     return 0;
}

大家猜猜这个结构体大小是多少?我先直接说答案,这个结构体大小计算出来是 8。

List

Redis列表是简单的字符串列表,可以类比到C++中的std::list,简单的说就是一个链表或者说是一个队列。可以从头部或尾部向Redis列表添加元素。列表的最大长度为2^32 - 1,也即每个列表支持超过40亿个元素。
比如b站的关注列表、粉丝列表等都可以用Redis的list结构来实现,再比如有的应用使用Redis的list类型实现一个简单的轻量级消息队列,生产者push,消费者pop/bpop。

Hash

Redis Hash对应Value内部实际就是一个HashMap,实际这里会有2种不同实现,这个Hash的成员比较少时Redis为了节省内存会采用类似一维数组的方式来紧凑存储,而不会采用真正的HashMap结构,对应的value redisObject的encoding为zipmap,当成员数量增大时会自动转成真正的HashMap
hash使我们平时使用比较多的。作用和java的map非常相似经常用来存储一些多key信息,比如我们用来缓存一下某个地区下活动的信息,活动的信息可能包含各种各样的属性。或者缓存商品信息,例如库存,定价等

set

set 的内部实现是一个 value永远为null的HashMap,实际就是通过计算hash的方式来快速排重的,这也是set能提供判断一个成员是否在集合内的原因
set一般用于存一些黑名单,白名单之类的,像他提供的一些交集,并集的api也是十分好用的,可以直接进行计算。

Sorted Set

Redis有序集合类似Redis集合,不同的是增加了一个功能,即集合是有序的。一个有序集合的每个成员带有分数,用于进行排序。
Redis sorted set的内部使用HashMap和跳跃表(skipList)来保证数据的存储和有序,HashMap里放的是成员到score的映射,而跳跃表里存放的是所有的成员,排序依据是HashMap里存的score,使用跳跃表的结构可以获得比较高的查找效率,并且在实现上比较简单。
非常常见的就是各种各样的排行榜啦!

AOF 和 RDB

AOF 和 RDB 都是 Redis 持久化机制,用于将 Redis 内存中的数据保存到磁盘上,以便在 Redis 重启或崩溃后能够恢复数据。

AOF(Append Only File)是一种日志形式的持久化方式,通过将 Redis 执行的写命令追加到 AOF 文件中来记录数据的变化。AOF 文件可以通过配置实现自动重写,减少文件的大小。AOF 文件的优点是可以提供更好的数据安全性,因为它记录了所有的写操作,而且可以通过配置不同的同步方式来保证数据的一致性。AOF 文件也具有更好的可读性,可以通过简单的文本编辑器来查看和分析文件内容,对于数据的备份和恢复也更加方便。

RDB(Redis DataBase)是一种快照形式的持久化方式,通过将 Redis 内存中的数据在指定时间间隔内进行定期快照备份,将快照保存到磁盘上。RDB 的优点是备份和恢复速度快,对于数据量较大的情况下,RDB 的恢复速度通常要比 AOF 文件快。此外,RDB 也可以通过配置自动化备份,来实现更好的数据保护和灾备。

在实际应用中,可以选择使用 AOF、RDB 或者两种持久化方式的结合使用,以适应不同的数据安全和恢复需求。AOF 比 RDB 更适合对数据的高频写入,而 RDB 更适合对数据的高频读取。

如何实现亿级的数据统计?

问题:我们的系统现在有20亿个用户,我们现在想快速的统计出哪些用户在登录态?

差的方案

用mysql进行存储,我们设置一个login_stat us。当用户登录了,我们就将其置为1,当用户退出了我们就将其改为0。然后进行查询,状态为1的数量。
20亿的用户,频繁改变数据库,io极大,数据库性能可能会被拖垮

方案升级

用redis来进行数据统计,定义一个set类型的key,login_user,如果我们的用户登录了,我们就往这个set中进行数据添加,如果用户进行了退出了,我们就把它从set中进行删除。我们还可以通过scard获取登录成员的数量,从而得到已经登录的用户数。还可以使用 O ( 1 ) O(1) O(1) 复杂度来对user进行判断是否登录了。
如果有10亿个用户,集合中有10亿个userld,每个userld按照4字节来算,就会导致我们的内存达到4G。集合太大了,耗费资源。

*最终方案

我们使用redis的bigmap来进行存储

setbit命令

语法:

setbit key offset value

设置或修改key上的偏移量 (offset) 的位 (value) 的值。
返回值:指定偏移量 (offset) 原来存储的值。

getbit命令

语法:

 getbit key offset

查询key所存储的字符串值,获取偏移量上的位
返回值: 返回指定key上的偏移量,若key不存在,那么返回0

bitcount 命令

语法:

bitcount key [start] [end]

计算给定key的字符串值中,被设置为1的位bit的数量
返回值:1比特位的数量

*实现

bigmap类型 key:login_status。offset: userid
用户进行登录则设置为

 setbit login_status 12345 1

用户进行登出则设置为

setbit login_status 12345 0

判断用户是否在线getbit login_status 12345
统计数量 bigcount login_status
10亿用户,1个用户只占用1bit 相比于set占用的4字节,优化了32倍,那么节省后的内存为125MB。
优点:查找,去效率高
缺点: 结果数据不能重复,数据如果太过分散会造成浪费,只有数据密集才可以。

Redis的大key如何处理?

大key的定义

  1. string类型的值大于10kb
  2. hash、list、set、zset元素个数超过5000个

如何找到大key

string类型通过命今查找

redis-cli -h 127.0.0.1 -p6379 -a "password" --bigkeys

RdbTools工具

rdb dump.rdb -c memory --bytes 10240 -f redis.csv

如何删除大key?

直接删除大key会造成阻塞,因为redis是单线程执行,阻塞期间,其他所有请求可能都会超时。超时越来越多,会造成redis连接会耗尽,产生各种异常

低峰期删除:

凌晨,观察qPS,选择低的时候,无法彻底解决阻塞

分批次删除:

对于hash,使用hscan扫描法,对于集合采用srandmember每次随机取数据进行删除。对于有序集合可以使用zremrangebyrank直接删除,对于列表直接pop即可。

异步删除法:

unlink代替del来删除,这样redis会将这个key放入到一个异步线程中,进行删除,这样不会阻塞主线程。

什么是缓存穿透?

缓存穿透是指高并发场景下,某一个key被高并发访问,然而缓存中没有这个key对应的数据,为了健壮性,我们的程序会到数据库中进行读取,然而数据库中也没有相应的数据,这会导致缓存这依然为空,这就会导致大量的无效请求打到数据库上,如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。严重的情况下会影响线上业务。

缓存穿透解决方案

  1. 接口层增加校验, 如用户鉴权校验,id做基础校验,id<=口的直接拦截;
  2. 对查询接口为空的对象也进行缓存,将null也存入缓存中。要保证好时效性,设置失效时间。
  3. 布隆过滤器: 将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉从而避免了对底层存储系统的查询压力ref

为什么Redis宕机还能恢复数据?

RDB与AOF

大家知道Redis是将数据存在内存中的,那么一旦宕机内存的数据就会丢失,那么我们在公司日常为什么没有这个困扰呢?原因就是RDB和AOF,Redis的两种持久化机制。为了防止宕机产生的数据丢失,Redis可以将数据放到磁盘中当重启后,进行数据的恢复。
RDB: 将内存中的数据定时dump到磁盘上。
AOF:将Redis的命令以日志追加的方式写入到文件上

RDB

RDB的持久化是在指定时间内将内存中的数据快照写入磁盘里面,定义一个二进制的数据格式,为了防止频繁的备份,产生不必要的消耗,于是redis是有参数配置的,通过参数来确定生成快照的频率。
save 9001# 900秒 (15分钟) 内有1个写入
save 300 10 # 300秒 (5分钟)内有10个写入
save 60 10000 # 60秒 (1分钟)内有10000个写入Redis想到,如果我备份的时候,使用单线程,那么我的服务就停止了,于是RDB都是由fork出的子线程来进行的。

AOF

了解了RDB之后,大家会想到一个问题,周期性的备份,在高请求下,如果没有触发阙值,是不是数据就丢了。Redis就又搞了一个AOF,每次产生命令,他会过滤掉读操作把写记录先全部放入到缓冲区,然后再把缓冲区的数据刷入到磁盘保存下来。那么什么时候刷入磁盘呢。Redis又搞了个参数appendsync,提供alwayseverysecno三种选项。随着越来越多的命令写入磁盘,redis为了瘦身,就又搞了一个aof重写。aof重写由fork子线程触发,每次触发的时候,会把新的写入命令也放入到重写缓冲区,防止数据不一致。

5. 脑裂问题如何解决

什么是Redis脑裂?

在生产环境中,Redis都是主从哨兵模式的高可用集群,一个master主机,多个slave从机。master提供写服务,slave提供读服务。因为网络问题,导致redis master节点跟redis slave节点和sentinel集群处于不同的网络分区,此时因为sentinel集群无法感知到master的存在,所以将slave节点提升为master节点此时存在两个不同的master节点,就像一个大脑分裂成了两个。

集群脑裂产生后,原有客户端还在基于原来的master节点继续写入数据那么新的master节点将无法同步这些数据到自身,新的数据也无法同步到老的节点,造成Redis整体的数据不一致。当网络问题解决之后,sentinel集群将原先的master节点降为slave节点,此时大家都会再从新的master中同步数据,那么老的master中的数据就会造成大量的数据丢失,没有人进行同步了。

redis中的信息是加密的还是已经解密的

Redis中的信息通常是未加密的,也就是明文存储的。Redis本身并不提供数据加密的功能,如果需要在Redis中存储敏感信息,比如密码、密钥等,应该使用加密算法对数据进行加密处理,然后再存储到Redis中。在读取这些加密数据时,需要进行解密操作才能获取原始数据。需要注意的是,加密算法的选择和实现方式也需要仔细考虑,以保证数据的安全性。

redis的内存淘汰策略是怎样的?

  1. volatile-lru:从设置过期时间的数据集(server.db[i].expires)中挑选出最近最少使用的数据淘汰。没有设置过期时间的key不会被淘汰,这样就可以在增加内存空间的同时保证需要持久化的数据不会丢失。
  2. volatile-ttl:除了淘汰机制采用LRU,策略基本上与volatile-lru相似,从设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰,ttl值越小越优先被淘汰。
  3. volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰。当内存达到限制无法写入非过期时间的数据集时,可以通过该淘汰策略在主键空间中随机移除某个key。
  4. allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰,该策略要淘汰的key面向的是全体key集合,而非过期的key集合。
  5. allkeys-random:从数据集(server.db[i].dict)中选择任意数据淘汰
  6. no-enviction:禁止驱逐数据,也就是当内存不足以容纳新入数据时,新写入操作就会报错,请求可以继续进行,线上任务也不能持续进行,采用no-enviction策略可以保证数据不被丢失,这也是系统默认的一种淘汰策略。

实际用法

在Redis中,数据有一部分访问频率较高,其余部分访问频率较低,或者无法预测数据的使用频率时,设置allkeys-lru是比较合适的。
如果所有数据访问概率大致相等时,可以选择allkeys-random。

如果研发者需要通过设置不同的ttl来判断数据过期的先后顺序,此时可以选择volatile-ttl策略。

如果希望一些数据能长期被保存,而一些数据可以被淘汰掉时,选择volatile-lru或volatile-random都是比较不错的。
由于设置expire会消耗额外的内存,如果计划避免Redis内存在此项上的浪费,可以选用allkeys-lru 策略,这样就可以不再设置过期时间,高效利用内存了。

Redis的哨兵是什么?

Redis哨兵机制

如果主节点挂了,从机是不能负责写命令的处理,必然不可取,就需要结合Redis哨兵。
Redis 主从复制的缺点:没有办法对 master 进行动态选举,需要使用 Sentinel 机制完成动态选举。

简介

Sentinel (哨兵)进程是用于监控 Redis 集群中 Master 主服务器工作的状态(主服务器本身知道从服务器信息,相当于哨兵能够监控整个集群的信息)
在 Master 主服务器发生故障的时候,可以实现 Master 和 Slave 服务器的切换,保证系统的高可用( HA )
其已经被集成在 redis2.6+ 的版本中, Redis 的哨兵模式到了 2.8 版本之后就稳定了下来。

哨兵进程的作用

监控( Monitoring ): 哨兵( sentinel ) 会不断地检查你的 Master 和 Slave 是否运作正常。
提醒( Notification ): 当被监控的某个 Redis 节点出现问题时, 哨兵( sentinel ) 可以通过API 向管理员或者其他应用程序发送通知。
自动故障迁移( Automatic failover ):当一个 Master 不能正常工作时,哨兵( sentinel )会开始一次自动故障迁移操作

1.3 哨兵模式的工作原理

在 Redis 哨兵模式中,哨兵节点通过向 Redis 集群发送命令来获取主节点和从节点的状态信息,包括连接数、延迟、偏移量等指标。如果发现主节点出现故障,则会启动选举算法,选举出一个从节点作为新的主节点。选举算法的过程如下:

  1. 哨兵节点通过广播消息向其他哨兵节点发送主节点宕机的通知。

  2. 收到通知的哨兵节点都会对 Redis 集群的从节点进行检查,确定哪个从节点可以升级为新的主节点。

  3. 选举出新的主节点后,哨兵节点会将其配置信息发送给其他从节点,让它们成为新的从节点,并同步主节点的配置信息,保证 Redis 集群的状态一致性。如果从节点无法正常工作,则哨兵节点会将其从 Redis 集群中移除,并在后续的检查中不再考虑该节点。

当主节点重新上线时,哨兵节点会将其恢复为原来的从节点,并重新选举出新的主节点。如果 Redis 集群中有多个哨兵节点,它们会通过心跳机制保持同步,确保 Redis 集群的状态一致性。
哨兵是如何选主的?
如果明确主库已经客观下线了,哨兵就开始了选主模式。

哨兵选主包括两大过程,分别是:过滤和打分。其实就是在多个从库中,先按照一定的筛选条件,把不符合条件的从库过滤掉。然后再按照一定的规则,给剩下的从库逐个打分,将得分最高的从库选为新主库

  1. 选主时,会判断从库的状态,如果已经下线,就直接过滤。
  2. 如果从库网络不好,老是超时,也会被过滤掉。看这个参数down-after-milliseconds,它表示我们认定主从库断连的最大连接超时时间。
  3. 过滤掉了不适合做主库的从库后,就可以给剩下的从库打分,按这三个规则打分:从库优先级、从库复制进度以及从库ID号
    从库优先级最高的话,打分就越高,优先级可以通过slave-priority配置。如果优先级一样,就选与旧的主库复制进度最快的从库。如果优先级和从库进度都一样,从库ID 号小的打分高

6. Redis 慢日志

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值