Redis面试题
为什么Redis很快?
- 采用非阻塞的IO多路复用,使得单个线程可以处理多个连接,并且把相关请求直接直接压到队列中
- 纯内存操作:文件事件分派器从队列中取出事件分配给对应处理器进行处理(连接应答、命令请求、命令回复三中处理器)
- 单线程避免的线程上下文切换的消耗、加锁、解锁等消耗
并发竞争问题
key过期策略以及内存淘汰机制
-
key过期策略
-
过期集合
-
定时删除(集中处理)
- 原理:贪心策略,默认会每秒进行十次过期扫描默认不会超过 25ms
- 问题:大量key同一时间过期,造成客户端卡顿
- 案例:掌阅服务端
-
惰性删除(零散处理)
-
-
从库的过期策略
- 原理:从库不会进行过期扫描,主库AOF文件同步到从库
- 问题:del 指令没有及时同步到从库的
话,会出现主从数据的不一致 - 案例:集群环境分
布式锁的算法漏洞就是因为这个同步延迟产生的
-
-
内存淘汰机制
-
noeviction
- 不会继续服务写请求 (DEL 请求可以继续服务),读请求可以继续进行。这样可以保证不会丢失数据,但是会让线上的业务不能持续进行。这是默认的淘汰策略
-
volatile-lru
- 尝试淘汰设置了过期时间的 key,最少使用的 key 优先被淘汰。没有设置过
期时间的 key 不会被淘汰,这样可以保证需要持久化的数据不会突然丢失
- 尝试淘汰设置了过期时间的 key,最少使用的 key 优先被淘汰。没有设置过
-
volatile-ttl
- 跟上面一样,除了淘汰的策略不是 LRU,而是 key 的剩余寿命 ttl 的值, ttl
越小越优先被淘汰
- 跟上面一样,除了淘汰的策略不是 LRU,而是 key 的剩余寿命 ttl 的值, ttl
-
volatile-random
- 跟上面一样,不过淘汰的 key 是过期 key 集合中随机的 key
-
allkeys-lru
- 区别于 volatile-lru,这个策略要淘汰的 key 对象是全体的 key 集合,而不
只是过期的 key 集合。这意味着没有设置过期时间的 key 也会被淘汰
- 区别于 volatile-lru,这个策略要淘汰的 key 对象是全体的 key 集合,而不
-
allkeys-random
- 跟上面一样,不过淘汰的策略是随机的 key
-
volatile-xxx
- 策略只会针对带过期时间的 key 进行淘汰, allkeys-xxx 策略会对所有的
key 进行淘汰。如果你只是拿 Redis 做缓存,那应该使用 allkeys-xxx,客户端写缓存时
不必携带过期时间。如果你还想同时使用 Redis 的持久化功能,那就使用 volatile-xxx
策略,这样可以保留没有设置过期时间的 key,它们是永久的 key 不会被 LRU 算法淘
汰
- 策略只会针对带过期时间的 key 进行淘汰, allkeys-xxx 策略会对所有的
-
近似LRU算法
-
每个key增加一个24bit的字段,表示最后一次被访问的时间戳
-
随机采样法
-
算法:;懒惰处理,每次写操作时,随机采样5个key(可以配置),按24bit的字段进行淘汰,如果还是超出 maxmemory,继续
-
采样大小
- maxmemory_samples
-
-
maxmemory-policy
- 看上面
-
Redis3.0增加了淘汰池
-
-
redis分布式锁
-
原子操作
原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch 线程切换
-
实质:占坑
-
版本1
setnx lock:codehole true
OK
… do something critical …
del lock:codehole
(integer) 1 -
版本2
setnx lock:codehole true
OK
expire lock:codehole 5
… do something critical …
del lock:codehole
(integer) 1 -
版本3
Redis 2.8增加了setnx 和 expire 组合在一起的原子指令。
set lock:codehole true ex 5 nx OK
… do something critical …
del lock:codehole-
超时问题
不能解决超时问题,如果在加锁和释放锁之间的逻辑执行的太长,以至于超出了锁的超时限制,就会出现问题。因为这时候锁过期了,第二个线程重新持有了这把锁,但是紧接着第一个线程执行完了业务逻辑,就把锁给释放了,第三个线程就会在第二个线程逻辑执行完之间拿到了锁。
-
解决
value为一个随机数,释放锁时先匹配
随机数是否一致,然后再删除 key-
最后问题
匹配 value 和删除 key 不是一个原子操作, Redis 也没有提供类似于 delifequals 这样的指令
-
解决
Lua 脚本:Lua 脚本可以保证连续多个指令的原子性执行
-
-
-
-
可重入锁
基于 ThreadLocal 和引用计数实现
-
主从环境下问题
锁还没有来得及同步到从节点,主节点突然挂掉了。然后从节点变成了主节点,这个新的节点内部没有这个锁,所以当另一个客户端过来请求加锁时,立即就批准了。这样就会导致系统中同样一把锁被两个客户端同时持有,不安全性由此产生
-
解决:Redlock 算法
同很多分布式算法一样, redlock 算法也使用「大多数机制」
-
参考:http://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html
-
redis主从同步
-
CAP原理
C - Consistent , 一致性
A - Availability , 可用性
P - Partition tolerance , 分区容忍性- 实质:网络分区发生时,一致性和可用性两难全
-
redis主从复制是异步的,AP,但保证最终一致性
-
增量同步
Redis 同步的是指令流,主节点会将那些对自己的状态产生修改性影响的指令记录在本地的内存 buffer 中,然后异步将 buffer 中的指令同步到从节点,从节点一边执行同步的指令流来达到和主节点一样的状态,一遍向主节点反馈自己同步到哪里了 (偏移量)。
内存 buffer 是一个定长的环形数组,如果数组内容满了,就会从头开始覆
盖前面的内容。- 存在指令覆盖问题
-
快照同步
-
bgsave到磁盘文件
从节点将快照文件接受完毕后,立即执行一次全量加载,加载之前先要将当前内存的数据清空。加载完毕后通知主节点继续进行增量同步。
-
存在快照同步的死循环问题
- 解决:设置合适的复制buffer大小
-
-
高可用-Sentinel
-
Sentinel集群(类似Zookeeper集群)
-
持续监控主从节点的健康
-
客户端首先连接Sentinel获取主节点地址
-
主节点挂了也会持久监控使其变成从节点
-
限制主从延迟过大
-
min-slaves-to-write 1
表示主节点必须至少有一个从节点在进行正常复制,否则就停止对外写服
务,丧失可用性 -
min-slaves-max-lag 10
何为正常复制,何为异常复制?这个就是由第二个参数控制的,它的单位是秒,表示如果 10s 没有收到从节点的反馈,就意味着从节点同步不正常,要么网络断开了,要么一直没有给反馈
-
-
redis持久化
-
快照(全量备份)
- 实现:操作系统的多进程COW
- 文件 IO 操作是不能使用多路复用 API
- 当父进程对其中一个页面的数据进行修改时,会将被共享的页面复制一份分离出来,然后对这个复制的页面进行修改
-
AOF日志(连续增量备份)
-
AOF重写
原理:Redis 提供了 bgrewriteaof 指令用于对 AOF 日志进行瘦身。其原理就是开辟一个子进程对内存进行遍历转换成一系列 Redis 的操作指令,序列化到一个新的 AOF 日志文件中。序列化完毕后再将操作期间发生的增量 AOF 日志追加到这个新的 AOF 日志文件中,追加完毕后就立即替代旧的 AOF 日志文件了,瘦身工作就完成了
-
先写磁盘,后执行指令
-
AOF写:Linux 的 glibc 提供了 fsync(int fd)函数
-
生产
- 主节点不进行持久化操作
- 从节点进行持久化操作
-
-
redis4.x混合持久化
延时队列
-
Redis的zset
-
消息为value,到期处理时间作为 score
-
多个线程轮询 zset 获取到期
的任务进行处理- 多个线程是为了保障可用性
- 考虑并发争抢任务,确保任务不能被多次执行
-
zrangeByScore、zrem
- 缺点:zrangeByScore、zrem不是原子操作
- 改进:使用Lua Scripting在服务端进行原子操作
-
限流
-
算法
-
滑动窗口
- 使用有序集合的score维护一个时间窗口
-
漏斗
-
单机版漏斗限流
-
分布式漏斗算法
- RedisCell模块(Redis 4.0模块化)
-
-
redis寻找特定key
-
keys命令
-
keys 字符串正则
-
缺点
- 没有 offset、 limit 参数
- 时间复杂度是 O(n)
-
-
scan命令
-
redis2.8
-
scan 游标cursor 正则key limit
第一次遍历时, cursor 值为 0,然后将返回结果中第一个整数值作为下一次遍历的 cursor。一直遍历到返回的 cursor 值为 0 时结束
-
redis线程IO模型
-
非阻塞 IO
-
事件轮询(多路复用)
-
解决非阻塞IO读写后下次什么时候读写的问题
-
多路复用 API
read_events, write_events = select(read_fds, write_fds, timeout)
for event in read_events:
handle_read(event.fd)
for event in write_events:
handle_write(event.fd)
handle_others()-
select
-
等待事件时阻塞,可以设置timeout
-
输入是读写描述符列表 read_fds & write_fds
-
输出是对应的可读可写事件
-
缺点
- 读写描述符多时性能差
-
-
epoll(linux)
-
kqueue(freebsd & macosx)
-
-
Java NIO
-
-
指令队列
-
响应队列
-
定时任务
-
redis后台存在定时任务
-
线程可能阻塞在select调用
-
解决
- 最小堆
- 计算下一次任务的时间
- 上一步的时间作为select调用的timeout
-
-
拓展
- NodeJs
- Nginx
通信协议
-
文本协议
-
RESP
- Redis Serialization Protocol
-
拓展
- https://juejin.im/post/5aaf1e0af265da2381556c0e
-
redis安全
-
指令安全
-
危险指令
- keys
- flushdb
- flushall
-
修改指令
-
rename-command
- rename-command keys abckeysabc
- rename-command flushall “”
-
-
-
端口安全
-
默认监听
- *:6379
-
bind 10.100.20.13
-
requirepass 密码
-
auth密码
-
主从复制
- masterauth 密码
-
-
-
Lua脚本安全
- 以普通用户身份启动Redis
- 禁止用户输入Lua
-
SSL代理
-
ssh
-
spiped
-
原理
- 客户端redis和服务端redis各起一个spiped进程
- 对称密钥
-
-
数据结构
-
字典
-
dict
struct dict {
dictht ht[2];
//dictht是hashtable
//类似Java的HashMap
}struct dictht {
dictEntry** table; // 二维
long size; // 第一维数组的长度
long used; // hash 表中的元素个数
}struct dictEntry {
void* key;
void* val;
dictEntry* next; // 链接下一个 entry
}-
RedisDb
struct RedisDb{
dict* dict;
dict* expires;
} -
hash
-
set
-
zset
struct zset {
dict *dict; // all values value=>score
zskiplist *zsl;
}
-
-
渐进式 rehash
-
查找过程
func get(key) {
let index = hash_func(key) % size;
let entry = table[index];
while(entry != NULL) {
if entry.key == target {
return entry.value;
}
entry = entry.next;
}
} -
hash 函数
- siphash
-
hash 攻击
-
扩容条件
- 考虑bgsave
-
缩容条件
- 不考虑bgsave
-
-
ziplist(压缩列表)
struct ziplist {
int32 zlbytes;
int32 zltail_offset;
int16 zllength;
T[] entries;
int8 zlend;
}struct entry {
// 前一个 entry 的字节长度
int prevlen;
// 元素类型编码
int encoding;
// 元素内容
optional byte[] content;
}-
zset 和 hash 容器对象在元素个数较少的时候使用
-
增加元素
- 重新分配内存
- 在原内存上扩展
-
级联更新
- 修改entry的内容
- 删除中间entry
-
-
IntSet(小整数集合)
struct intset {
// 决定整数位宽是 16 位、 32 位还是 64 位
int32 encoding;
// 元素个数
int32 length;
// 整数数组,可以是 16 位、 32 位和 64 位
int contents;
}- set 集合容纳的元素都是整数并且元素个数较小时使用
-
quicklist(快速列表)
struct quicklist {
quicklistNode* head;
quicklistNode* tail;
long count; // 元素总数
int nodes; // ziplist 节点的个数
int compressDepth; // LZF 压缩深度
}struct quicklistNode {
quicklistNode* prev;
quicklistNode* next;
ziplist* zl; // 指向压缩列表
int32 size; // ziplist 的字节总数
int16 count; // ziplist 中的元素数量
// 存储形式 2bit,
//原生字节数组还是 LZF 压缩存储
int2 encoding;
}struct ziplist_compressed {
int32 size;
byte[] compressed_data;
}struct ziplist {
}-
list实现
-
早期
- 压缩列表 ziplist+双向链表linkedlist
-
改进
- quicklist
-
-
quicklistNode中ziplist限制
- int32 size; // ziplist 的字节总数
- 参数list-max-ziplist-size
-
压缩深度
- 默认压缩深度为0,不压缩
- 参数list-compress-depth
-
拓展
- https://matt.sh/redis-quicklist
-
复杂数据结构
-
HyperLogLog
-
解决精确度不高的统计需求
-
指令
pf是发明者名字首字母缩写
-
pfadd
-
pfcount
-
pfmerge
- 合并
-
没有pfcontains
-
-
解决set存储空间过大和去重问题
- sadd
- scard
-
存储
- 稀疏矩阵
- 稠密矩阵
- 12k内存
-
-
位图
-
byte数组
-
指令
-
get/set
-
getbit/setbit
-
bitcount
- 位图统计
- 统计指定位置范围内1的个数
-
bitpos
- 位图查找
- 查找指定范围内出现的第一个0或1
-
魔术指令bitfield
-
-
操作
- 零存/整存
- 零取/整取
-
-
GeoHash
-
Stream
- Redis5.0
模块(Redis4.0)
-
布隆过滤器
Redis4.0之前版本已经有相关类库实现了布隆过滤器,底层利用的是Redis的位图数据结构
高级指令
-
scan
-
info
-
unlink
- 异步化删除key,del阻塞
- 异步之前先判断key大小
-
flushdb
- flushdb async
-
flushall
- flushall async
异步删除点
Redis4.0提供
- slave-lazy-flush 从库接受完 rdb 文件后的 flush 操作
- lazyfree-lazy-eviction内存达到最大时进行淘汰
- lazyfree-lazy-expire key 过期删除
- lazyfree-lazy-server-del rename 指令删除 destKey
Redis事件监听
-
原理
- 发布订阅
-
默认不开启
XMind - Trial Version