面试-Redis知识点整理
Redis概述
什么是Redis?优缺点?
Redis是Key-Value内存型数据库
优点:
- 读写性能极高。Redis是内存数据库,相比较于去访问磁盘,访问内存的速度快很多
- 支持数据持久化
- 支持事务
- 支持的数据类型丰富
- 提供原生集群支持
缺点:
- 数据库容量受到物理内存的限制。
- 数据不一致问题
Redis为什么快?(Redis是单线程为什么快?)
- 基于内存,读写速度快
- 单线程没有上下文切换带来的消耗
- 非阻塞IO多路复用模型
Redis为什么用单线程?
- 不用考虑锁的问题
- 没有上下文切换带来的消耗
- 处理逻辑简单
- CPU不是性能瓶颈
Redis线程模型
Redis线程模型包含4个部分:
- 多个socket
- IO多路复用程序
- 文件事件分派器
- 文件事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)
文件事件处理器使用IO多路复用机制同时监听多个socket,根据socket上的事件来选择对应的事件处理器进行处理
IO多路复用如何保证读写的顺序?
多个套接字产生的事件,由IO多路复用程序将它们放到一个队列中,然后通过这个队列,有序、同步地传递给文件事件分派器
4.0和6.0引入的多线程
4.0:
引入多线程处理异步任务,包括命令如UNLINK、FLUSHALL ASYNC、FLUSHDB ASYNC。
6.0:
引入多线程IO,处理网络数据的读写和协议解析,而执行命令依旧是单线程
为什么用Redis做缓存而不是用Map?
用Map实现的缓存,生命周期随JVM的销毁而结束,而且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。
使用Redis的称为分布式缓存,在多实例的情况下,各实例共用一份缓存,缓存具有一致性。缺点是需要保持Redis的高可用性,整个架构比较复杂
Redis和Memcached的区别
- Redis支持的数据类型更多
- Redis支持数据的持久化,可以将内存中的数据保存到磁盘上,重启的时候可以再次加载,而Memcached把数据全部存放在内存中
- Redis原生支持集群
- Redis使用单线程的IO多路复用模型,Memcached是多线程的非阻塞IO复用模型
Redis常用的数据结构和使用场景
String
- 命令:
set, get, incr, decr, mget - 底层实现:int、embstr、raw(即SDS)
- 应用场景:常规key-value缓存应用;计数:微博数、粉丝数
List
- 命令:
lpush, lpop, rpush, rpop, lrang - 底层实现:ziplist、linkedlist
- 应用场景:微博的关注列表,粉丝列表,消息列表
Hash
-
命令:
hset, hget, hgetall -
底层实现:ziplist、hashtable
-
应用场景:存储用户信息,商品信息
-
渐进式rehash的优缺点:
优点:rehash操作分摊到对字典的增删改查操作上,避免了集中式rehash带来的庞大计算量,避免了Redis阻塞
缺点:rehash过程中有两张表同时在使用,增加了占用内存
Set
- 命令:
sadd, spop, smembers, scard - 底层实现:intset、hashtable
- 应用场景:共同关注、共同粉丝、共同爱好等
SortedSet(zset)
-
命令:
zadd, zrange, zcard, zrank, zscore -
底层实现:ziplist、skiplist(包含一个跳表和一个字典)
-
应用场景:直播间的在线用户列表,礼物排行版
-
补充
-
什么是跳表?
跳表的插入、删除、查找的平均时间复杂度是O(logN)。它实际上是有序的链表,只是它为每个节点维护多个指向其他节点的指针,每个节点的指针数量可以理解为层数,每层是一个指针,指针会链接同一层的下一个节点,层数是随机出来的,越高的层数随机的概率越低,也就是说同一层中,层数越高节点数量就越少。查找的时候从最高层开始,找到最后一个小于当前元素的位置,跳到下一层,每层可以跳过部分节点,从而实现快速的查找
-
跳表的其他应用:ConcurrentSkipListMap(对数据没有强一致性要求的高并发场景)、ElasticSearch的倒排索引(对于英文单词,可以通过跳表快速查询)
-
实现:面试题之跳表
-
底层
- SDS:free + len + buf。String的长度大于39字节时使用
- embstr:free + len + buf。和SDS的不同是buf不是指针而是一块连续的空间。对embstr做修改后会变成SDS
- ziplist:zlbytes+zltail+zllen+节点+zlend,zlbytes表示整个列表占用的字节数,zltail表示尾节点距离起始地址的字节数,zllen表示节点数量,zlend表示末端(0xFF特殊值)。节点结构为:前一个节点的长度+编码+实际数据
- linkedlist:head+tail+len
- hashtable:size+mask+used+table
- intset:encoding(int或者long)+len+整数数组
Redis过期键删除策略
惰性删除+定期删除
- 惰性删除策略:所有的读写操作都会对键检查,如果键已失效被删除或者键不存在,则执行键不存在的操作;否则执行键存在的操作
- 定期删除策略:默认每隔100ms随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除。它会在规定的时间内,分多次遍历所有的数据库。
内存淘汰机制
在两种策略下仍然可能出现内存中堆积大量过期的key,或者说堆积大量没有设置过期但不是热点数据的key
- volatile-lru:从已设置过期的时间数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
- allkeys-lru:在键空间中,移除最近最少使用的key(常用)
- volatile-lfu:从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰
- allkeys-lfu:在键空间中,移除最近最不经常使用的key(常用)
- volatile-random:从已设置过期的时间数据集(server.db[i].expires)中随机挑选数据淘汰
- allkeys-random:在键空间中,随机移除key
- volatile-ttl:从已设置过期的时间数据集(server.db[i].expires)中挑选将要过期的数据淘汰
- no-eviction:拒绝移除任何数据,报错
Redis持久化
RDB
默认的持久化方式。在指定时间间隔内生成数据库的快照。Redis创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis主从结构,主要用来提高Redis性能),还可以将快照留在原地以便重启服务器的时候使用。
三种情况下会保存RDB:
save 900 1:900秒之后,如果至少一个key发生变化,Redis自动触发BGSAVEsave 300 10:300秒之后,如果至少有10个key发生变化,Redis自动触发BGSAVEsave 60 10000:60秒之后,如果至少有10000个key发生变化,Redis自动触发BGSAVE
记录的内容:二进制形式的键值对
AOF
持久化记录服务器执行的所有写操作命令。
三种持久化方式:
- appendfsync always:每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度
- appendfsync everysec:每秒钟同步一次,显式地将多个写命令同步到硬盘
- appendfsync no:让操作系统决定何时进行同步
为了兼顾数据和写入性能,用户可以考虑 appendfsync everysec选项 ,让Redis每秒同步一次AOF文件,Redis性能几乎没受到任何影响。而且这样即使出现系统崩溃,用户最多只会丢失一秒之内产生的数据。当硬盘忙于执行写入操作的时候,Redis还会优雅的放慢自己的速度以便适应硬盘的最大写入速度。
记录的内容:操作
AOF重写
如何解决AOF体积过大、还原时间长的问题?
使用BGREWRITEAOF命令,实现原理:AOF重写不需要读取旧的AOF文件,而是读取数据库状态,将键值对的关系用命令保存下来(若某个键有多个值,多个值会合并成一条命令,默认是每64个元素变成一条命令)。在AOF重写期间,新的写命令会保存到AOF重写缓冲区内;当AOF重写完成后,AOF重写缓冲区内的操作写入到新的AOF文件中,新的AOF文件原子地覆盖旧AOF文件(该过程会阻塞父进程)
混合持久化
Redis4.0支持RDB和AOF的混合持久化。持久化开始的时刻,生成数据库的RDB快照,在生成过程中的修改操作记录到AOF缓冲区,之后将RDB快照内容和AOF增量内容写入到新的AOF文件中,最后原子地覆盖旧AOF文件
优点:数据恢复快
缺点:AOF里面的RDB部分是压缩格式而不是AOF格式,可读性较差
RDB和AOF的优缺点
RDB
优点:
- 保存的是二进制文件,占用的空间小
- 做数据恢复时速度快
缺点:
- 可读性差
- 服务器出现故障时,可能丢失较多数据
- 数据量大时,每次备份可能比较耗时
AOF
优点:
- 存放的是操作,可读性好
- 通过配置持久化方式,最多丢失一秒的数据
- 操作会记录在文件末尾,写入性能高
缺点:
- 占用的空间比RDB文件大
- 恢复数据时速度相对较慢
缓存异常
缓存雪崩
定义:缓存在同一时间大面积失效,导致请求全部落到数据库上,造成数据库短时间接受大量请求而崩掉
产生原因:
- 大量的key设置了相同的过期时间
- Redis宕机
解决办法:
- 在原有的过期时间上再加一个随机值。避免了采用相同的过期时间导致的缓存雪崩
- 使用熔断机制。当流量达到一定阈值,直接返回提示或者默认值,防止过多的请求打在数据库上,至少保证一部分用户是可以正常使用的,其他用户多刷新几次也能得到结果
- 保证Redis集群的高可用性
- 提高数据库的容灾能力,使用分库分表、读写分离策略
缓存击穿
定义:大量请求的热点key突然失效,导致请求直接落在数据库上
解法办法:
- 过期时间设置在系统低流量的时间段,比如凌晨三四点
- 使用互斥锁。只有拿到锁才可以查询数据库
- 使用限流。根据数据库的承受能力限制进入数据库查询的请求数量
缓存穿透
定义:大量请求的key不存在于缓存中,导致请求直接落在数据库上
产生原因:黑客大量制造不存在于缓存中key
解决办法:使用布隆过滤器。若布隆过滤器判断某个key不存在,则一定不存在,否则可能存在
缓存和数据库一致性
-
先更新数据库,再删除缓存。
不一致场景:缓存刚好失效,线程A去数据库里读数据,另一个线程B更新数据库并删除缓存,然后A将旧的数据写入到了缓存中。不过由于读的速度快于写的速度,出现的概率很小。
-
先删除缓存,再更新数据库。
不一致场景:线程A删除缓存,另一个线程查询缓存未命中,查询数据库并更新到缓存中,A更新数据库。由于读的速度快于写的速度,出现的概率高于第一种。
-
先更新数据库,再更新缓存。
不一致场景:线程A更新数据库,另一个线程B更新数据库并更新缓存,然后A更新缓存。从缓存层面看,出现了线程安全问题,因为缓存中的数据是先更新了数据库的那个线程的数据。此外,从业务上看,如果写操作多,频繁的更新缓存浪费性能。
-
先更新缓存,再更新数据库。
不一致场景:线程A更新缓存,另一个线程B更新缓存并更新数据库,然后A更新数据库。问题和情况3相同
事务
原子性:事务队列中的命令要么全部执行,要么全部不执行。但是在执行过程中发生的错误不会让事务回滚(比如string类型的数据用了LPUSH,没有执行的时候服务器无法判断是否正确的)
一致性:对于入队错误,Redis拒绝执行此事务;对于执行错误,Redis不会中断事务,其他命令不会被错误的命令影响
隔离性:Redis使用单线程执行事务,并且保证在执行事务期间不会中断事务
Lua脚本如何保证原子性的?Lua脚本在执行过程中,不会有其他的脚本和命令同时执行,因为Redis是单线程的
Redis高可用方案
主从复制
一个master,多个slave
拓扑结构
- 星形结构
- 链式结构
复制模式(同步策略)
- 全量复制:master全部同步到slave,用于初次复制
- 增量复制:slave断线重连后只复制丢失的数据(通过复制积压缓冲区实现)
存在的问题
- 同步故障
- 复制数据延迟(master和slave数据不一致)
- 读取过期数据(slave不能主动删除过期的数据)
- 避免全量复制
- 增大复制积压缓冲区(默认为1MB)
- 如果节点运行id不匹配(如master重启、master运行id发生变化),此时执行全量复制,应该配合哨兵和集群解决
哨兵集群
是Redis的高可用性解决方案
节点下线
-
主观下线
Sentinel(哨兵)节点对Redis节点失败的偏见,超出超时时间认为节点宕机
哨兵集群的每个Sentinel节点会定时(默认1s)对Redis集群的所有节点发送心跳包检测节点是否正常。如果一个节点在
down-after-milliseconds时间内没有回复Sentinel节点的心跳包,则该Sentinel将该Redis节点标记为主观下线 -
客观下线
当节点被一个Sentinel节点标记为主观下线,并不意味着该节点一定故障了,还需要Sentinel集群里的其他Sentinel节点共同判断为主观下线才行。该Sentinel节点会询问其他Sentinel节点,如果Sentinel集群中超过quorum个Sentinel节点认为master主观下线,则该Sentinel节点认为master客观下线
Leader选举
-
当一个master被判断为客观下线时,由Leader Sentinel 对客观下线的master执行故障转移
-
选举一个Sentinel作为Leader的前提:集群中至少有三个Sentinel节点
-
选举流程
- 每个发现master客观下线的 Sentinel 节点向其他 Sentinel 节点发送命令,要求设置它为领导者.
- 收到命令的Sentinel节点如果没有同意过其他Sentinel节点发送的命令,则同意请求,否则拒绝
- 如该Sentinel节点发现自己的票数超过Sentinel集群一半的数量,则它成为Leader
- 如果此过程没有选出Leader,则等待一段时间再重新进行选举。
故障转移
转移流程
- Sentinel选出一个合适的slave作为新的master(slaveof no one命令)
- 向其余slave发出通知,让他们成为新master的slave
- 更新新master的slave信息,待旧master复活,让他成为新master的slave
- 向客户端通知master变化
选择slave的规则
- 选择slave-priority最高的节点
- 选择复制偏移量最大的节点(同步数据最多)
- 选择runId最小的节点
Sentinel 集群运行过程中故障转移完成,所有 Sentinel 又会恢复平等。Leader 仅仅是故障转移操作出现的角色。
定时任务
-
每1s每个Sentinel对所有节点执行ping,进行心跳检测
-
每2s每个Sentinel向master和slave的命令连接发送信息(channel为
__sentinel__:hello)Sentinel通过订阅连接接收master和slave的信息
Sentinel之间创建的是命令连接,因为Sentinel是通过master和slave的订阅连接来发现Sentinel,不需要创建订阅连接
-
每10s每个Sentinel对master和slave执行info,目的是发现slave节点,确定主从关系
补充
-
Sentinel如何发现master?不存在这个问题,只能一开始就配置好!
-
Sentinel如何发现slave?
每10秒Sentinel通过命令连接向master发送INFO,通过master回复的消息不仅可以得到master的信息,也可以得到slave的地址信息
-
slave如何成为master的slave?在slave上执行命令SLAVEOF
-
Sentinel如何发现Sentinel?
Sentinel与服务器建立订阅连接后,会发送hello消息,订阅了服务器的Sentinel能收到消息后就能更新自身对其他Sentinel的认识
-
故障转移流程中,如何当旧的master上线时通知它?(因为此时Leader已经不存在了)
新的master的slave信息会更新,旧的master上线后,Sentinel发送SLAVEOF命令让它成为新master的slave
优缺点
优点:基于主从复制,解决主从复制master宕机时需要手动切换的问题
缺点:只有一个Redis主机接收处理写请求,容易成为瓶颈
分布式集群(cluster)
Gossip协议:某个节点要传播消息时,会随机选择周围的几个节点传播消息,收到消息的节点也重复该过程。
寻址分片
hash取模
最朴素的方法,hash(key) % 机器数量
存在的问题:当新增一台机器或者机器宕机时,导致hash的结果发生变化,使得对所有服务器的缓存请求全部失效,大量请求打到数据库上
一致性hash
- 一致性hash对2^32取模,将整个空间组织成hash环
- 所有服务器先用哈希函数算hash,从而确定在hash环上的位置
- 数据key用相同的哈希函数计算hash,确定数据在环上的位置,从该位置顺时针遍历遇到的第一个服务器就是其应该定位的服务器
容错性和可扩展性
容错性:当服务器C宕机后,只会让所有以C为第一次遇到服务器的数据查询缓存失败
扩展性:在B和C之间增加一台服务器后,查询C上缓存的数据部分失败
存在的问题
由于节点不均匀会导致被缓存的对象大部分缓存在某一台服务器上。(数据倾斜)
解决办法:引入虚拟节点机制,即每个服务器计算多个哈希函数,分布在hash环上,然后再映射虚拟节点到实际节点的关系。实际应用有Dubbo一致性hash负载均衡。取模操作通过TreeMap解决(若计算的hash值大于最大值,会得到null,因此取第一个元素就相当于取模操作了)
Redis中的实现:hash槽
hash -> 槽 -> 实际节点
CRC16(key) & 16383:return crc16(key,keylen) & 0x3FFF;
每个master持有部分slot,增加一个master,就将其他master的hash slot部分迁移过去;减少一个master,就将它的slot迁移到其他master上。
每个master知道所有槽的负责对象,以及自己负责的槽
- 当请求算出来的槽不由当前master负责,则它返回
MOVED让请求到对应的服务器上再次请求一次 - 当请求算出来的槽正在迁移阶段,则它返回
ASK错误,让请求到迁移目的服务器上再次请求一次
节点下线
-
疑似下线
每个节点定时向其他节点发送PING消息,超时未收到PONG消息则将接收PING命令的节点标记为疑似下线
如果主节点A通过消息知道主节点B将主节点C标记为疑似下线,则A会将B的下线报告添加到自己维护的C的结构
疑似下线报告是有时效性的,如果超过
cluster-node-timeout * 2的时间,这个下线报告会被忽略,C会恢复成正常状态 -
下线
一个集群里,半数以上负责处理槽的master将某个主节点x标记为疑似下线,则x被标记为下线,将x标记为下线的节点(该节点通过疑似下线报告的数量超过了一半从而确定了x下线)会向集群广播(所有节点立即知道x下线)
故障转移
- 从下线master的slave中选择一个合适的节点
- 被选中的slave执行
SLAVEOF no one成为新的master - 新的master撤销所有指派给旧master的槽指派,让这些槽全部指派给自己
- 新的master广播一条PONG消息,集群中其他节点立即知道这个节点成为了master,并且它已经接管了槽
- 新的master开始接收和处理指派给自己槽的命令
master选举
类似于Sentinel集群的Leader选举
- slave发现自己正在复制的master是下线状态,则向集群广播消息,要求设置它为master
- 每个负责槽的master有一次投票机会,如果它未投过票,则同意
- 如果slave收到超过半数的同意,则它成为新的master
- 如果此过程没有选出master,则集群进入下一个配置纪元(currentEpoch),再次进行选举直到选出新的master
最终一致性
master处理完客户端的写请求后就返回,它的slave是异步复制的
https://redis.io/topics/cluster-tutorial
READONLY
默认情况下slave会将客户端的请求重定向到master,使用READONLY允许客户端的读请求由slave处理
https://redis.io/commands/readonly
Redis使用场景
分布式锁
单个Redis实例
用SET命令设置key,并指定过期时间,即使获得锁的线程挂了,超时后锁也能被释放。当用完后,主动删除key
SET key_name random_value NX PX 3000 # NX表示不存在key则设置,否则不设置;PX表示过期时间用毫秒级
存在的问题
- 线程拿到锁后,被挂起,在挂起期间锁过期了,其他线程拿到了锁
- 线程拿到锁后,执行的时间大于锁租约的时间,其他线程拿到了锁
- 线程执行的时间大于锁租约的时间后,主动删除key把其他线程的锁删除了
- 线程如何实现可重入
解决方案
锁续租,在线程拿到锁之后,再开另一个线程,在该锁要过期之前为它续租
SET的值用唯一ID标识线程,线程主动删除key的时候要判断是否是自己持有锁
用ThreadLocal记录重入次数
多个Redis实例(Redlock)
防止单点故障。
原理
5个master节点
- client得到当前的时间,微秒单位
- 尝试顺序地在5个节点上申请锁,client需要设置与master沟通的timeout大小,避免长时间和一个fail的节点浪费时间
- 当client在大于等于3个节点上成功申请到锁时,计算一个申请锁的流逝时间(当前时间减去第一步的时间),如果锁的持续时间(lock validity time)多于流逝时间,那么就真正获得锁
- 锁的有效时间是
lock validity time - 流逝时间 - 如果client申请锁失败,则它在成功申请到锁的master上释放锁
- 等待随机的时间后,重新尝试获得锁
存在的问题
使用时间来解决一致性,但是它依赖于时间假设:
- 时间本身是正确的(不会有时钟突然提前或延后的情况)
- 跟锁的过期时间相比,网络延迟很小
- 跟锁的过期时间相比,进程停顿很短
消息队列
- 用list做队列,生产者用rpush放入消息,消费者用blpop消费消息,当没有消息的时候会阻塞,直到有消息
- 一次生产多次消费如何实现?用发布订阅模型
- 发布订阅模型有什么缺点?消费者掉线后生产的消息会丢失
- 如何实现延时队列?用zset,时间戳作为score,消息作为内容。消费者通过
zrangebyscore获取 N 秒之前的数据
https://blog.csdn.net/flyingwzb/article/details/83866215
注册中心
原理
Key存服务名,Value可以用Hash类型,它的键是URL地址,值是过期时间
- 服务提供方在指定的Key下添加自己的信息,同时向指定通道发送
register事件(也就是消息) - 服务消费方订阅指定通道的
register和unregister事件 - 服务消费方收到
register和unregister事件后,获得指定Key的服务提供者列表 - 服务监控中心可以查看服务提供方的信息,清理掉线的服务提供方
缺点
-
注册中心的可靠性依赖于Redis的可靠性
-
要求服务器时间必须同步,用于心跳检测过期数据,对服务器有一定压力
[Dubbo:Redis 注册中心](