Redis是一个使用ANSI C编写的开源、包含多种数据结构、支持网络、基于内存、可选持久性的键值对存储数据库。也是当下互联网首选的一款高性能nosql数据库。
随着市面上使用的人越来越多,企业在招聘过程中对人才的选拔也越来越高,大多数开发者可能只是停留在使用状态,缺少对底层原理的了解。本文就针对一些常见的大厂面试题做一个汇总。
Redis使用的场景有哪些
- 数据缓存(用户信息、商品数量、文章阅读数量)
- 消息推送(站点的订阅)
- 队列(削峰、解耦、异步)
- 排行榜(积分排行)
- 社交网络(共同好友、互踩、下拉刷新)
- 计数器(商品库存,站点在线人数、文章阅读、点赞)
- 基数计算
- GEO计算
Redis功能特点都有哪些
- 持久化
- 丰富的数据类型(string、list、hash、set、zset、发布订阅等)
- 高可用方案(哨兵、集群、主从)
- 事务
- 丰富的客户端
- 提供事务
- 消息发布订阅
- Geo
- HyperLogLog
- 事务
- 分布式事务锁
Redis的数据类型都有哪些
- 有五种基本数据类型,分别是string、hash、list、有序集合(zset)、集合(set)。在5.0之后增加了一种Stream类型。
- 额外的有GEO、HyperLogLog、BitMap。
Redis如何实现分布式锁
- Redis可以使用
setnx key value
expire key expire_time
来实现分布式锁。 - 正常情况下,上面的命令是没有问题的。当Redis出现异常的情况下,很容易出现非原子性操作。
- 非原子性操作指的的setnx命令执行成功,但是expire没有执行成功,此时key就成为了一个无过期时间的key,一直保留在Redis中,导致其他的请求就无法执行。
- 要解决该问题,可以使用lua脚本实现。通过lua实现命令的原子性操作。
在Redis中使用set命令,加参数也可以实现分布式锁。
set key vale nx ex|px ttl
。tips:如果对一个key第一次set添加了过期时间,第二次操作时没有添加过期时间,此时key是没有过期时间的(过期时间被覆盖为永久不过期)。
Redis底层数据结构有哪些
Redis底层数据结构主要有六种,这六种构成了五种常用的数据类型。其他的数据类型,例如bitmap、hyperLogLog也是基于这五大数据类型实现。具体的数据结构图如下:
说说Redis的全局Hash表
为了实现从键到值的快速访问,Redis 使用了一个哈希表来保存所有键值对。结构图如下:
Hash表应用如此广泛的一个重要原因,就是从理论上来说,它能以 O(1) 的复杂度快速查询数据。Hash 表通过Hash函数的计算,就能定位数据在表中的位置,紧接着可以对数据进行操作,这就使得数据操作非常快速。那么我们该如何解决哈希冲突呢?可以考虑使用以下两种解决方案:
- 第一种方案,就是使用链式哈希。但是链式哈希容易导致Hash的链过长,查询效率降低。
- 第二种方案,就是当链式哈希的链长达到一定长度时,我们可以使用rehash。不过,执行rehash本身开销比较大。
del删除大量key有什么问题
- 使用del命令可以删除一个key或者多个key,其时间复杂度为O(N),这里的N表示删除的key数量。
- 删除单个key时,其时间复杂度为O(1)。
- 当删除单个列表、集合、有序集合或者哈希列表类型的key时,时间复杂度为O(M),这里的M表示key对应的内部元素个数。
说说Redis的全局hash实现原理
说说Zset在skiplist和ziplist实现原理
Redis事务都有哪些命令
mutil: 开启事务;exec: 提交事务;discard: 回滚事务。watch: 监听key;unwatch: 取消监听key。
Redis中的事务是否是原子性
严格来说,Redis中的事务并非满足事务的原子性操作。当事务在命令组队时没有发生错误,则事务是原子性;当事务在命令组队时发生错误,则事务是非原子性的。
Redis如何解决事务之间的冲突
- 使用watch监听key变化,当key发生变化,事务中的所有操作都会被取消。
- 使用乐观锁,通过版本号实现。
- 使用悲观锁,每次开启事务时,都添加一个锁,事务执行结束之后释放锁。
悲观锁:悲观锁(Pessimistic Lock),顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人拿到这个数据就会block(阻塞)直到它拿到锁。传统的关系型数据库里面 就用到了很多这种锁机制,比如行锁、表锁、读锁、写锁等,都是在做操作之前先上锁。
乐观锁:乐观锁(Optimistic Lock),顾名思义,就是很乐观,每次去那数据的时候都认为别人不会修改,所以 不会上锁,但是在修改的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机 制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。redis就是使用这种check-and-set机制实现 事务的。
事务中的watch有什么用
在执行multi之前,先执行watch key1 [key2 ...],可以监视一个或者多个key。若在事务的exec命令之前,这些key对应的值被其他命令所改动了,那么事务中所有命令都将被打断,即事务所有操作将被取消执行。
Redis事务的三大特性
- 事务中的所有命令都会序列化、按顺序地执行,事务在执行过程中,不会被其他客户端发送来的命令请求所打断。
- 队列中的命令没有提交(exec)之前,都不会实际被执行,因为事务提交前任何指令都不会被实际执行。
- 事务中如果有一条命令执行失败,后续的命令仍然会被执行,没有回滚。如果在组队阶段,有1个失败了,后面都不会成功;如果在组队阶段成功了,在执行阶段有那个命令失败 就这条失败,其他的命令则正常执行,不保证都成功或都失败。
如何使用Redis实现队列功能
- 可以使用list实现普通队列,lpush添加到嘟列,lpop从队列中读取数据。
- 可以使用zset定期轮询数据,实现延迟队列。
- 可以使用发布订阅实现多个消费者队列。
- 可以使用stream实现队列。(推荐使用该方式实现)。
如何用Redis实现异步队列
- 一般使用list结构作为队列,rpush生产消息,lpop消费消息。当lpop没有消息的时候,要适当sleep一会再重试。
- 如果对方追问可不可以不用sleep呢?list还有个指令叫blpop,在没有消息的时候,它会阻塞住直到消息到来。
- 如果对方追问能不能生产一次消费多次呢?使用pub/sub主题订阅者模式,可以实现1:N的消息队列。
- 如果对方追问pub/sub有什么缺点?在消费者下线的情况下,生产的消息会丢失,可以使用Redis6增加的stream数据类型,也可以使用专业的消息队列如rabbitmq等。
- 如果对方追问redis如何实现延时队列?使用sortedset,拿时间戳作为score,消息内容作为key调用zadd来生产消息,消费者用zrangebyscore指令获取N秒之前的数据轮询进行处理。
Stream与list、zset和发布订阅区别
- list可以使用lpush向队列中添加数据,lpop可以向队列中读取数据。list作为消息队列无法实现一个消息多个消费者。如果出现消息处理失败,需要手动回滚消息。
- zset在添加数据时,需要添加一个分值,可以根据该分值对数据进行排序,实现延迟消息队列的功能。消息是否消费需要额外的处理。
- 发布订阅可以实现多个消费者功能,但是发布订阅无法实现数据持久化,容易导致数据丢失。并且开启一个订阅者无法获取到之前的数据。
- stream借鉴了常用的MQ服务,添加一个消息就会产生一个消息ID,每一个消息ID下可以对应多个消费组,每一个消费组下可以对应多个消费者。可以实现多个消费者功能,同时支持ack机制,减少数据的丢失情况。也是支持数据值持久化和主从复制功能。
如何设计一个网站每日、每月和每天的PV和UV
实现这样的功能,如果只是统计一个汇总数据,推荐使用HyperLogLog数据类型。Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定 的、并且是很小的。在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基 数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。
Redis如何实现距离检索功能
实现距离检索,可以使用Redis中的GEO数据类型。GEO 主要用于存储地理位置信息,并对存储的信息进行操作,该功能在 Redis 3.2 版本新增。但是GEO适合精度不是很高的场景。由于GEO是在内存中进行计算,具备计算速度快的特点。
list和发布订阅实现队列有什么问题
- list可以使用lpush向队列中添加数据,lpop可以向队列中读取数据。list作为消息队列无法实现一个消息多个消费者。如果出现消息处理失败,需要手动回滚消息。
- 发布订阅可以实现多个消费者功能,但是发布订阅无法实现数据持久化,容易导致数据丢失。并且开启一个订阅者无法获取到之前的数据。
Redis如何实现秒杀功能
- 在秒杀场景下,超卖是一个非常严重的问题。常规的逻辑是先查询库存在减少库存。但在秒杀场景中,无法保证减少库存的过程中有其他的请求读取了未减少的库存数据。
- 由于Redis是单线程的执行,同一时刻只有一个线程进行操作。因此可以使用Redis来实现秒杀减少库存。
- 在Redis的数据类型中,可以使用lpush,decr命令实现秒杀减少库存。该命令属于原子操作。
Redis如何实现用户签到功能
- 使用Redis实现用户签到可以使用bitmap实现。bitmap底层数据存储的是1否者0,占用内存小。
- Redis提供的数据类型BitMap(位图),每个bit位对应0和1两个状态。虽然内部还是采用String类型存储,但Redis提供了一些指令用于直接操作BitMap,可以把它看作一个bit数组,数组的下标就是偏移量。
- 它的优点是内存开销小,效率高且操作简单,很适合用于签到这类场景。
- 缺点在于位计算和位表示数值的局限。如果要用位来做业务数据记录,就不要在意value的值。
Redis如何实现延迟队列
- 使用Redis实现延迟队列,可以使用zset数据类型。
- zset在添加数据时,需要添加一个分值,将时间作为分值,根据该分值对数据进行排序。
- 单独开启线程,根据分值大小定期实行数据。
Redis实现一个积分排行功能
- 使用Redis实现积分排行,可以使用zset数据类型。
- zset在添加数据时,需要添加一个分值,将积分作为分值,值作为用户ID,根据该分值对数据进行排序。
字符串类型存储最大容量是多少
一个字符串最大可存储512M。
哨兵的作用是什么
Redis主从复制的模式下,能够提高系统的并发能力。但是存在一个问题,当写入数据的节点不能正常工作的情况下,整个系统只能读而不能写。除非人为的对主节点进行恢复。但是人为操作存在不及时、响应慢、误操作等等问题。哨兵机制基于该现象,通过创建多个非数据节点来监控主从节点的运行状态,当其中的主节点宕机或者不能正常停止工作时,能够自动的从从节点中选择主节点进行数据的写入操作。
哨兵选择哨兵领导者的原理是什么
假如Sentinel节点对于主节点已经做了客观下线,那么是不是就可以立即进行故障转移了?当然不是,实际上故障转移的工作只需要一个Sentinel节点来完成即可,所以Sentinel节点之间会做一个领导者选举的工作,选出一个Sentinel节点作为领导者进行故障转移的工作。Redis使用了Raft算法实现领导者选举,因为Raft算法相对比较抽象和复杂,以及篇幅所限,所以这里给出一个Redis Sentinel进行领导者选举的大致思路:
- 每个在线的Sentinel节点都有资格成为领导者,当它确认主节点主观下线时候,会向其他Sentinel节点发送sentinel is-master-down-by-addr命令,要求将自己设置为领导者。
- 收到命令的Sentinel节点,如果没有同意过其他Sentinel节点的sentinel is-master-down-by-addr命令,将同意该请求,否则拒绝。
- 如果该Sentinel节点发现自己的票数已经大于等于max(quorum,num(sentinels)/2 1),那么它将成为领导者。
如果此过程没有选举出领导者,将进入下一次选举。
哨兵机制主从选举的判断依据
- 从节点集群中选择slave-priority配置最小的值那台从节点作为主节点。如果设置的是0,表示该节点不能切换为主节点。
- 如果不存在slave-priority配置,这选择从节点集群中同步数据最多,偏移量最大的从节点作为主节点
- 如果还不存在,这选择runid最小的值作为主节点(即最新启动的从节点)
哨兵模式的工作机制
- 每个Sentinel以每秒钟一次的频率向它所知的Master,Slave以及其他 Sentinel 实例发送一个 PING 命令。
- 如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值, 则这个实例会被当前 Sentinel 标记为主观下线。
- 如果一个Master被标记为主观下线,则正在监视这个Master的所有 Sentinel 要以每秒一次的频率确认Master的确进入了主观下线状态。
- 当有足够数量的 Sentinel(大于等于配置文件指定的值)在指定的时间范围内确认Master的确进入了主观下线状态, 则Master会被标记为客观下线 。
- 当Master被 Sentinel 标记为客观下线时,Sentinel 向下线的 Master 的所有 Slave 发送 INFO 命令的频率会从 10 秒一次改为每秒一次 (在一般情况下, 每个 Sentinel 会以每 10 秒一次的频率向它已知的所有Master,Slave发送 INFO 命令 )。
- 若没有足够数量的 Sentinel 同意 Master 已经下线, Master 的客观下线状态就会变成主观下线。若 Master 重新向 Sentinel 的 PING 命令返回有效回复, Master 的主观下线状态就会被移除。
- sentinel节点会与其他sentinel节点进行“沟通”,投票选举一个sentinel节点进行故障处理,在从节点中选取一个主节点,其他从节点挂载到新的主节点上自动复制新主节点的数据。
说说Redis的同步机制
主从同步。第一次同步时,主节点做一次bgsave,并同时将后续修改操作记录到内存buffer,待完成后将rdb文件全量同步到复制节点,复制节点接受完成后将rdb镜像加载到内存。加载完成后,再通知主节点将期间修改的操作记录同步到复制节点进行重放就完成了同步过程。
主从同步时间不一致问题
我们假设,slave 的机器时钟比 master 走得快很多。此时,Redis master里设置了过期时间的key,从 slave 角度来看,可能会有很多在master 里没过期的数据其实已经过期了。如果此时操作主从切换,把 slave 提升为新的 master。它成为 master 后,就会开始大量清理过期 key,此时就会导致以下结果:
- master 大量清理过期 key,主线程可能会发生阻塞,无法及时处理客户端请求。
- Redis 中数据大量过期,引发缓存雪崩。
当 master与slave 机器时钟严重不一致时,对业务的影响非常大。所以,我们一定要保证主从库的机器时钟一致性,避免发生这些问题。
谈谈你对Redis脑裂的理解
什么是脑裂
所谓的脑裂,就是指在主从集群中,同时有两个主节点,它们都能接收写请求。而脑裂最直接的影响,就是客户端不知道应该往哪个主节点写入数据,结果就是不同的客户端会往不同的主节点上写入数据。而且,严重的话,脑裂会进一步导致数据丢失。
脑裂发生原因
确认是不是数据同步出现了问题。
在主从集群中发生数据丢失,最常见的原因就是主库的数据还没有同步到从库,结果主库发生了故障,等从库升级为主库后,未同步的数据就丢失了。如果是这种情况的数据丢失,我们可以通过比对主从库上的复制进度差值来进行判断,也就是计算master_repl_offset
和slave_repl_offset
的差值。如果从库上的slave_repl_offset
小于原主库的 master_repl_offset
,那么,我们就可以认定数据丢失是由数据同步未完成导致的。
排查客户端的操作日志,发现脑裂现象
在排查客户端的操作日志时,我们发现,在主从切换后的一段时间内,有一个客户端仍然在和原主库通信,并没有和升级的新主库进行交互。这就相当于主从集群中同时有了两个主库。根据这个迹象,我们就想到了在分布式主从集群发生故障时会出现的一个问题:脑裂。但是,不同客户端给两个主库发送数据写操作,按道理来说,只会导致新数据会分布在不同的主库上,并不会造成数据丢失。那么,为什么我们的数据仍然丢失了呢?
发现是原主库假故障导致的脑裂。
我们是采用哨兵机制进行主从切换的,当主从切换发生时,一定是有超过预设数量(quorum配置项)的哨兵实例和主库的心跳都超时了,才会把主库判断为客观下线,然后,哨兵开始执行切换操作。哨兵切换完成后,客户端会和新主库进行通信,发送请求操作。
但是,在切换过程中,既然客户端仍然和原主库通信,这就表明,原主库并没有真的发生故障(例如主库进程挂掉)。
脑裂导致数据丢失
主从切换后,从库一旦升级为新主库,哨兵就会让原主库执行 slave of
命令,和新主库重新进行全量同步。而在全量同步执行的最后阶段,原主库需要清空本地的数据,加载新主库发送的 RDB 文件,这样一来,原主库在主从切换期间保存的新写数据就丢失了。
解决方案
Redis 已经提供了两个配置项来限制主库的请求处理,分别是min-slaves-to-write
和min-slaves-max-lag
。
min-slaves-to-write
:这个配置项设置了主库能进行数据同步的最少从库数量;
min-slaves-max-lag
:这个配置项设置了主从库间进行数据复制时,从库给主库发送ACK消息的最大延迟(以秒为单位)。
我们可以把min-slaves-to-write
和min-slaves-max-lag
这两个配置项搭配起来使用,分别给它们设置一定的阈值,假设为 N 和 T。这两个配置项组合后的要求是,主库连接的从库中至少有N个从库,和主库进行数据复制时的ACK消息延迟不能超过T秒,否则,主库就不会再接收客户端的请求了。
即使原主库是假故障,它在假故障期间也无法响应哨兵心跳,也不能和从库进行同步,自然也就无法和从库进行ACK确认了。这样一来,min-slaves-to-write
和min-slaves-max-lag
的组合要求就无法得到满足,原主库就会被限制接收客户端请求,客户端也就不能在原主库中写入新数据了。