关于redis的面试题总结,精简回答,便于面试前快速回忆
- redis的优点和缺点有哪些?
- redis为什么快?
- redis的线程模型
- Redis单点吞吐量
- redis的事务
- redis的分布式锁
- redis的发布订阅
- redis的流水线管道操作
- Redis的内存优化
- redis的持久化原理
- Redis集群会有写操作丢失吗?为什么?
- redis的缓存雪崩-缓存穿透-缓存预热-缓存降级-缓存更新(redis的双写一致性问题)
- 1.缓存雪崩
- 2.缓存穿透
- 3.缓存预热
- 4.缓存降级
- 5.缓存更新(redis的双写一致性问题)
- redis的3种淘汰算法-6种淘汰策略
- redis的三种集群模式和优缺点
- 1.主从复制:
- 2.哨兵模式:
- 3.分槽(Cluster集群)模式:
- redis的类型-常用的场景-常用的方法和场景
- 如何解决redis的并发竞争key问题
redis的优点和缺点有哪些?
优点:操作速度快,
支持的数据类型多,
支持设置失效时间等特性,
支持集群,分布式
缺点:Redis较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂
redis是单线程的,单台服务器无法充分利用多核服务器的CPU
redis是存储内存的,注定无法保存海量数据存储。只适合小量的数据查询
缓存雪崩-缓存穿透-缓存更新等问题,下文给予解释
redis为什么快?
- io多路复用线程模型
- 单线程,避免的多线程之间的上下文切换的开销
- 操作的是内存
redis的线程模型
1.客户端通过socket连接到redis服务端
2.redis服务端通过io多路复用来观察多个客户端的socket状态
3.如果发现某个socket有事件发生则把socket转发给给事件分发器
4.事件分发器根据socket的事件类型交给不同类型的事件处理器处理
Redis单点吞吐量
单点TPS达到8万/秒,QPS达到10万/秒,补充下TPS和QPS的概念
1.QPS: 应用系统每秒钟最大能接受的用户访问量
简单理解就是qps是单一的一个命令的访问
2.TPS: 每秒钟最大能处理的请求数
tps是用户操作的一个访问,但是这个访问里需要调用N多个接口,用户请求开始到给用户反馈是一次tps。一次用户请求里的每一次接口调用返回是一次qps
(上面说的接口调用是抽象的概念,可以是访问一次redis,访问一次mysql等等)
判断接口性能还是用qps比较合理一些。
redis的事务
对于Redis而言,命令的原子性指的是:一个操作的不可以再分,操作要么执行,要么不执行。
Redis的操作之所以是原子性的,是因为Redis是单线程的。
Redis本身提供的所有API都是原子操作,Redis中的事务其实是要保证批量操作的原子性。
多个命令在并发中也是原子性的吗?
不一定, 将get和set改成单命令操作,incr 。使用Redis的事务,或者使用Redis+Lua==的方式实现.
- redis事务不支持回滚,运行期间有些语句执行错误,下面语句仍然继续执行
- 不支持分槽模式,只支持主从和哨兵,因为始终是单机在执行
- redis的事务是通过CAS的模式进行的
redis的分布式锁
目前常见的分布式锁有两种,1.redis实现分布式锁(无序)2.zookeeper实现分布式锁(有序)
咱们今天说说redis实现的方式,步骤如下:
- 使用setnx命令(key,value,time),如果key不存在则插入成功(设置一个锁的名称作为key)
- 自己生成一个标识作为value,可以是uuid,(后面会用到)
- 执行自己的业务逻辑
- 判断是否超时
- 如果没有超时判断value是否是自己的value
- 然后删除key
为什么需要value设置一个标识呢,因为当你在第4步的时候判断没有超时,但是这个时候你的锁立刻超时了然后被别人立刻抢占了,这个时候你在走第5步就会规避掉,这个时候你可能会说,那如果把刚才说的第4步和第5步换成第5步和第6步呢,所以这里我们的问题是删除key的操作没有原子性。所以我推荐把上面删除key的步骤用lua脚本去执行来保证原子性。
redis的发布订阅
"发布/订阅"模式包含两种角色,分别是发布者和订阅者。订阅者可以订阅一个或者多个频道(channel),而发布者可以向指定的频道(channel)发送消息,所有订阅此频道的订阅者都会收到此消息。
redis的发布订阅有两种用法:
1.普通的发布/订阅
发布者发布消息的命令是 publish
用法是 publish channel message
ps:publish channelA helloword订阅频道的命令是 subscribe
可以同时订阅多个频道,用法是 subscribe channel1 [channel2 …]
ps:subscribe channelA
2.按照规则发布/订阅
除了可以使用subscribe命令订阅指定的频道外,还可以使用psubscribe命令订阅指定的规则。规则支持通配符格式。命令格式为 psubscribe pattern [pattern …]订阅多个模式的频道。
通配符中?表示1个占位符,表示任意个占位符(包括0),?表示1个以上占位符。
订阅者ps: psubscribe c? b d?
redis的流水线管道操作
redis的耗时主要体现在网络带宽和内存,一个客户端到服务端之间的正常网络延迟在20ms左右,查询内存几乎是不耗时的,那么如果你要批量for循环操作100条命令要执行多久呢?不算其他耗时,只网络延迟就消耗掉了2s。redis的管道就是为了解决批量操作的场景而生的。用管道执行100条语句,客户端会批量把语句先发给服务端,然后服务端执行完毕后,将结果一次性全部发送回客户端。这样便节约了很大的网络延迟。管道流水线的api自行查询,本章不在阐述。
Redis的内存优化
- 尽量缩短key的长度,尽量缩短value的长度
- 尽量使用hash类型
- 共享对象池:即 Redis 内部维护[0-9999]的整数对象池,开发中在满足需求的前提下,尽量使用整数对象以节省内存
- 控制key的数量
- 编码优化,控制编码类型
redis的持久化原理
redis持久化主要通过两种方式:
RDB:
rdb(redisDataBase),是redis的全量数据的数据快照,默认开启,会按照配置的指定时间将内存中的数据快照到磁盘中,创建一个dump.rdb文件,Redis启动时再恢复到内存中。Redis会单独创建fork()一个子进程,将当前父进程的数据库数据复制到子进程的内存中,然后由子进程写入到临时文件中,持久化的过程结束了,再用这个临时文件替换上次的快照文件,然后子进程退出,内存释放。
特点:数据紧凑,文件传输速度快,
缺点:数据有可能缺失
AOF:
以日志的形式记录每个写操作(读操作不记录),只需追加文件但不可以改写文件,Redis启动时会根据日志从头到尾全部执行一遍以完成数据的恢复工作。包括flushDB也会执行。
主要有两种方式触发:有写操作就写、每秒定时写(也会丢数据)。
因为AOF采用追加的方式,所以文件会越来越大,针对这个问题,新增了重写机制,就是当日志文件大到一定程度的时候,会fork出一条新进程来遍历进程内存中的数据,每条记录对应一条set语句,写到临时文件中,然后再替换到旧的日志文件(类似rdb的操作方式)。默认触发是当aof文件大小是上次重写后大小的一倍且文件大于64M时触发。
当两种方式同时开启时,数据恢复Redis会优先选择AOF恢复。一般情况下,只要使用默认开启的RDB即可,因为相对于AOF,RDB便于进行数据库备份,并且恢复数据集的速度也要快很多。
开启持久化缓存机制,对性能会有一定的影响,特别是当设置的内存满了的时候,更是下降到几百reqs/s。所以如果只是用来做缓存的话,可以关掉持久化。
特点:保存的是命令语句,类似mysql的binlog文件。
缺点:看模式而定,数据有可能缺失(1s内的)
Redis集群会有写操作丢失吗?为什么?
会,因为上面这个题说到rdb和aof都有可能丢失数据。除非aof的实时处罚模式,但是这个模式很消耗性能,所以没人用
1.rbd:rdb在进行快照的过程中如果有新的指令产生,则新指令会丢失
2aof(每秒模式):如果突然服务崩了,那么崩之前的一秒内操作指令还没来得及存入aof文件就崩了。数据也就丢失了
redis的缓存雪崩-缓存穿透-缓存预热-缓存降级-缓存更新(redis的双写一致性问题)
这个题是面试redis的必问题,也是实际开发中实实在在用到的场景 下面我们来一个个的说明:
1.缓存雪崩
缓存雪崩指的是当大量的key有效期同一时刻失效后,导致所有的用户请求全部落到了DB上,进而导致DB崩溃,然后服务崩溃引发雪崩问题
解决方案有三种:
- 把key的有效期设置的分散一些(这是最简单的办法)
- 查询完redis之后,落在DB之前用锁控制一下
- 同2,不同的是用mq的队列形式缓解DB的压力
2.缓存穿透
缓存穿透指的是用户一直在请求一些redis没有的数据,因为redis没有缓存,所以会一直落在DB上。
另外:这缓存穿透和缓存雪崩这两个问题,说句实在话,一般中小型传统软件企业,很难碰到这个问题。如果有大并发的项目,流量有几百万左右。这两个问题一定要深刻考虑。
解决方案有量两种:
- 查询DB后发现没有数据时,就把null给缓存一下,下次在访问就直接正在缓存里给用户返回null。避免再次落在DB上
- 写一个拦截器,拦截器里去查询bitmap,如果bitmap里有则查询DB,没有则返回用户
3.缓存预热
缓存预热指的是提前把一些热点数据从DB里缓存到redis里,这样可以在活动开始后缓解DB的压力
一般做法是写个程序在活动开始前手动(或者定时自动)的把DB里的热点数据刷到redis里去。
4.缓存降级
缓存降级指的是redis扛不住目前的请求量了,需要把一些非核心的业务模块关闭,达到一个”弃卒保帅“的目的
可以运维人员手动降级,也可以程序自动降级。
5.缓存更新(redis的双写一致性问题)
缓存更新指的是修改DB后要在redis的缓存数据上同步更新一下
同步缓存分为两大类:
- 读请求:
(1):先读缓存,缓存有则返回数据,如果没有则查询DB,然后在更新redis并返回用户结果 - 更新请求
这里说明一下,无论是先删缓存然后操作DB,还是先操作DB在删除缓存,都会存在问题,前者情况有可能导致缓存和DB数据一直不一致,后者情况有可能导致暂时缓存和DB数据不一致。这也就是redis的双写一直性的问题。
(1):先删除redis缓存
(2):然后修改DB
(3):延迟1s后再删缓存,(这里延迟一秒可以启动一个线程也可以通过监控mysql的binlog实现)
以上做法就叫延迟双删。
redis的3种淘汰算法-6种淘汰策略
分析:这个问题其实相当重要,到底redis有没用到家,这个问题就可以看出来。比如你redis只能存5G数据,可是你写了10G,那会删5G的数据。怎么删的,这个问题思考过么?还有,你的数据已经设置了过期时间,但是时间到了,内存占用率还是比较高,有思考过原因么?
回答:
redis采用的是定期删除+惰性删除策略。
为什么不用定时删除策略?
定时删除,用一个定时器来负责监视key,过期则自动删除。虽然内存及时释放,但是十分消耗CPU资源。在大并发请求下,CPU要将时间应用在处理请求,而不是删除key,因此没有采用这一策略.
定期删除+惰性删除是如何工作的呢?
定期删除,redis默认每个100ms检查,是否有过期的key,有过期key则删除。需要说明的是,redis不是每个100ms将所有的key检查一次,而是随机抽取进行检查(如果每隔100ms,全部key进行检查,redis岂不是卡死)。因此,如果只采用定期删除策略,会导致很多key到时间没有删除。
于是,惰性删除派上用场。也就是说在你获取某个key的时候,redis会检查一下,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除。
采用定期删除+惰性删除就没其他问题了么?
不是的,如果定期删除没删除key。然后你也没即时去请求key,也就是说惰性删除也没生效。这样,redis的内存会越来越高。那么就应该采用内存淘汰机制。
在redis.conf中有一行配置
reids内存淘汰策略如下:
1)noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。应该没人用吧。
2)allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。推荐使用,目前项目在用这种。
3)allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。应该也没人用吧,你不删最少使用Key,去随机删。
4)volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。这种情况一般是把redis既当缓存,又做持久化存储的时候才用。不推荐
5)volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。依然不推荐
6)volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。不推荐
ps:如果没有设置 expire 的key, 不满足先决条件(prerequisites); 那么 volatile-lru, volatile-random 和 volatile-ttl 策略的行为, 和 noeviction(不删除) 基本上一致。
redis的三种集群模式和优缺点
redis支持三种集群模式,1.主从复制,2.哨兵模式,3.分槽(Cluster集群)模式 各有优缺点,下面进行说明:
1.主从复制:
和Mysql主从复制的原因一样,Redis虽然读取写入的速度都特别快,但是也会产生读压力特别大的情况。为了分担读压力,Redis支持主从复制,Redis的主从结构可以采用一主多从或者级联结构,Redis主从复制可以根据是否是全量分为全量同步和增量同步。下图为级联结构。
全量同步
Redis全量复制一般发生在Slave初始化阶段,这时Slave需要将Master上的所有数据都复制一份。具体步骤如下:
- 从服务器连接主服务器,发送SYNC命令;
- 主服务器接收到SYNC命名后,开始执行BGSAVE命令生成RDB文件并使用缓冲区记录此后执行的所有写命令;
- 主服务器BGSAVE执行完后,向所有从服务器发送快照文件,并在发送期间继续记录被执行的写命令;
- 从服务器收到快照文件后丢弃所有旧数据,载入收到的快照;
- 主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令;
- 从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令;
完成上面几个步骤后就完成了从服务器数据初始化的所有操作,从服务器此时可以接收来自用户的读请求。
增量同步
Redis增量复制是指Slave初始化后开始正常工作时主服务器发生的写操作同步到从服务器的过程。
增量复制的过程主要是主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令。
注意点
如果多个Slave断线了,需要重启的时候,因为只要Slave启动,就会发送sync请求和主机全量同步,当多个同时出现的时候,可能会导致Master IO剧增宕机。
优点:
- 配置简单,逻辑简单
缺点:
- 始终无法解决内存扩容问题
- master节点宕机后,无法自动热切换slave节点
2.哨兵模式:
在主从复制的基础上,出现了一个哨兵进程,它会监控各个节点状态,如果发现master节点投票后判定为宕机状态,则立刻热切换slave节点为master节点
优点:
- 配置简单,逻辑简单
缺点:
- 始终无法解决内存扩容问题
3.分槽(Cluster集群)模式:
A、采用去中心化的思想,没有中心节点的说法,它使用hash slot(哈希槽)方式将16348个hash slot覆盖到所有节点上,对于存储的每个key值,使用CRC16(KEY)&16348=slot得到他对应的hash slot,
并在访问key的时候就去找他的hash slot在哪一个节点上,然后由当前访问节点从实际被分配了这个hash slot的节点去取数据,节点之间使用轻量协议通信 减少带宽占用 性能很高,
自动实现负载均衡与高可用,自动实现failover并且支持动态扩展。
B、其内部中也需要配置主从,如果有半数节点发现某个异常节点,共同决定更改异常节点的状态,如果改节点是主节点,则对应的从节点自动顶替为主节点,当原先的主节点上线后,则会变为从节点。
(详细的节点选举过程:某个master的slave发现自己的master挂掉了,就会发起投票,集群里的所有节点参与投票,票数过半则提升为master。原master标记为slave并且开始复制新master数据,如果2个slave票数相同,则发起第二轮投票,方式同上)
如果集群中的master没有slave节点,则master挂掉后整个集群就会进入fail状态,因为集群的slot映射不完整。如果集群超过半数以上的master挂掉,无论是否有slave,集群都会进入fail状态。
C、根据官方推荐 集群部署至少要3台以上的master节点。
redis的类型-常用的场景-常用的方法和场景
- String:
setnx:分布式锁
incr-decr:计数器,限流等场景使用 - hash:Session共享(sso单点登录)等
- list:队列场景等
- set:做一些数据的去重,求交集并集差集等等
- zset:排行榜等
- bitmap:位图,可以用于解决缓存穿透等场景
如何解决redis的并发竞争key问题
由于单线程所以Redis本身并没有锁的概念,多个客户端连接并不存在竞争关系,但是生产环境中如果多个连接同时操作一个key,都是先get在set就会导致并发问题,除非用incr或者decr就没问题,所以咱们要解决的是批量语句原子性问题。
分析:这个问题大致就是,同时有多个子系统去set一个key。这个时候要注意什么呢?大家思考过么。需要说明一下,博主提前百度了一下,发现答案基本都是推荐用redis事务机制。博主不推荐使用redis的事务机制。因为我们的生产环境,基本都是redis集群环境,做了数据分片操作。你一个事务中有涉及到多个key操作的时候,这多个key不一定都存储在同一个redis-server上。因此,redis的事务机制,十分鸡肋。
回答:如下所示
(1)如果对这个key操作,不要求顺序
redis实现分布式锁,上文说过
(2)如果对这个key操作,要求顺序
zookeeper实现分布式锁
(3)或者用乐观锁的CAS思路,设置一个时间戳,判断是自己跟目前缓存里的时间戳哪个最新就用哪个的数据
(4)另外就是利用队列,将set方法变成串行访问也可以。
(5)利用lua脚本来实现原子性也可以。