redis-面试题
一、基本概念
Redis为什么这么快
- 基于内存
- 单线程,IO多路复用
- 高级数据结构(如 SDS、Hash以及跳表等)
缓存三大问题以及解决方案?
缓存穿透(查询数据不存在)
解决办法
- 缓存空值
- 布隆过滤器做Key值校验
缓存击穿(缓存过期,伴随大量对该 key 的请求)
解决办法
- 互斥锁
- 热点数据永不过期
- 熔断降级
缓存雪崩(同一时间大批量的 key 过期)
解决办法
- 随机分散过期时间
- 热点数据永不过期
一致性解决办法
强一致性:串行化
弱一致性:延时双删
保证redis的高并发
主从加集群,读写分离
保证原子性
- 使用 incr、decr、setnx 等原子操作
- 使用锁
- 使用lua脚本
Redis 是如何实现字典的?
1 | typedef struct dictEntry { |
渐进式Hash
扩容
满足任一条件
- 服务器目前没有在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 1
- 服务器目前正在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 5
缩容
- 正在执行 BGSAVE 命令或者 BGREWRITEAOF 命令,则不进行缩容
- 当哈希表的负载因子小于 0.1 时, 程序自动开始对哈希表执行收缩操作。
步骤:
- 为 ht[1] 分配空间, 让字典同时持有 ht[0] 和 ht[1] 两个哈希表
- 字典中维持一个索引计数器变量 rehashidx , 并将它的值设置为 0 , 表示 rehash 工作正式开始
- rehash 进行期间, 每次对字典执行添加、删除、查找或者更新操作时, 程序除了执行指定的操作以外, 还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1] , 当 rehash 工作完成之后, 程序将 rehashidx 属性的值增一
- redis周期函数中,如果发现有字典正在进行渐进式rehash操作,则会花费1毫秒的时间,帮助一起进行渐进式rehash操作
- ht[0] 的所有键值对都会被 rehash 至 ht[1] , 这时程序将 rehashidx 属性的值设为 -1 , 表示 rehash 操作已完成
影响:
- 读取、删除、更新会同时在俩张表上进行,先ht[0],后ht[1]
- 新增只会在ht[1]
- 在rehash期间,同时有两个hash表在使用,会使得redis内存使用量瞬间突增,在Redis 满容状态下由于Rehash会导致大量Key驱逐
拓展:redis的周期函数有哪些
Zset 为何不使用红黑树等平衡树?
- 跳跃表范围查询比平衡树操作简单
- 平衡树的删除和插入需要对子树进行相应的调整,而跳表只需要修改相邻的节点即可
- 跳表和平衡树的查询操作都是O(logN)的时间复杂度
什么是 RedisObject?
也就是我们常说的五种数据结构:字符串对象、列表对象、哈希对象、集合对象和有序集合对象等
这样做有两个好处:
1)通过不同类型的对象,Redis 可以在执行命令之前,根据对象的类型来判断一个对象是否可以执行该的命令。
2)可以针对不同的使用场景,为对象设置不同的实现,从而优化内存或查询速度。
Redis过期策略
采用惰性+定期删除策略,memcached只用了惰性删除
惰性删除
- 进行get或setnx等操作时,先检查key是否过期,过期则删除
定期删除
在配置文件中根据server.hz来配置,默认10,即每秒10次
- 遍历每个数据库,检查指定个key
持久化文件对过期策略的处理?
RDB:
- 持久化时检查是否过期,过期不持久化
- 恢复时同理判断
AOF
- 当key过期后,还没有被删除,此时进行执行持久化操作(该key是不会进入aof文件的,因为没有发生修改命令)
- 当key过期后,在发生删除操作时,程序会向aof文件追加一条del命令(在将来的以aof文件恢复数据的时候该过期的键就会被删掉)
- 重写时,会先判断key是否过期,已过期的key不会重写到aof文件
Redis 有哪些内存淘汰机制?
volatile
- volatile-lru:设置过期时间且最近最少使用淘汰
- volatile-ttl:设置过期时间且将要过期的淘汰
- volatile-random:设置过期时间的随机淘汰
- volatile-lfu:设置过期时间的且使用频率最低淘汰
allKeys
- allkeys-lru:最近最少使用
- allkeys-lfu: 使用频率最低
- allkeys-random: 随机
no-enviction
禁止驱逐数据,默认策略
Redis 有哪些持久化机制?
RDB
在指定的时间间隔内将内存中的数据集快照写入磁盘,默认的文件名为 dump.rdb,支持 同步(save 命令)、后台异步(bgsave)以及自动配置三种方式触发
优点:
- 文件紧凑,全量备份,非常适合用于进行备份和灾难恢复
- 支持异步
- 恢复大数据集时比AOF快
缺点:
- 在快照持久化期间修改的数据不会被保存,可能丢失数据
AOF
将每一个收到的写命令追加到文件中,三种触发方式:1)每修改同步 always 2)每秒同步 everysec 3)不同no:从不同步
优点
- 更好的保护数据不丢失
- 通过非常可读的方式进行记录,这个特性非常适合做灾难性的误删除的紧急恢复
- 没有任何磁盘寻址的开销,写入性能非常高,文件不容易破损
- 不会影响客户端的读写
缺点:
- AOF 日志文件通常比 RDB 数据快照文件更大
- 支持的写 QPS 会比RDB支持的写 QPS 低
结合使用
Redis从4.0版本开始引入RDB-AOF混合持久化模式,这种模式是基于AOF持久化模式构建而来,打开了服务器的AOF持久化功能,并且将aof-use-rdb-preamble
原理:
Redis服务器 在执行 AOF重写操作时,就会像执行BGSAVE命令那样,根据数据库当前的状态 生成出 相应的RDB数据,并将这些数据 写入 新建的AOF文件中,至于那些 在AOF重写开始之后 执行的Redis命令,则会继续以协议文本的方式 追加到 新AOF文件的末尾,即已有的RDB数据的后面
redis单线程IO多路复用
- IO 线程要么同时在读 socket,要么同时在写,不会同时读或写
- IO 线程只负责读写 socket 解析命令,不负责命令处理。
集群
Redis主从复制
全量同步和增量同步
全量同步
- slave发送SYNC命令
- master收到并执行 BGSAVE 命令生产 RDB 文件,并使用缓冲区记录此后执行的所有写命令;
- master 执行完 BGSAVE 后,向所有的 slave 发送快照文件,并在发送过程中继续记录执行的写命令;
- slave 收到快照后,丢弃所有的旧数据,载入收到的数据;
- master 快照发送完成后就会开始向 slave 发送缓冲区的写命令;
- slave 完成对快照的载入,并开始接受命令请求,执行来自 master 缓冲区的写命令;
- slave 完成上面的数据初始化后就可以开始接受用户的读请求了。
也可以通过无盘复制来达到目的,由master直接开启一个socket将rdb文件发送给slave服务器
部分同步
从Redis 2.8开始,如果遭遇连接断开,重新连接之后可以从中断处继续进行复制,而不必重新同步。
它的工作原理是这样:
主服务器端为复制流维护一个内存缓冲区(in-memory backlog)。主从服务器都维护一个复制偏移量(replication offset)和master run id ,
当连接断开时,从服务器会重新连接上主服务器,然后请求继续复制,假如主从服务器的两个master run id相同,并且指定的偏移量在内存缓冲
区中还有效,复制就会从上次中断的点开始继续。如果其中一个条件不满足,就会进行完全重新同步(在2.8版本之前就是直接进行完全重新同步)。
因为主运行id不保存在磁盘中,如果从服务器重启了的话就只能进行完全同步了。
部分重新同步这个新特性内部使用PSYNC命令,旧的实现中使用SYNC命令。Redis2.8版本可以检测出它所连接的服务器是否支持PSYNC命令,不支持的
话使用SYNC命令。
增量同步
- 将master接到的写命令也发给slave
限制有N个以上从服务器才允许写入
从Redis 2.8版本开始,可以配置主服务器连接N个以上从服务器才允许对主服务器进行写操作。但是,因为Redis使用的是异步主从复制,
没办法确保从服务器确实收到了要写入的数据,所以还是有一定的数据丢失的可能性。这一特性的工作原理如下:
1)从服务器每秒钟ping一次主服务器,确认处理的复制流数量。
2)主服务器记住每个从服务器最近一次ping的时间。
3)用户可以配置最少要有N个服务器有小于M秒的确认延迟。
4)如果有N个以上从服务器,并且确认延迟小于M秒,主服务器接受写操作。还可以把这看做是CAP原则(一致性,可用性,分区容错性)不严格的一致性实现,虽然不能百分百确保一致性,但至少保证了丢失的数据不会超过M秒内的数据量。
如果条件不满足,主服务器会拒绝写操作并返回一个错误。
1)min-slaves-to-write(最小从服务器数)
2)min-slaves-max-lag(从服务器最大确认延迟)
一致性Hash
hash环,key 通过 hash 计算之后得到在 hash 环中的位置,然后顺时针方向找到第一个节点,这个节点就是存放 key 的节点。为了解决扩容和宕机问题。
因为普通Hash扩容或者宕机时,会影响每个节点上的Key,导致Key大面积失效,而用一致性Hash只会影响相邻的节点
同时还可以通过虚拟节点来解决数据倾斜的问题:就是在节点稀疏的 hash 环上对物理节点虚拟出一部分虚拟节点,key 会打到虚拟节点上面,而虚拟节点上的 key 实际也是映射到物理节点上的,这样就避免了数据倾斜导致单节点压力过大导致节点雪崩的问题。
Hash槽
槽的长度为16384,Redis 集群没有使用一致性hash, 而是引入了哈希槽slots的概念,jedis客户端jar包就是实现了一致性hash算法(客户端模式),或者在redis集群前面加上一层前置代理如Twemproxy也实现了hash一致性算法(代理模式)
在redis节点发送心跳包时需要把所有的槽放到这个心跳包里,以便让节点知道当前集群信息,16384=16k,在发送心跳包时使用char进行bitmap压缩后是2k(2 * 8 (8 bit) * 1024(1k) = 16K),也就是说使用2k的空间创建了16k的槽数。 虽然使用CRC16算法最多可以分配65535(2^16-1)个槽位,65535=65k,压缩后就是8k(8 * 8 (8 bit) *1024(1k)=65K),也就是说需要需要8k的心跳包,作者认为这样做不太值得;并且一般情况下一个redis集群不会有超过1000个master节点,所以16k的槽位是个比较合适的选择。
大Key优化
热key优化
请求到的分片过于集中,超过单台 Server 的性能极限
解决办法:
1)服务端缓存:即将热点数据缓存至服务端的内存中;
2)备份热点Key:即将热点Key+随机数,随机分配至 Redis 其它节点中
如何发现:
- 凭借业务经验,进行预估哪些是热key
- 在客户端进行收集
- 在Proxy层做收集
- 用redis自带命令:monitor命令和-hotkeys启动参数