redis/redis 面试题
redis 主要用来实现缓存功能。
缓存的主流应用架构如下:
1. 客户端请求数据,如果数据在缓存层里面,那么直接返回。 2. 如果数据不在缓存层里面,那么就到数据库里查询,这个过程叫做穿透查询。并把数据回种到缓存层。 3. 此外,这种架构还能实现熔断机制:如果我们发现数据库挂了,那么就可以将请求拦在缓存层,无论数据是否在缓存层中,都直接返回。
Mencache 和 Redis 的区别
- Mencache:代码层次类似 Hash
- 支持简单数据
- 不支持数据持久化存储
- 不支持主从
- 不支持分片(不支持将大量数据分布到多个物理节点)
- Redis
- 数据类型丰富
- 支持数据磁盘持久化存储
- 支持主从同步
- 支持分片
根据业务特性来选择适合自己的缓存:
- 如果是简单类型,并且不需要持久化,那么可以考虑用 Mencache
- 否则使用 Redis
为什么 Redis 能这么快
- 完全基于内存。
- 数据结构简单,对数据的操作也简单。
- 采用单线程,主线程负责IO 事件、集群协调、过期键的处理等。除了 IO 事件之外,其他事件会被周期性低执行。由于采用了单线程,多个客户端对同一个键修改,就不会出现并发的问题,避免频繁的上下文切换和锁竞争。再配合 IO 多路复用,单线程可以达到 10万+ 的 QPS。也可以在多核的服务器中启动多个 Redis 实例。这里的单线程是指处理网络请求时只使用一个线程。在持久化等操作中,会启动另外的线程。
- IO 多路复用(Select 系统调用)。
Redis 采用的 IO 多路复用函数有:epoll、kqueue、evport、select。
- epoll、kqueue、evport 等的时间复杂度都是 O(1),但这些函数不是每个平台都有。具体来说,会根据不同系统调用不同的函数,优先选择 O(1) 的函数。
- 如果当前系统没有实现 epoll、kqueue、evport 等函数,那么就会选择时间复杂度为 O(n) 的 select。
- 这些都是基于 react 的设计模式监听 IO 事件。当 accept、write、close 等事件产生时,会回调 FD 绑定的事件处理器。通过 IO 多路复用就可以监听多个 FD,使得 Redis 高效。
Redis 的数据类型
- String:最基本的数据类型,是二进制安全的,可以存储任何数据,如图片、序列化的对象。使用简单动态字符串实现
- Hash:String 元素组成的字段,适合用于存储对象。
- List:列表,按照 String 元素插入顺序排序。先进后出,类似于栈
- Set:String 元素组成的无序集合,通过哈希表实现,不允许重复。
- Sorted Set:通过分数为集合中的成员从小到大排序,使用跳表实现。
- 用于计数的 HyperLogLog,用于支持存储地理位置信息的 Geo。
从海量 Key 里查询出某一固定前缀的 Key
使用 keys 指令会卡顿,对线上业务造成影响。
1 | keys k1* |
应该使用 SCAN cursor [MATCH pattern] [COUNT count]。
1 | SCAN 0 MATCH k1* COUNT count |
会返回不大于 count 数量的数据,已经下一个 cursor。我们在 Java 代码中可以使用 Jedis 库,循环使用 SCAN 取出 某一固定前缀的 Key,注意需要使用 HashSet 存储,因为可能会重复取出数据。
如何通过 Redis 实现分布式锁
- 分布式锁要解决的问题
- 互斥性:同一时刻只能有一个客户端获取锁。
- 安全性:锁只能被持有该锁的客户端删除,不能由其他客户端删除。
- 死锁:获取锁的客户端因为某些原因宕机,无法释放锁。
- 容错:当部分 Redis 节点宕机时,需要保证客户端依然能够获取锁。
SETNX 可以用来实现分布式锁,如果设置成功(返回 1),表示获取锁成功;如果设置失败(返回 1),则获取锁失败。
1 | setnx locknx test |
然后设置过期时间
1 | expire locknx 2 |
但程序可能执行完第一个 setnx
后就宕机了,无法设置过期时间,导致无法释放锁。这是因为两个方法组合起来不满足原子性。
解决方法是使用 SET 命令,在设置 key 的时候同时设置过期时间。
1 | SET key value [EX seconds] [PX milliseconds] [NX|XX] |
- NX 表示只有键不存在时,才对键进行设置操作。
- XX 表示只有键存在时,才对键进行设置操作。
- SET 操作成功时,返回 OK,否则返回 nil。
1 | set locktarget 12345 ex 10 nx |
大量的 Key 同时过期的注意事项
如果大量的 Key 同时过期,由于删除大量的 key 很耗时,会出现短暂的卡顿现象。
解决方法是:
- 在设置 key 的过期时间时,给每个 key 加上随机值
如何使用 Redis 做异步队列
使用 List 作为队列,RPUSH 生产消息,LPOP 消费信息。
- 缺点:没有等待队列里有值就直接消费。
- 弥补方案:可以通过在应用层使用 sleep 机制调用 LPOP 重试。
更好的方案是使用 BLPOP [key] timeout
阻塞,直到队列有消息或者超时。
1 | BLPOP testlist 10 |
上面生产出来的胡数据只能给一个消费者消费。
可以使用发布订阅模式来实现一个消息给多个消费者消费。
- pub 发送消息,sub 接收消息
- 订阅者可以订阅任意数量的 topic。
但这种方式是有缺点的:消息的发布是无状态的,无法保证可达。
Redis 持久化
stop-writes-on-basave-error
设置为 yes。在备份进程出错时,主进程就停止接收新的写入操作,保护持久化数据一致性的问题。
修改 redis.conf 的 save 配置,就可以修改 RDB 持久化。
一般使用 BGSAVE,它会 fork 出一个子进程来创建 RDB 文件,不阻塞服务器进行。
自动化触发 RDB 持久化的方式
- 根据 redis.conf 配置里的 SAVE m n 定时触发
- 主从复制时,主节点自动触发。
- 执行 Debug Reload
- 执行 Shutdown 且没有开启 AOF 持久化
BGSAVE 的原理
使用系统调用 fork():创建进行,实现了 Copy on Write。内核只为子进程创建虚拟空间,父子进程使用相同的物理空间。只有发生改变时,才会为子进程分配新的物理空间。
使用 lastsave 可以查看上次成功备份的时间。
- RDB 持久化
- 内存数据的全量同步,数据量大会由于 IO 而严重影响性能。
- 可能会丢死最近一次快照之后的数据
- AOF 持久化:保存写状态
- 记录下除了查询以外的所有变更数据库状态的指令
- 以 append 的形式追加保存到 AOF 文件中
1 | config set appendonly yes |
AOF 持久化,可以通过日志重写解决 AOF 文件大小不断增大的问题,原理如下:
- 调用 fork(),创建一个子进程。
- 子进程把新的 AOF 写到临时文件里,不依赖原来的 AOF 文件
- 主进程持续将新的变动写到内存和原来的 AOF 里
- 主进程获取子进程重写 AOF 的完成信号,往新的 AOF 同步增量变动。
- 使用新的 AOF 文件替换旧的 AOF 文件
AOF 和 RDB 的缺点
- RDB 优点:全量数据快照,文件小,恢复快。
- RDB 缺点:无法保存最近一次快照之后的数据。
- AOF 优点:可读性高,适合保存增量数据,数据不易丢失
- AOF 缺点:文件体积大,恢复时间长
Redis 4.0 之后,默认使用 RDB + AOF 混合持久化方式。
先以 RDB 格式写入全量数据,再追加增量数据。
BGSAVE 做镜像全量持久化,AOF 做增量持久化。
在恢复时,先恢复 RDB 的全量数据,再使用 AOF 重放历史命令。
Pipeline
使用 Pipeline 批量执行命令,节省多次 IO 往返的时间。
Redis 的同步机制有全同步过程和增量同步过程。
- 全同步过程
- Slave 发送 sync 命令道 Master
- Master 启动一个后台进程,将 Redis 中的数据快照保存到文件中
- Master 将保存数据快照期间接收到的命令缓存起来
- Master 完成写文件操作后,将该文件发送给 Salve。
- 使用新的 AOF 文件替换旧的 AOF 文件
- Master 将这期间收集的增量写命令发送给 Slave 端
- 增量同步过程
Redis 通过 哨兵机制来解决主从同步时,Master 宕机后的主从切换问题。
Redis 集群
如何从海量数据里快速找到需要的数据?
- 分片:按照某种规则区划分数据,分散存储在多个节点上
- 常规的按照哈希划分(Hash 对节点数量取模)无法实现节点的动态增加和减少
因此使用一致性哈希算法,(IP 地址+ 主机名)对 \(2^{32}\) 取模,将哈希值空间组织成虚拟的圆环。
这种方法只需要重新定位环中一小部分的数据,有比较好的容错性和扩展性,可以做到最小化的有损服务。
但在节点少时,可能会出现数据倾斜问题。引入虚拟节点解决数据倾斜的问题,对每个服务器节点计算多个 hash,映射到多个虚拟节点上。
将虚拟节点设置为 32 或者更大。还可以引入主从同步,哨兵机制来进一步确保高可用性。
总结
- 主流应用架构
- Mencache 和 Redis 区别
- 单线程 + IO 多路复用
- Redis 常用数据类型
- 利用 SCAN 在海量数据里筛选某一固定前缀的 key
- 使用 SETNX 实现简单的分布式锁
- 使用 List 和 BLPOP 实现异步消息队列
- 持久化原理
- 使用 BGSAVE 的 RDB 快照持久化
- 基于操作指令的 AOF 增量持久化
- 混合模式
- Redis 主从原理
- Redis 哨兵原理
- Redis 集群原理