REDIS面试问题总结

系统命令

shutdown 正常关闭服务器
redis-server 启动服务器
redis-cli 客户端连接服务器
flushall 删库跑路,一般不这么做

REDIS 持久化 RDB AOF区别

RDB:[Redis Database] 在指定时间间隔把内存中的数据快照写入磁盘,之后可以备份快照,或者复制到其他服务器创建相同副本,或者服务器重启也会用到这个快照恢复数据,默认持久化方式

触发时机
手动执行save和bgsave时
配置文件 设置  save <seconds> <changes>,自动间隔执行
主从复制时
执行flushall时
执行shutdown时

save 
阻塞redis服务器进程,完成之前服务器都是炸的
bgsave
主进程会fork一个子进程出来创建RDB,先把数据写入临时文件,再用二进制压缩并替换之前的RDB文件,除了fork的时候会堵塞一下下,其余全程主进程不受影响继续工作
RDB文件是采用LZF算法压缩的二进制文件,耗时但是体积小,可以关闭
config set rdbcompression no

##redis.conf配置文件  save <seconds> <changes>
`save 900 1` 当时间到900秒时,如果至少有1个key发生变化,就会自动触发`bgsave`命令创建快照
`save 300 10` 当时间到300秒时,如果至少有10个key发生变化,就会自动触发`bgsave`命令创建快照  
`save 60 10000` 当时间到60秒时,如果至少有10000个key发生变化,就会自动触发`bgsave`命令创建快照
----------------
AOF:[Append Only File] 默认没有开启,开启后每执行一条写命令就会记录到aof_buf缓存,随后写入AOF文件末尾,且根据配置定时重写压缩文件,多条合并成一条

##redis.conf配置文件
appendonly yes    //是否开启
appendfilename "appendonly.aof"   //日志名称
appendfsync always/everysec/no  每次/每秒/操作系统自行决定,默认是everysec
no-appendfsync-on-rewrite no //重写期间是否同步
auto-aof-rewrite-percentage 100 //日志文件如果增长100%触发重写
auto-aof-rewrite-min-size 64mb//日志文件大于64mb时,才会触发重写,权重比上面大

AOF重写
手动触发bgrewriteaof 和 bgsave 同一个原理,这里按下不表
重写aof文件的操作,并没有读取旧的aof文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的aof文件,RDB相似,RDB存的是二进制压缩数据,AOF存的是日志

混合持久化
aof-use-rdb-preamble yes //开启,默认关闭
bgrewriteaof时fork出的子进程先将数据以RDB方式写入aof文件,然后在将重写缓冲区的增量命令以AOF方式写入到文件,写入完成后通知主进程更新统计信息,并将新的含有RDB格式和AOF格式的AOF文件替换旧的的AOF文件
重启的时候如果开启了AOF模式,则加载AOF文件,否则加载RDB

RESP 协议 [REdis Serialization Protocol]

redis客户端 和 redis-server 之间的通讯协议,这个了解一下就好

"+OK\\r\\n"//simple string
"-ERR unknown command 'foobar'"//error msg
":1000\\r\\n"//integer
"$12\\r\\nHello World!\\r\\n"//bulk string
"$0\\r\\n\\r\\n"  //空字符串
"$-1\\r\\n"    //nil
"*2\\r\\n$5\\r\\nhello\\r\\n$5\\r\\nworld\\r\\n"//数组

架构

单机模式
优点:部署简单,成本低,高性能,不需要同步数据
缺点:有宕机风险,单线程受限于CPU处理能力,单机承载QPS大概在几万左右,如果QPS达到10+,单机模式会直接挂掉
主从复制
slaveof 192.168.1.1 6379 //Version<5.0
replicaof 192.168.249.20 6379 //Version>=5.0
成功配置一个从服务器, 从服务器向主服务器发送一个SYNC命令,主服务器BGSAVE,完成后发送RDB文件到从服务器,从服务器加载RDB文件启动,第一次连接是全量重同步
之后主服务器每执行一次写命令都会顺便发一份给从服务器,如果主从断开,重连的时候会根据offset复制偏移量部分重同步,比全量重同步块一些
优点:增加从服务器,QPS增加,降低MASTER读压力
缺点:主服务器写压力没有解决,宕机无法继续写入,从节点晋升主节点需要改代码的配置,和从服务器的配置,及其麻烦
哨兵模式 Sentinel
相当于主从模式下,给每台(主从)服务器添加一个进程,根据配置定时每秒,每十秒去判断服务器是否宕机,如果是主服务器宕机,则当有足够数量的哨兵进程确定主服务器宕机之后,将投票从从服务器中选举出新的主服务器,代码方面需要执行SENTINEL相关命令获取当前的主服务器是哪台,再实例化操作
优点:主服务器宕机的时候,不用半夜跑起来一顿操作,全自动化
缺点:已然没有解决主库写压力,QPS大的时候,该宕机还得宕机
集群模式  REDIS CLUSTER  version>3.0
采用无中心结构,至少6个节点(11从 乘以3)才能保证高可用集群,3个主服务器采用虚拟哈希槽分区,公式为
hash_slot = crc16(key) % 16384 ;
哈希槽的区间为[0,16383],扩容和缩容都是对槽的重新分配,服务不需要下线
某一组主节点服务器宕机,他的从节点会升级成主节点,宕掉的主节点重启之后会变成新主节点的从节点
也就是说每个主节点只保存有部分数据,不是全部,不过如果某一组主节点和他的从节点都宕机的话,整个集群会挂掉
优点:有条件的话肯定用这个方案啦
缺点:搭建发杂,用docker搭建会好点

一致性哈希算法

传统哈希取模算法 N表示服务器的总数
hash(key) % N  每个键都根据哈希算法分布到N台服务器中,但是当N变化时(宕机/添加)会导致键和服务器的映射关系发生变化导致缓存失效,新的键值不影响,主要是旧的键值,hash(key1)%3=1突然变成hash(key1)%2=2,到第2台服务器发现没有key1,如果同一时刻发生大量缓存失效,会导致缓存雪崩
一致性哈希算法
通过一个哈希环数据结构实现,环的取值范围[0-2^32-1]
将服务器的主机名或者IP进行哈希运算映射到哈希环上
hash(server.name/server.ip) 
将键值通过哈希运算也映射到哈希环上,在哈希环上顺时针寻找最近哈希值的服务器,把值存进去
服务器增加的时候同样哈希运算映射到哈希环,只会影响当前哈希值逆时针上一个服务器到当前哈希值之间的区间对象需要重新分配服务器,其他服务器没影响
服务器减少的时候,也只是需要把当前减少的服务器哈希值到逆时针上一个服务器哈希值之间的区间对象重新分配,其他服务器没影响
虚拟节点
可以把一台服务器虚拟成多个节点,使数据分布更加分散,但是节点越多,添加和减少服务器时重新分配的对象就越多,可以权衡一下

REDIS面试问题总结

分布式锁

单机分布式锁
set lock_resource_id 1 nx ex 10 //原子操作,抢锁10秒自动释放
正常情况:A获得锁,A执行逻辑,成功之后,A10秒内释放锁
异常情况:A获得锁,A执行逻辑,超过10,redis主动释放锁
B获得锁,A完成业务,然后把B的锁给释放了,
拉都拉不住,此时B的心态逐渐发生了一些变化
解决方案:
避免在可能会超时的场景使用锁,加锁时可以把value设置成一个自己知道的数字,释放的时候判断一下是否仍是自己的锁再删除,判断和删除是两个原子性操作,需要用到LUA脚本才能实现,这并不是一个完美解决方案,这里没有解决A超时锁被提前释放,B乘虚而入的问题,不过这也是一个无解的问题
set lock_resource_id thread_id nx ex 10
##LUA脚本##
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end    
##LUA脚本##
Redisson分布式锁
开启一个定时的守护线程/进程,判断是否要给当前锁添加过期时间,也就是再给你10秒钟
Redlock
单节点不需要用到redlock
多节点,主从复制的时候,会发生A从主节点获取锁,没同步到从节点发生故障转移,从节点被选举成主节点,B获得相同的锁,就变成两个客户端都获取到同一把锁的状态
redlock原理:客户端向所有主节点获取锁,当且仅当从一半以上的节点取到锁,且使用的时间小于锁失效的时间,才算获得成功,释放的时候向全部节点释放
php有现成的包(暴露身份了),使用简单
https://github.com/ronnylt/redlock-php
这个算法不是100可靠的,详情可以去研究
https://redis.io/topics/distlock
使用REDLOCK需要3台以上的Redis实例,运维成本高,另外REDIS发生主从切换的概率并不高,即使发生了主从切换出现锁丢失的概率也很低,因为主从切换会有一个过程,这个过程往往超过锁的超时时间,所以用第一种单机分布式锁可以解决很多问题

异步队列

List 常用命令
lpush,lpop,blpop(blocking 堵塞)
rpush,rpop,brpop(blocking 堵塞)
rpoplpush,brpoplpush(blocking 堵塞)
llen,lrange

可以用(b)rpoplpush把队列的值pop出来push到另一个队列里面,业务处理完再删除,有问题就回滚,实现消息确认机制,原子性操作
rpoplpush myqueue queuebak

缺点:不能生产一次消费多次,其实不算缺点,设计如此

PUB/SUB

发布/订阅 可以实现生产一次消费多次,但是不能实现消息持久化,客户端下线之后,就会丢失信息,设计如此

stream Version>=5.0

就目前来说,stream还不能当做主流MQ使用,慎用在生产环境
stream实现了消费组消费和应答机制,简单使用如下

xadd mystream * k1 v1 k2 v2 k3 v3//添加元素
xadd mystream 1609404470049-1 k4 v4//添加元素
xrange mystream - + //查看全部元素
xdel mystream 1609404470049-1//删除元素
xdel mystream 删除key
xlen mystream//容量
##独立消费
xread count 2 streams mystream 0//从头开始读2条
xread count 2 streams mystream $//从尾开始读最新
xread block 0 streams mysteam $//永久堵塞读最新
xread streams mystream1 mystream2 $ $//同时读两个
##消费组消费 每个组状态独立,互不影响,同一个组内多个消费者是竞争关系,每个消费组都有一个游标last_delivered_id在数组上往前移动,表示消费组消费到哪条信息,消费未应答的id存储在pending_ids里面.
xgroup create mystream mygroup $//创建组读最新数据
xinfo stream mystream//查看stream和消费者组的信息
xreadgroup group mygroup c1 count1 streams mystream //消费组mygroup的消费者c1消费1条数据

延时队列

异步队列如何和分布式锁一起使用,会出现死锁,导致异步任务集体睡眠,队列长度爆表
当加锁失败的时候,可以把消息丢到延时队列里,过一会再处理
通过zset的score来实现
zadd key 100 member1
zadd key 110 member2
zadd key 120 member3
zrangebyscore key  100 100 //member1
zrem key member1
上述写法不是原子性的,可能zrangebyscore拿出来,zrem发现不见了,使用LUA脚本
###LUA脚本###
local res = nil 
local tasks = redis.pcall("zrangebyscore", KEYS[1], ARGV[1], 0, "LIMIT", 0, 1) 
if #tasks > 0 then 
    local ok = redis.pcall("zrem", KEYS[1], tasks[1]) 
    if ok > 0 then 
        res = tasks[1] 
    end 
end 
return res
###LUA脚本###

布隆过滤器

原理:使用多个不同的哈希函数把元素哈希到一个BIT向量表中,二向箔打击,非常节省空间和成本,查找也快,缺点就是判断元素是否已经存在会有误差,很小,但是无法完全消除,不过判断元素是否不存在准确率是100%,另外不支持删除操作,REDIS并不自带布隆过滤器的实现,需要应用端实现多个哈希函数和利用REIDSSETBIT,GETBIT命令实现
应用:
1.大量的邮件,URL去重,过滤,识别
2.记录用户已经读过的文章
3.解决缓存穿透的问题

缓存穿透

一般的业务逻辑是先去REDIS查询有没缓存KEY,如果没有就去数据库查,但是一些恶意请求会故意查询不存在的key,导致每个请求直达数据库
解决方案:
1.查询结果为空的情况也缓存进REDIS,过期时间设置稍微短些
2.将所有可能存在的KEY哈希到一个足够大的bitmap中,对一定不存在的key进行过滤,用到上面说的布隆过滤器

缓存雪崩

REDIS重启或者大量缓存同一个时间点失效,这样请求又直达数据库,引起连锁反应
解决方案:
1.不同的KEY,设置不同的过期时间,可以带个随机数

HYPERLOGLOG 基数统计

本来想去研究一下原理的,知道我看到伯努利方程...
这个功能主要是用来模糊计数的,底层是位图,跟布隆过滤器差不多,误差在0.81%,占用空间很小,最多只占用12k的存储空间,可以用来统计日活月活

pfadd sign_uv_20210722  uid1
pfadd sign_uv_20210722  uid2
pfcount sign_uv_20210722   // 返回2

GEOSPATIAL 地理空间索引

将地理空间位置(经纬度)添加到key中,然后进行相关地址位置计算
GEOADD keyname 13.361389 38.115556 "Peter" 15.087269 37.502669 "Lily"
GEODIST keyname Peter Lily距离 //计算 Peter Lily距离
GEORADIUS keyname  15  37  100 km //列出(15,37)附近100公里的人
GEORADIUSBYMEMBER keyname  Peter  100 km//列出Peter附近100公里的人

淘汰策略

config get maxmemory-policy //获取内存淘汰策略
config set maxmemory-policy allkeys-lru    //设置淘汰策略
config get maxmemory    //获取REDIS能使用的最大内存
config set maxmemory 20gb     //设置REDIS最大内存

1.noeviction(默认策略):对于写请求直接返回错误,删除清除除外
2.allkeys-lru:所有keys中淘汰最久没有使用的键    //推荐使用
3.allkeys-random:所有keys中随机淘汰    //不推荐使用
4.allkeys-lfu:所有keys中淘汰使用频率最少的键    //推荐使用
5.volatile-lru:从设置了过期时间的keys中淘汰最久没有使用的键//推荐使用
6.volatile-random:从设置了过期时间的keys中随机淘汰//不推荐使用
7.volatile-ttl:从设置了过期时间的keys中淘汰剩余时间剩余最短的//推荐使用
8.volatile-lfu:从设置了过期时间的keys中淘汰使用频率最少的键    //推荐使用
5,6,7,8这三种如果没有key符合策略,则退化成第1

开发建议和性能优化

1.存储的KEY一定要设置超时时间
2.大文本数据压缩后存储,SIZE>500字节
3.禁止KEYS操作,可以用SCAN替代
4.使用HASH,SET,注意FIELD不要太多
5.禁止MONITOR操作
6.禁止大STRING,消耗带宽
7.单机内存建议在10-20GB,键的个数控制在1000万内,太多会影响回收
8.使用连接池和定时监控redis健康信息
本作品采用《CC 协议》,转载必须注明作者和本文链接
遇强则强,太强另说
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!