Redis面试精简
缓存
缓存穿透
用户查询一个数据库中没有的数据
在发起请求后,首先会查询Redis,在Redis中查询不到,然后会查询数据库,数据库中无数据,程序也不会写入Redis缓存中,就会导致每次请求不存在数据时都会穿透Redis直接查询数据库,增加数据库压力
解决方案:
-
缓存空数据 将没查询到的数据当作空数据加载到Redis中
优点:简单
缺点:消耗内存,可能会发成不一致问题
-
添加布隆过滤器 先查询布隆过滤器,判断该数据是否真实存在 布隆过滤器:通过bitmap(位图)方式进行实现,底层为一个以bit位为单位的数组,数组中只存储二进制数,在存储数据时首先对数据进行多次hash运算,将运算后结果对应位置的bitmap置为1,后续在进行查询时步骤同上,查询对应位置的bitmap是否为1 实现方案包括Redisson、Guava
优点:内存占用少,没有多余key
缺点:实现复杂、存在误判
缓存击穿
给一个key设置了有效期,当key过期的时候,恰好这个时间点对这个key有大量并发请求过来,在Redis中查询未果后,都会越过Redis去查询数据库,增加数据库压力
解决方案:
-
加入互斥锁 第一个发现Redis中数据过期的线程持有锁去数据库中查询 第一个线程在查询DB的途中,其余线程无法获得锁对象在原地进行休眠-重试操作,直到第一个线程在DB中查询到对应的数据并写入进Redis中,释放锁供其余线程进行查询
优点: 数据一致性较强
缺点: 性能较差
-
逻辑过期 不在Redis中设置过期时间,使用逻辑字段进行替代 在第一个发现Redis中数据逻辑过期时,持有查询数据库锁对象进行数据库中查询操作,在此期间其余线程查询到逻辑过期后获取不到锁对象,直接将过期对象进行返回,直到第一个发现Redis中数据逻辑过期的线程重新加载数据库中数据对逻辑过期数据进行覆写
优点: 高可用、性能优
缺点: 数据可能存在不一致
缓存雪崩
在同一时间大量缓存的key同时失效,或者Redis服务器宕机,导致大量请求到达数据库,增加数据库压力
解决方案:
- 给不同的key的TTL添加随机值 可以达到削峰填谷的作用
- 利用Redis集群提高服务的可用性 哨兵模式、集群模式
- 给缓存业务添加降级限流策略 ngxin或Spring cloud gateway
- 给业务添加多级缓存 Guava或Caffeine
双写一致
当修改了数据库中的数据也要同时更新缓存中的数据,缓存和数据库的数据要保持一致
一致性要求高
解决方案:
-
延时双删 删除缓存中数据-修改数据库-(延时)-删除缓存 先删除缓存避免其余请求获得缓存中的脏数据,修改数据库后延迟保证数据库为主从一致的情况下的数据同步,后删除缓存中数据防止其他线程在第一次删除缓存后查询数据库将未更改的信息重新加载到缓存中
优点: 没有锁,效率高
缺点: 存在脏数据风险
-
分布式锁 适用于读多写少时,加入共享锁和排他锁 共享锁:读锁,加锁之后,其他线程可以共享读数据 排他锁:写锁,加锁之后,阻塞其他线程进行读写操作
优点: 一致性高
缺点: 效率较低
允许延迟一致
异步通知保证数据的最终一致性
解决方案:
- 基于RabbitMQ消息队列 修改数据请求发布消息,写入数据库,Redis监听消息,更新缓存
- 基于Canal异步通知 canal监听mysql的binlog日志,有更改通知Redis进行缓存更新
持久化
RDB
Redis数据快照,将内存中的所有数据都记录到磁盘中。当Redis实例重启后,从磁盘读取快照文件,恢复数据
save 900 1 表示900秒内,如果至少有1个key被修改,执行bgsave
bgsave会先fork主进程得到子进程,子进程共享主进程的内存数据,完成fork后读取内存数据并写入RDB文件 fork为拷贝主进程的页表(虚拟内存和物理内存的映射关系表),再拷贝时将内存指定为read-only类型,在主进程更改Redis数据时会拷贝一份数据出来,完成修改后,更改主进程页表映射关系
AOF
追加文件,Redis处理的每一个写命令都会记录在AOF文件,可以看做是命令日志文件,AOF默认关闭,需要更改配置文件进行开启共有三种配置项
| 配置项 | 刷盘时机 | 优点 | 缺点 |
|---|---|---|---|
| Always | 同步刷盘 | 可靠性高,几乎不丢数据 | 性能影响大 |
| everysec | 每秒刷盘 | 性能适中 | 最多丢失1秒数据 |
| no | 操作系统控制 | 性能最好 | 可靠性较差,可能丢失大量数据 |
AOF文件会被RDB文件大,AOF存在命令覆盖问题,中bgrewriteof命令,可以让AOF文件执行重写功能,用最少的命令达到相同的效果
数据过期策略
Redis对数据设置有效时间,数据过期后需要将数据从内存中删除,有两种删除策略,Redis默认两种配合使用
惰性删除
当查询到一个过期的key时,删除Redis中的key并返回空数据
优点: 对CPU友好,不用定期检查
缺点: 内存占用大,不常用的key可能不会被删除
定期删除
每隔一段时间对key进行检查(随机抽查),删除里面过期的key,两种策略,一种10HZ,每次不超过25ms,另一种两次间隔不低于2ms,每次耗时不超过1ms
优点: 对内存友好
缺点: 难以确定删除操作执行的时长和频率
数据淘汰策略
当Redis内存不够用,添加新的数据时,对数据的处理策略
LRU:最近最少使用 LFU:最少频率使用
Redis支持8种不同的策略:
- 不淘汰任何key,内存满后不允许写入新数据(默认策略)
- 对设置了TTL的key比较其TTL剩余时间,TTL越小淘汰优先级越高
- 对全体key进行随机淘汰
- 对设置了TTL的key进行随机淘汰
- 对全体key进行LRU淘汰
- 对设置了TTL的key进行LRU淘汰
- 对全体key进行LFU淘汰
- 对设置了TTL的key进行LFU淘汰
根据不同的业务场景选择不同的淘汰策略 有明显的冷热数据区分使用全体LRU 没有明显冷热数据使用全体随机
分布式锁
在分布式部署条件下,无法使用所有基于JVM层面的本地锁,只能使用Redis提供的分布式锁
获取锁:
添加锁,NX是互斥,EX是设置超时时间
set lock value nx ex 10
释放锁
del key
设置过期时间防止,获取锁成功后服务器宕机,没有释放锁,其他线程无法获取锁,可能会导致死锁
如何合理控制锁的有效时长
- 根据业务的执行时间进行预估
- 给锁续期 redisson实现的分布式锁会有一个看门狗线程,该线程每隔一段时间进行续期操作,在手动释放锁之后需要通知对应的看门狗线程不必再进行监听,其他的线程会在已经加锁是循环,超过循环次数会直接结束
可重入锁
redisson实现的锁为可重入锁,会利用hash结构记录线程id和重入次数
主从一致
在Redis集群/哨兵环境中,客户在向一个master节点获取锁成功后(还未进行数据同步)该节点宕机,导致有两个线程同时持有锁,会有脏读的问题
解决方案:
-
红锁 在多个Redis实例上创建锁,应该在多个Redis实例上创建锁(n/2 + 1),避免在一个Redis实例上加锁
缺点: 实现复杂、性能差、运维繁琐
-
使用zookeeper实现分布式锁
Redis集群方案
单节点的Redis并发能力有上限,需要搭建Redis集群提高并发能力,实现读写分离
主从复制
在首次建立主从同步时,为全量同步,后续为增量同步
全量同步
- 建立连接,从节点发送Replication Id和offset
- 主节点判断Replication Id不一致,为第一次建立连接发送数据版本信息
- 从节点保存版本信息
- 主节点执行bgsave生成RDB文件,发送给从节点
- 从节点删除自身数据,加载RDB文件
- 主节点发送repl_baklog日志中的新命令
- 从节点执行命令
增量同步
- 建立连接,从节点发送Replication Id和offset
- 主节点根据Replication Id判断出为同一数据集,后根据offset值在repl_baklog中获取其之后的数据
- 主节点发送offset后的命令
- 从节点执行命令
注:repl_baklog日志为环形链表结构,如果从节点断开时间过长,无法获取到offset后完整的数据时,则会进行全量同步
哨兵模式
Redis提供了哨兵机制实现主从集群的自动故障恢复
- 监控:哨兵会不断检查主节点和从节点是否正常工作
- 自动故障恢复:如果主节点故障,会重新选举一个主节点
- 通知:哨兵充当Redis客服端的服务发现来源
服务状态监控
哨兵基于心跳机制检测服务状态,每隔1秒向集群的每个实例发送ping,如果超时没回复则认为主观下线,超过指定数量的哨兵认为该实例主观下线后则该实例客观下线。
自动故障恢复
发现主节点故障,哨兵需要在从节点中选举新的主节点,依据如下:
- 先判断从节点的断开时间,超过规定时间的节点不参与选举
- 判断从节点的优先级(默认为1)越小越优先,0不参与选举
- 判断offset大小,offset越大说明数据越新,优先级越高
- 最后判断slave节点运行id大小,越小优先级越高(越老)
脑裂
由于内部网络原因,哨兵联系不到主节点,认为主节点故障,重新进行选举,但是主节点还在正常提供服务,在内部网络恢复后,该集群就会有两个主节点,老的主节点会被迫称为从节点
在此期间主节点写入的数据会丢失
解决方案:
- 修改最少的从节点个数 减少脑裂概率
- 缩短修改数据复制和同步延迟时间 减少发生脑裂时数据的丢失量
如何实现故障转移
- 哨兵向被选中的从节点发送slaveof no one命令,让该节点称为主节点
- 哨兵向其他所有的从节点发送slaveof ip port命令,让这些从节点成为新主节点的从节点
- 最后将故障节点标记为从节点,重启后自动成为新主节点的从节点
分片集群
主从和哨兵可以解决高可用、高并发读问题但存在以下两个问题:
- 海量数据存储问题
- 高并发写问题
使用分片集群可以解决上述问题
分片集群:
- 集群中有多个主节点,每个主节点保存不同数据
- 每个主节点可以有多个从节点
- 主节点直接通过ping命令检测健康状态
- 客户端请求可以访问集群中任意节点,最终都会被转发到正确节点
数据读写
Redis引入了哈希槽概念,Redis共有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放入哪个槽,集群的每个节点负责一部分哈希槽
Redis为什么快
- 纯内存操作
- 采用单线程,避免不必要的上下文切换可竞争条件,不考虑多线程安全问题
- 使用IO多路复用模型(详情见IO - 掘金 (juejin.cn))
Redis网络模型
Redis通过IO多路复用来提高网络性能,并且支持各种不同多路复用实现
Redis使用IO多路复用和事件派发机制
- 将收到的客户端请求进行时间派发
- 连接应答处理器
- 命令回复处理器(耗时,在高版本改为多线程)
- 命令请求处理器
- 接收命令请求处理器传来的数据,并将其转换为Redis命令(耗时,在高版本改为多线程)
- 选择执行命令把结果写入缓冲区(串行执行)
- 缓冲区数据回复给命令回复处理器