Redis面试题
-
Redis的持久化机制
持久化就是把内存的数据写到磁盘中去,防止服务宕机了内存数据丢失。
Redis提供了两种持久化方式:RDB和AOF,RDB是基于快照的方式保存当前系统状态,操作速度快,适用于灾难备份。而AOF是基于记日志的方式,将引起key变化的语句记录下来,类似于MySQL的Binlog日志。因为我们生产中使用了Redis的cluster集群,所以我们会把关闭集群中的master服务器的持久化,让master机有最佳的读写性能,然后开启slave服务器的RDB持久化模式。
Redis很少采用读写分离架构,因为Redis本身是在内存操作,不涉及IO吞吐,即使读写分离也不会提升太多性能,而且如果使用了还需要考虑主从同步的延迟性问题,徒增系统复杂度。
事实上,因为有了主从复制,数据已经有了备份,之所以还需要持久化更多的是当Redis服务器挂了能够在重启之后马上恢复到原来的状态。
-
缓存雪崩、缓存穿透、缓存击穿、服务降级
缓存雪崩就是对大量的key因为设置了相同过期时间而大面积的缓存过期,导致原本访问缓存的请求直接去查询数据库,导致数据库压力过大而奔溃。
应对方法:可以在key原设置的过期时间上加一个随机值来预防这种情况。或者是通过加锁或者是队列的方式保证不会有大量的线程在同时直接访问数据库。
缓存穿透是用户查询一个在数据库和Redis中都不存在的一个数据,经过两次查询后返回空,如果在大量并发请求下会导致底层数据库异常。
应对方法有两种:一种是在第一次查询为空时将这个空结果缓存起来,并设置一定的过期时间,这样之后的查询就不会继续去访问数据库了。但这种方法会造成Redis的空间浪费。所以更常用的第二种方法,就是使用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
(Bloom-Filter算法的核心思想就是利用多个不同的Hash函数来解决“冲突”。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。
多引入几个Hash,如果通过其中的一个Hash值我们得出某元素不在集合中,那么该元素肯定不在集合中。只有在所有的Hash函数告诉我们该元素在集合中时,才能确定该元素存在于集合中。
Bloom-Filter一般用于在大数据量的集合中判定某元素是否存在。)
缓存击穿是一个或某几个key快要到过期时,大量并发请求过来时会绕过缓存直接查询数据库,使得数据库压力过大而导致异常。
应对方法:如果某些key使用频率过高,可以使用链表等结构对其进行二级缓存。
服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户。
-
Redis优于本地缓存的原因?
Python中可以自己通过内置的字典来实现一个缓存的功能,并且用LRU(近期最少使用)算法来实现缓存淘汰。
虽然都是使用内存来存取数据,但Redis的缓存可以持久化,而且可以实现分布式,这是本地缓存做不到的。另外Redis的性能极佳,每秒可以处理百万级别的并发。
-
单线程的Redis为什么这么快
-
纯内存操作
-
单线程操作,避免了频繁的上下文切换(可以确定的是不会有两条命令被同时执行,不会产生并发问题)
-
采用了非阻塞I/O多路复用机制(一个服务端可以通过Socket连接多个客户端)
补充:Redis客户端在进行操作时,会产生具有不同事件类型的socket,在服务端,有一个IO多路复用程序会将这些socket存放在一个队列中,然后服务端中另一个叫文件事件分派器,会依次去队列里面取,转发到相应的事件处理器中。然后事件处理器就会根据socket关联的事件进行相应的读取或写入或连接应答等操作。
-
Redis的线程模型
Redis基于Reactor模式开发了网络事件处理器,这个处理器被称为文件事件处理器(file event handler)。它的组成结构为4部分:多个套接字、IO多路复用程序、文件事件分派器、事件处理器。因为文件事件分派器队列的消费是单线程的,所以Redis才叫单线程模型。过程就如上面的补充。
-
Redis的过期策略 / 过期键的删除策略
Redis如果一个键设了过期时间,那么Redis会在expires区域里面记录这个key的内存地址以及到期时间倒数。
Redis采用的是定期删除+惰性删除策略。
还有一种过期策略是定时删除,就是用一个定时器来负责监视key,过期则自动删除。但是这种策略十分消耗CPU资源,所以Redis没有采用。
所谓的定期删除是指Redis默认每100ms会对Expires区域里面进行检查,随机抽取区域里面的key,如果过期了则把key删除。因为是随机抽查,所以会出现部分key到时间了却没有被删除的情况,所以会混合使用惰性删除,即如果我们在读取某个key时会去检查这个key是否过期,如果没有过期就返回查询结果,如果过期了则把这个key真正的删除。
-
内存淘汰策略 / MySQL有2000w数据,Redis只能存20w数据,如何保证Redis存的都是热点数据?
Redis有六种内存淘汰策略,我们可以在Redis的配置文件中配置参数“maxmemory-policy xxxx”来达到我们想要的结果。在内存剩余量不足以存放新的key写入时调用。
具体的淘汰策略有:
allkeys-lru:移除键空间里最近最少使用的key(大多时候的选择)
allkeys-random:随机移除键空间中某些key
volatile-lru:在设置了过期时间的键空间中,移除最近最少使用的key
volitile-random:在设置了过期时间的键空间中,随机移除某些key
volitile-ttl:在设置了过期时间的键空间中,有更早过期时间的key优先删除
noeviction:当空间不足是直接报错。
-
Redis和数据库如何确保双写一致性问题
一致性问题是分布式常见问题,还可以再分为最终一致性和强一致性。而如果对数据有强一致性的要求,则不能放缓存。我们能够做到的时保证最终一致性。
在更新数据库的数据时,我们需要先把数据库中的数据进行修改,修改成功后将之前存于缓存中的数据删除。如果考虑上删除缓存数据的操作可能会失败的情况,那么就需要提供一个补偿措施,例如利用消息队列。
这上面的具体做法是成功执行完数据库的更新数据操作后,会产生相应的binlog日志,MySQL有一个中间件叫canal的有订阅binlog日志的功能,提取出相应的数据以及key,去执行删除缓存操作,如果失败了就将操作放入消息队列MQ,让MQ重试删除操作。
- 先改缓存再改数据库会出现,改完缓存,机器挂了,这时候缓存存的值是脏数据。
- 先改数据库再改缓存会出现,A更新数据库,之后B更新数据库,B更新缓存,A再更新缓存的情况,此时缓存的值是脏数据。
- 先删缓存再改数据库会出现,A删缓存然后准备改数据库,同时B查数据,查不到缓存读到了数据库的旧数据,然后把旧数据写入缓存,之后A在数据库改完了数据,此时缓存中的数据与数据库的数据就不一致了。这种情况的补救方法就是延时双删,也就是A改完数据过一段时间再通过其他程序把缓存给删了。
-
如何解决Redis的并发竞争key的问题 / 多个客户端去同时set一个key需要注意什么问题
如果对这个key操作不要求顺序的话,可以加一个分布式锁,获得锁之后才能对key进行set操作。
如果对这个key操作要求顺序的话,可以利用队列,将set方法变成串行访问。或者是设置分布式锁+时间戳,如果操作时间早于对应的时间戳则放弃set操作。
-
Redis的哨兵机制
Redis的哨兵作用是监控Redis中的主从服务器是否正常工作,如果主服务器发生故障则通过内部投票的机制选取某一个从服务器,将其转变成主服务器,保证Redis的高可用性。不过更多的是使用Cluster集群来管理Redis。
-
使用Redis的Cluster集群的好处
- 分散单台服务器的访问压力,实现负载均衡
- 分散单台服务器的存储压力,可方便的进行水平扩展,实现可扩展性
- 降低单台服务器宕机带来的业务灾难
- 无需Sentinel哨兵监控,如果master挂了,Redis Cluster内部自动将Slave切换为Master
注:如果Django需要使用Redis的Cluster集群需要装Django-Redis包以及Django-Cluster-Redis包。
注:集群会有写操作丢失的可能性
-
Redis的Cluster集群工作原理。
Redis的Cluster集群引入了哈希槽的概念, 默认设置16384 个槽位,集群中每个节点均分存储一定哈希槽区间的数据,假设集群中的A节点存储的哈希槽区间为0到5300,现在当我们要存储数据时,会先对数据进行CRC16算法哈希,计算出来的值再对16384 ,也就是总槽位取模,这样得到的值,例如是3000,那么就将这个数据存在A节点上。
而读数据的话,如果我们客户端操作的key没有分配在该节点上时,redis会返回转向指令,指向正确的节点。这个就是查询路由。
-
集群之间的节点如何通信?
在Redis Cluster架构下,每个Redis要开放两个端口号,一个用来对外交流,一个是节点间的相互通信。节点间利用的是一种叫gossip二进制协议(占用带宽小,处理时间快),用来定期进行故障检测、配置更新、故障转移等数据交换。实际表现是每个节点会定期向其他节点发送ping消息作为心跳包,这个包里面存放了本节点在总槽位的占比状况,如果总槽位设置为65536,则这个包的消息头过大,浪费带宽。实际上总槽位设为16384就足够面对绝大部分集群配置需求。
-
Redis事务
Redis事务实际上是执行一系列的Redis命令集合,可以通过Multi开启和Exec执行。不过因为在生产中使用了Cluster集群,不同的key可能会被分配到不同的Redis节点上,在这种情况下Redis的事务机制是不生效的,所以我们没有用事务。
-
Cluster集群的缺点
-
进行批量操作比较麻烦,不能直接使用mset和mget。如果执行的key数量不多,那么就直接用串行get操作;如果需要执行的key数量较多,则使用同样的 Hashtag保证这些key映射到同一台Redis节点上。
-
如果一个key的value是hash类型的,而这个hash对象又非常大,是不支持映射到不同节点上的。
-
如何使用Redis实现分布式锁
Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系Redis中可以使用SETNX命令实现分布式锁。(这是前提)
可以利用Redis的SETNX命令来实现分布式锁的功能,这个命令会去检查设置的键是否存在,如果不存在则创建这个键值对,如果存在则放弃执行。
在设置键的同时加上键的过期时间,保证操作原子性的同时,也可以防止出现客户端挂了而没有解锁而造成死锁的情况发生。
这个键的值可以使用一个随机值,这个值在每个客户端都是唯一的,然后解锁的话可以通过lua脚本判断删掉的值是否与指定的值一样,如果一样则可以删除这个键。
这种分布式锁实际上在高并发下不是绝对安全的,所以真正要实现分布式锁则需要用zookeeper。
下面就有这么一个例子就是讲如何不安全的。
在这种场景(主从结构)中存在明显的竞态: 1. 客户端A从master获取到锁 2. 在master将锁同步到slave之前,master宕掉了。 3. slave节点被晋级为master节点 4. 客户端B取得了同一个资源被客户端A已经获取到的另外一个锁。安全失效!
-
生产环境中的 redis 是怎么部署的?
redis cluster,10 台机器,5 台机器部署了 redis 主实例,另外 5 台机器部署了 redis 的从实例,每个主实例挂了一个从实例,5 个节点对外提供读写服务,每个节点的读写高峰qps可能可以达到每秒 5 万,5 台机器最多是 25 万读写请求/s。
机器是什么配置?32G 内存+ 8 核 CPU + 1T 磁盘,但是分配给 redis 进程的是10g内存,一般线上生产环境,redis 的内存尽量不要超过 10g,超过 10g 可能会有问题。
5 台机器对外提供读写,一共有 50g 内存。
因为每个主实例都挂了一个从实例,所以是高可用的,任何一个主实例宕机,都会自动故障迁移,redis 从实例会自动变成主实例继续提供读写服务。
你往内存里写的是什么数据?每条数据的大小是多少?商品数据,每条数据是 10kb。100 条数据是 1mb,10 万条数据是 1g。常驻内存的是 200 万条商品数据,占用内存是 20g,仅仅不到总内存的 50%。目前高峰期每秒就是 3500 左右的请求量。
-
Redis常见性能问题和解决方案?
- Master最好不要做任何持久化工作,包括内存快照和AOF日志文件,特别是不要启用内存快照做持久化。
- 如果数据比较关键,某个Slave开启AOF备份数据,策略为每秒同步一次。
- 为了主从复制的速度和连接的稳定性,Slave和Master最好在同一个局域网内。
- 尽量避免在压力较大的主库上增加从库
- Master调用BGREWRITEAOF重写AOF文件,AOF在重写的时候会占大量的CPU和内存资源,导致服务load过高,出现短暂服务暂停现象。
- 为了Master的稳定性,主从复制不要用图状结构,用单向链表结构更稳定,即主从关系为:Master<–Slave1<–Slave2<–Slave3…,这样的结构也方便解决单点故障问题,实现Slave对Master的替换,也即,如果Master挂了,可以立马启用Slave1做Master,其他不变。
-
假如Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,如果将它们全部找出来?
可以用keys指令找出指定模式的key列表,不过这个操作会阻塞其他的正常操作。所以,我们还可以使用Scan指令,它可以无阻塞的提取出指定模式的key列表,不过会有一定概率出现重复数据,而且消耗的时间也比keys指令多。所以看是在什么情况下进行key的查找了。
-
使用Redis做过异步队列吗,是如何实现的?
使用list类型保存数据信息,rpush生产消息,lpop消费消息,当lpop没有消息时,可以sleep一段时间,然后再检查有没有信息,如果不想sleep的话,可以使用blpop, 在没有信息的时候,会一直阻塞,直到信息的到来。redis可以通过pub/sub主题订阅模式实现一个生产者,多个消费者,当然也存在一定的缺点,当消费者下线时,生产的消息会丢失。
-
Redis如何实现延时队列?
使用sortedset,使用时间戳做score, 消息内容作为key,调用zadd来生产消息,消费者使用zrangbyscore获取n秒之前的数据做轮询处理。