仅对以下资料做整理!!! 如果版权问题,立刻删除!
参考资料:
图解Redis介绍 | 小林coding
新版Java面试专题视频教程,java八股文面试全套真题+深度详解(含大厂高频面试真题)_哔哩哔哩_bilibili
尚硅谷Redis零基础到进阶,最强redis7教程,阳哥亲自带练(附redis面试题)_哔哩哔哩_bilibili
Redis面试题
Redis中的事务是一组命令的集合,是Redis的最小执行单位。它可以保证一次执行多个命令,每个事务是一个单独的隔离操作,事务中的所有命令都会被序列化、按顺序地执行,服务端在执行事务的过程中,不会被其他客户端发送来的命令请求打断。Redis事务通过MULTI(开启一个事务)、EXEC(提交事务,从命令队列中取出提交的操作命令,进行实际执行)、DISCARD(放弃一个事务,清空命令队列)、WATCH(检测一个或多个键的值在事务执行期间是否发生变化,如果发生变化,那么当前事务放弃执行)等命令来实现的。
Redis事务是不支持回滚的。但是执行的命令如果有语法错误,Redis会执行失败,这些问题可以从程序层面捕获并解决。但是如果出现其他问题,则依然会继续执行剩下的命令。
继续探究:Redis事务为什么不支持回滚?
缓存降级:指在缓存失效或缓存访问异常时,为了保证系统的可用性,通过一些机制,将请求转发到其他服务或者直接返回默认值,从而避免系统崩溃或者因为缓存故障导致业务受损。
在Redis中提供了两种数据持久化的方式:1、RDB 2、AOF
RDB是一个快照文件,它是把redis内存存储的数据写到磁盘上,当 redis实例宕机恢复数据的时候,方便从RDB的快照文件中恢复数据。
AOF的含义是追加文件,当redis操作写命令的时候,都会存储这个文件中, 当redis实例宕机恢复数据的时候,会从这个文件中再次执行一遍命令来恢复 数据。
RDB因为是二进制文件,在保存的时候体积也是比较小的,它恢复 的比较快,但是它有可能会丢数据,我们通常在项目中也会使用AOF来恢复 数据,虽然AOF恢复的速度慢一些,但是它丢数据的风险要小很多,在AOF 文件中可以设置刷盘策略,我们当时设置的就是每秒批量写入一次命令。
RDB | AOF | |
---|---|---|
持久化方式 | 定时对整个内存做快照 | 记录每一次执行的命令 |
数据完整性 | 不完整,两次备份之间会丢失 | 相对完整,取决于刷盘策略 |
文件大小 | 会有压缩,文件体积小 | 记录命令,文件体积很大 |
宕机恢复速度 | 很快 | 慢 |
数据恢复优先级 | 低,因为数据完整性不如AOF | 高,因为数据完整性更高 |
系统资源占用 | 高,大量CPU和内存消耗 | 低,主要是磁盘IO资源 但AOF重写时会占用大量CPU和内存资源 |
使用场景 | 可以容忍数分钟的数据丢失,追求更快的启动速度 | 对数据安全性要求较高常见 |
当主进程执行读操作时,访问共享内存;
当主进程执行写操作时,则会拷贝一份数据,执行写操作。
配置项 | 刷盘时机 | 优点 | 缺点 |
---|---|---|---|
Always | 表示每执行一次写命令,立即记录到AOF文件 | 可靠性高,几乎不丢数据 | 性能影响大 |
everysec | 写命令执行完先放入AOF缓冲区,然后表示每隔1秒将缓冲区数据写到AOF文件,是默认方案 | 性能适中 | 最多丢失1秒数据 |
no | 写命令执行完先放入AOF缓冲区,由操作系统决定何时将缓冲区内容写回磁盘 | 性能最好 | 可靠性较差,可能丢失大量数据 |
由于redis的单线程的,通过setnx(SET if not exists)命令,只能有一个客户端对某一个key设置值,在没有过期或删除key的时候是其他客户端是不能设置这个key的。为了锁的可重入性,使用hsetnx命令。如果是当前线程持有的锁就会计 数,如果释放锁就会在计算上减一。key是当前线程的唯一标识,value 是当前线程重入的次数。
解锁是有两个操作(保证执行操作的客户端就是加锁的客户端以及删除锁),这时就需要 Lua 脚本来保证解锁的原子性。
// 释放锁时,先比较 unique_value 是否相等,避免锁的误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
阶段一:全量同步
注:如果在rdb生成执行期间,依然有请求到了主节点,而主节点会以命令的方式记录到缓冲区,缓冲区是一个日志文件,最后把这个日志文件发送给从节点,这样就能保证主节点与从节点完全一致了,后期再同步数据的时 候,都是依赖于这个日志文件,这个就是全量同步
阶段二:增量同步
使用scan命令,千万不要使用keys命令。
优点:
缺点:
BRPOP
、BLPOP
、BRPOPLPUSH
、SUBSCRIBE
等)时,会阻塞其他客户端的请求,直到命令执行完毕才能继续处理其他请求。SORT
、KEYS
、Set 的差集、并集和交集等)时,也会阻塞其他客户端的请求,同样需要等待命令执行完毕后才能继续处理其他请求。SET
、INCR
等)可能会导致Redis阻塞。这是因为Redis需要执行内存回收操作以释放内存空间,如果回收操作耗时过长,就会导致Redis阻塞。进一步探究:如何解决?
LPUSH
和RPOP
代替BLPOP
,使用Lua脚本实现多个操作的原子性等。String :最基本的 key-value 结构,key 是唯一标识,value 是具体的值,value其实不仅是字符串, 也可以是数字(整数或浮点数),value 最多可以容纳的数据长度是 512M
。底层实现:int 和 SDS(简单动态字符串)
SDS相比于C 的原生字符串的优点?
字符串对象的内部编码是什么样子的?
保存的是一个字符串,并且这个字符申的长度小于等于 32 字节。那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串,并将对象的编码设置为embstr
, embstr
编码是专门用于保存短字符串的一种优化编码方式:
embstr 编码和 raw 编码的边界在 redis 不同版本中一样吗?
embstr
会通过一次内存分配函数来分配一块连续的内存空间来保存redisObject
和SDS
,而raw
编码会通过调用两次内存分配函数来分别分配两块空间来保存redisObject
和SDS
,这样的优点和缺点是什么?
raw
编码的两次降低为一次;内存释放也只需要一次,字符串对象的所有数据都保存在一块连续的内存里面可以更好的利用 CPU 缓存提升性能。String有哪些常用指令?
# 设置 key-value 类型的值
> SET name lin
OK
# 根据 key 获得对应的 value
> GET name
"lin"
# 判断某个 key 是否存在
> EXISTS name
(integer) 1
# 返回 key 所储存的字符串值的长度
> STRLEN name
(integer) 3
# 删除某个 key 对应的值
> DEL name
(integer) 1
批量设置 key-value 类型的值
> **MSET** key1 value1 key2 value2
OK
# 批量获取多个 key 对应的 value
> **MGET** key1 key2
1) "value1"
2) "value2"
# 充当计数器,字符串的内容为整数的时候可以使用
# 设置 key-value 类型的值
> SET number 0
OK
# 将 key 中储存的数字值增一
> INCR number
(integer) 1
# 将key中存储的数字值加 10
> INCRBY number 10
(integer) 11
# 将 key 中储存的数字值减一
> DECR number
(integer) 10
# 将key中存储的数字值键 10
> DECRBY number 10
(integer) 0
# 过期时间的相关设置
# 设置 key 在 60 秒后过期(该方法是针对已经存在的key设置过期时间)
> EXPIRE name 60
(integer) 1
# 查看数据还有多久过期
> TTL name
(integer) 51
#设置 key-value 类型的值,并设置该key的过期时间为 60 秒
> SET key value EX 60
OK
> SETEX key 60 value
OK
# 不存在就插入(not exists)
>SETNX key value
(integer) 1
String有哪些应用场景?
缓存数据:如直接缓存整个对象的 JSON
常规计数功能:用于实现问次数、点赞、转发、库存数量等等
分布式锁:有个 NX 参数可以实现「key不存在才插入」,还会对分布式锁加上过期时间(为了避免客户端发生异常而无法释放锁)。SET lock_key unique_value NX PX 10000
分布式系统中共享Session
List:简单的字符串列表,按照插入顺序排序,可以从头部或尾部向 List 列表添加元素。列表的最大长度为 2^32 - 1
,也即每个列表支持超过 40 亿
个元素。
List的底层数据结构是什么?
512
个(默认值,可由 list-max-ziplist-entries
配置),列表每个元素的值都小于 64
字节(默认值,可由 list-max-ziplist-value
配置),Redis 会使用压缩列表作为 List 类型的底层数据结构;List的常用指令有哪些?
# 将一个或多个值value插入到key列表的表头(最左边),最后的值在最前面
LPUSH key value [value ...]
# 将一个或多个值value插入到key列表的表尾(最右边)
RPUSH key value [value ...]
# 移除并返回key列表的头元素
LPOP key
# 移除并返回key列表的尾元素
RPOP key
# 返回列表key中指定区间内的元素,区间以偏移量start和stop指定,从0开始
LRANGE key start stop
# 从key列表表头弹出一个元素,没有就阻塞timeout秒,如果timeout=0则一直阻塞
BLPOP key [key ...] timeout
# 从key列表表尾弹出一个元素,没有就阻塞timeout秒,如果timeout=0则一直阻塞
BRPOP key [key ...] timeout
List有哪些应用场景?
BRPOPLPUSH
命令,这个命令的作用是让消费者程序从一个 List 中读取消息,同时,Redis 会把这个消息再插入到另一个 List(可以叫作备份 List)留存。List 作为消息队列有什么缺陷?
Hash:一个键值对(key - value)集合
Hash类型的底层实现?
512
个(默认值,可由 hash-max-ziplist-entries
配置),所有值小于 64
字节(默认值,可由 hash-max-ziplist-value
配置)的话,Redis 会使用压缩列表作为 Hash 类型的底层数据结构;Hash类型有哪些常用命令?
# 存储一个哈希表key的键值
HSET key field value
# 获取哈希表key对应的field键值
HGET key field
# 在一个哈希表key中存储多个键值对
HMSET key field value [field value...]
# 批量获取哈希表key中多个field键值
HMGET key field [field ...]
# 删除哈希表key中的field键值
HDEL key field [field ...]
# 返回哈希表key中field的数量
HLEN key
# 返回哈希表key中所有的键值
HGETALL key
# 为哈希表key中field键的值加上增量n
HINCRBY key field n
Hash的应用场景有哪些?
Set:无序并唯一的键值集合,一个集合最多可以存储 2^32-1
个元素。
Set内部实现是什么样子的?
512
(默认值,set-maxintset-entries
配置)个,Redis 会使用整数集合作为 Set 类型的底层数据结构;Set中有哪些常见命令?
# 往集合key中存入元素,元素存在则忽略,若key不存在则新建
SADD key member [member ...]
# 从集合key中删除元素
SREM key member [member ...]
# 获取集合key中所有元素
SMEMBERS key
# 获取集合key中的元素个数
SCARD key
# 判断member元素是否存在于集合key中
SISMEMBER key member
# 从集合key中随机选出count个元素,元素不从key中删除
SRANDMEMBER key [count]
# 从集合key中随机选出count个元素,元素从key中删除
SPOP key [count]
# 交集运算
SINTER key [key ...]
# 将交集结果存入新集合destination中
SINTERSTORE destination key [key ...]
# 并集运算
SUNION key [key ...]
# 将并集结果存入新集合destination中
SUNIONSTORE destination key [key ...]
# 差集运算
SDIFF key [key ...]
# 将差集结果存入新集合destination中
SDIFFSTORE destination key [key ...]
Set有哪些应用场景?
Zset:相比于 Set 类型多了一个排序属性 score。
Zset的底层实现?
128
个,并且每个元素的值小于 64
字节时,Redis 会使用压缩列表作为 Zset 类型的底层数据结构;Zset常见的命令有哪些?
# 往有序集合key中加入带分值元素
ZADD key score member [[score member]...]
# 往有序集合key中删除元素
ZREM key member [member...]
# 返回有序集合key中元素member的分值
ZSCORE key member
# 返回有序集合key中元素个数
ZCARD key
# 为有序集合key中元素member的分值加上increment
ZINCRBY key increment member
# 正序获取有序集合key从start下标到stop下标的元素
ZRANGE key start stop [WITHSCORES]
# 倒序获取有序集合key从start下标到stop下标的元素
ZREVRANGE key start stop [WITHSCORES]
# 返回有序集合中指定分数区间内的成员,分数由低到高排序。
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]
# 返回指定成员区间内的成员,按字典正序排列, 分数必须相同。
ZRANGEBYLEX key min max [LIMIT offset count]
# 返回指定成员区间内的成员,按字典倒序排列, 分数必须相同
ZREVRANGEBYLEX key max min [LIMIT offset count]
# 并集计算(相同元素分值相加),numberkeys一共多少个key,WEIGHTS每个key对应的分值乘积
ZUNIONSTORE destkey numberkeys key [key...]
# 交集计算(相同元素分值相加),numberkeys一共多少个key,WEIGHTS每个key对应的分值乘积
ZINTERSTORE destkey numberkeys key [key...]
Zset的应用场景有哪些?
BitMap:一串连续的二进制数组(0和1),可以通过偏移量(offset)定位元素。
BitMap的内部实现?
BitMap常用的命令有哪些?
# 设置值,其中value只能是 0 和 1
SETBIT key offset value
# 获取值
GETBIT key offset
# 获取指定范围内值为 1 的个数
# start 和 end 以字节为单位
BITCOUNT key start end
BitMap有哪些应用场景?
HyperLogLog:Redis 2.8.9 版本新增的数据类型,是一种用于「统计基数」的数据集合类型,基数统计就是指统计一个集合中不重复的元素个数,不是非常准确,标准误算率是 0.81%。但是每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64
个不同元素的基数,非常节省空间。
HyperLogLog常见命令有哪些?
# 添加指定元素到 HyperLogLog 中
PFADD key element [element ...]
# 返回给定 HyperLogLog 的基数估算值。
PFCOUNT key [key ...]
# 将多个 HyperLogLog 合并为一个 HyperLogLog
PFMERGE destkey sourcekey [sourcekey ...]
HyperLogLog有哪些应用场景?
GEO:Redis 3.2 版本新增的数据类型,主要用于存储地理位置信息,并对存储的信息进行操作。GEO 本身并没有设计新的底层数据结构,而是直接使用了 Sorted Set 集合类型。
# 存储指定的地理空间位置,可以将一个或多个经度(longitude)、纬度(latitude)、位置名称(member)添加到指定的 key 中。
GEOADD key longitude latitude member [longitude latitude member ...]
# 从给定的 key 里返回所有指定名称(member)的位置(经度和纬度),不存在的返回 nil。
GEOPOS key member [member ...]
# 返回两个给定位置之间的距离。
GEODIST key member1 member2 [m|km|ft|mi]
# 根据用户给定的经纬度坐标来获取指定范围内的地理位置集合。
GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]
应用场景:主要是一些位置服务,如滴滴快车
Stream:Redis 5.0 版本新增加的数据类型,Redis 专门为消息队列设计的数据类型。
Redis 是使用了一个「哈希表」保存所有键值对,哈希表的最大好处就是让我们可以用 O(1) 的时间复杂度来快速查找到键值对。哈希表其实就是一个数组,数组中的元素叫做哈希桶。
哈希桶存放的是指向键值对数据的指针(dictEntry*),这样通过指针就能找到键值对数据,然后因为键值对的值可以保存字符串对象和集合数据类型的对象,所以键值对的数据结构中并不是直接保存值本身,而是保存了 void * key 和 void * value 指针,分别指向了实际的键对象和值对象,这样一来,即使值是集合数据,也可以通过 void * value 指针找到。
redisDb 结构,表示 Redis 数据库的结构,结构体里存放了指向了 dict 结构的指针;
dict 结构,结构体里存放了 2 个哈希表,正常情况下都是用「哈希表1」,「哈希表2」只有在 rehash 的时候才用;
ditctht 结构,表示哈希表的结构,结构里存放了哈希表数组,数组中的每个元素都是指向一个哈希表节点结构(dictEntry)的指针;
dictEntry 结构,表示哈希表节点的结构,结构里存放了void * key 和 void * value 指针;
void * key 和 void * value 指针指向的是 Redis 对象,Redis 中的每个对象都由 redisObject 结构表示,如下图:
优点:
__attribute__ ((packed))
,它的作用是:告诉编译器取消结构体在编译过程中的优化对齐,按照实际占用字节数进行对齐。Redis 的链表实现优点如下:
链表的缺陷也是有的:
由连续内存块组成的顺序型数据结构,有点类似于数组。
encoding
决定;缺陷:
typedef struct dictEntry {
//键值对中的键
void *key;
//键值对中的值
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
//指向下一个哈希表节点,形成链表
struct dictEntry *next;
} dictEntry;
继续探究:如果「哈希表 1 」的数据量非常大,那么在迁移至「哈希表 2 」的时候,因为会涉及大量的数据拷贝,此时可能会对 Redis 造成阻塞,无法服务其他请求,如何处理?
继续探究:什么时情况下会触发 rehash 操作呢?
整数集合是 Set 对象的底层实现之一。当一个 Set 对象只包含整数值元素,并且元素数量不大时,就会使用整数集这个数据结构作为底层实现。
整数集合本质上是一块连续内存空间,它的结构定义如下(contents 数组并不保存任何 int8_t 类型的元素,contents 数组的真正类型取决于 intset 结构体里的 encoding 属性的值。):
typedef struct intset {
//编码方式
uint32_t encoding;
//集合包含的元素数量
uint32_t length;
//保存元素的数组
int8_t contents[];
} intset;
整数升级:
在原本的数组上扩展空间,然后在将每个元素按间隔类型大小分割。不支持降级操作。
Redis 只有 Zset 对象的底层实现用到了跳表,跳表的优势是能支持平均 O(logN) 复杂度的节点查找。
zset 结构体里有两个数据结构:一个是跳表,一个是哈希表。这样的好处是既能进行高效的范围查询,也能进行高效单点查询。
typedef struct zset {
dict *dict;
zskiplist *zsl;
} zset;
Zset 对象能支持范围查询(如 ZRANGEBYSCORE 操作),这是因为它的数据结构设计采用了跳表,而又能以常数复杂度获取元素权重(如 ZSCORE 操作),这是因为它同时采用了哈希表进行索引。
跳表是在链表基础上改进过来的,实现了一种「多层」的有序链表。
继续探究:quicklist 虽然通过控制 quicklistNode 结构里的压缩列表的大小或者元素个数,来减少连锁更新带来的性能影响,但是并没有完全解决连锁更新的问题。那怎么办?
Redis 在 5.0 新设计一个数据结构叫 listpack,最大特点是 listpack 中每个节点不再包含前一个节点的长度了。
listpack 没有压缩列表中记录前一个节点长度的字段了,listpack 只记录当前节点的长度,当我们向 listpack 加入一个新元素的时候,不会影响其他节点的长度字段的变化,从而避免了压缩列表的连锁更新问题。