java面试八股 redis篇
1.redis使用场景
缓存: 缓存三兄弟 穿透 击穿 雪崩 双协一致 持久化 数据过期策略 数据过期策略
分布式锁: setnx redisson
消息队列 延迟队列 何种数据类型
2.缓存击穿
缓存穿透:通过查询一个不存在的数据,数据库查询不到数据也不会写入缓存,导致每次都直接查数据库
解决方法一 :缓存空数据,查询返回的数据为空,仍贾昂这个空结果进行缓存
优点:简单
缺点:消耗内存,可能发送不一致问题(一开始 id为 10000 可能数据库中没有,然后缓存了空。后面数据库添加了这个数据 就导致缓存和数据库不一致)
解决方法二:布隆过滤器
根据id查询数据 查询布隆过滤器 布隆过滤器 预热缓存时,预热布隆过滤器 redis查询不到查询数据库
不存在 直接返回 布隆过滤器存在,返回redis
bitmap(位图)二进制数组
布隆过滤器 :检索一个元素是否在一个集合追踪
存储数据,通过多个hash函数,获取hash值,将其hash数改为1
优点:内存占用较小,没有多余的key
缺点:实现复杂,存在误判 要求95%以上正确率
3.缓存穿透
缓存击穿:某一个key设置了过期时间,当key过期的时候,恰好这个时间点对这个key有大量的并发请求过来,这些并发请求可能压倒数据库
解决方法一:互斥锁
线程一: 1查询缓存未命中 2获取互斥锁成功 3查询数据库重建缓存数据
线程二: 1查询缓存未命中 2获取互斥锁失败 3重试 4重试 5缓存命中
优点:强一致
缺点:性能差
解决方法二:逻辑过期 (不设置过期时间)
在数据库中添加一个字段 过期时间
线程一: 1查询缓存 发现逻辑过期 已过期 2获取互斥锁成功 3开始一个新的线程去 重构缓存 4返回过期数据
线程二: 1 查询数据库重建缓存数据 2写入缓存 重置逻辑过期时间 3 释放锁
进程三: 1查询缓存 发现逻辑过期 已过期 2获取互斥锁成功 3返回过期数据
优点:高可用 性能优
缺点:无法做到强一致
4.缓存雪崩
缓存雪崩:同一时间大量的缓存key同时失效或者redis服务宕机,导致大量请求来到数据库,带来巨大压力
解决方案一:给不同的key的ttl添加随机值
解决方案二:利用redis集群提高服务的可用性 例如 哨兵模式 集群模式
解决方案三:给缓存业务增加 降级限流的策略 在springboot中 使用ngxin springcloud中 使用gateway网关进行配置
解决方案四:给业务添加多级缓存 Guava或者Caffeine 一级缓存 redis 二级缓存
5.双写一致性
双写一致性:当修改了数据库的数据,也要同时更新缓存的数据,缓存和数据库保持一致
1.一致性要求高
互斥锁。存入缓存的数据 一般都是 读多写少
所以可以使用 redisson读写锁
共享锁:加锁后,其他线程可以读操作,但是不可以写
排他锁:加锁就,阻塞其他线程读写操作
2.允许短暂不一致(大多数情况)
异步通知 保证数据的最终一致性
1.使用MQ
修改数据 写入数据库 发一条消息给MQ 缓存去监听MQ 去更新缓存
(需要保证MQ的可靠性)
2.使用Canal (阿里的中间件)MySQL 数据库增量日志解析工具 代码低侵入 基本不需要修改代码
基于mysql的主从同步来实现的。
修改数据 写入数据库 (数据库会自动记录到 binlog文件 所有的 dll 数据定义语句 和 dml 数据库操作语句 不包括查询)
canal会监听 mysql的binlog 去更新缓存
6.持久化
对数据安全要求高,将两者进行结合。
1.RDB
数据快照,将内存中的数据全部记录到磁盘中,当redis实例故障重启后,从磁盘读取快照文件,恢复数据。
save 由redis主进程来执行RDB,,会阻塞所有命令
bgsave 开启子进程执行RDB,避免主进程收到影响
redis 内部有触发RDB的机制 在redis.conf文件中
900秒内,如果只要有一个key被修改,则执行bgsave
save 900 1
save 300 10
save 60 10000
执行原理: bgsave开始时会fork主进程得到子进程,子进程 共享 主进程的内存数据。完成fork后读取内存数据并写入RDB文件。
fork使用的是 copy-on-write技术
主进程有一个 页表(这个页表会记录 虚拟地址和物理地址的映射关系 ) 子进程会 拷贝 这个页表 有相同的映射关系,实现内存共享。
主进程在写数据 子进场在读写数据,会导致脏数据出现 (写新的RDB文件 覆盖就得RDB文件)
2.AOF
追加文件,redis处理的每一个写命令都会记录在AOF文件中,可以看做命令日志文件
默认是关闭状态 修改redis.conf配置文件来开启AOF appendonly
每执行一次写命令,立即记录到AOF文件
写完命令执行完后先放入AOF缓冲区,每隔1秒将缓冲区的数据写到AOF文件(默认方案)
写完命令执行完后先放入AOF缓冲区,由操作系统决定何时将缓冲区的数据写到AOF文件
always 同步刷盘 可靠性高,几乎不丢数据 性能影响较大
everysec 每秒刷盘 性能适中 最多丢失1秒数据
no 操作系统控制 性能最好 可能丢失大量数据
AOF是记录命令,AOF文件比RDB文件大的多,而且会记录对同一个key的多次写操作,但是只有最后一次写操作才有意义。通过执行bgrewriteaoof命令,可以让AOF文件执行重写功能,用最少得命令达到相同效果。
7.数据过期策略
惰性+定期 配合使用
惰性删除:设置该redis过期时间后,不去管塔,当需要该key时,我们在检查其是否过期,如果过期,我们就删掉它,反之返回该key。
定期删除:每隔一段时间,我们就对一些key进行检查,删除里面过期的key(从一定数量的数据库中取出一定数量的key进行检查,并删除其中过期的key)
SLOW 定时任务 执行频率固定
FAST 执行频率不固定
优点:通过限制删除操作执行的时长和频率减少删除操作对CPU的影响,定期删除也可以有效释放过期key占用的内存。
缺点:难以确定删除操作执行的时长和频率。
8.数据淘汰策略
数据淘汰策略:当redis中的内存不够用时,此时在向redis中添加新的key,那么redis就会按照某以规则将内存中的数据删除。
8种不同的淘汰策略
不淘汰任何key,但内存满时不允许写入新数据,默认策略,内存满了 继续添加数据 会报错。
LRU算法:最近最少使用,用当前时间减去最后一次访问时间。
LFU算法:最少频率使用。
或者使用随机淘汰。
还可以 将顶置数据不设置过期时间,同时使用LRU算法。
9.redis分布式锁 使用场景
集群情况下的定时任务,抢单,幂等性场景
线程一:1.获取互斥锁成功 2.查询优惠券 3.库存是都充足 是:扣除库存 否:抛出异常 4.释放锁
线程二:1.获取互斥锁失败 重试 重试 重试 2.查询优惠券 3.库存是都充足 是:扣除库存 否:抛出异常
为了支持并发请求,会使用集群部署。使用ngix,进行反向代理,负载均衡到几个服务器上。不能使用本地锁,需要使用分布式锁。
10.redis分布式锁 实现原理
redis实现分布式锁主要利用redis的setnx命令。
如果不设置超时时间,当业务超时或者服务宕机就没有办法释放锁了。
redis分布式锁要合理控制锁的有效时长:给锁续期。
redisson实现分布式锁 执行流程
加锁成功后 有一个看门狗 每隔 release Time/3 对锁进行续期。业务完成,去手动释放锁,并通知看门狗不续期。
如果加锁失败 会进行while循环,不断尝试加锁。
redisson所有命令lua脚本实现 保证原子性。
redisson实现分布式锁 可重入
同一个线程可以多次获取同一把锁,而不会造成死锁。每次加锁,内部会维护一个计数器(或持有次数),只有当释放次数等于加锁次数时,锁才会真正被释放。
利用hash结构记录线程id和重入次数。
redisson实现分布式锁 主从一致性
从节点(redis slave)
主节点(redis master)
从节点(redis slave)
redlock(红锁):不能只在一个redis实例上创建锁,应该在多个redis实例上创建 (n/2+1),避免在一个redis实例上加锁
缺点:实现复杂 性能差 运维繁琐
如果一定要保证数据的强一致性,建议采用zookeeper实现分布式锁
11.主从复制,主从同步
三种集群方案
1.主从复制
2.哨兵模式
3.分片集群
主从复制:单节点redis的并发能力有上限,进一步提高并发能力,需要搭建主从集群,实现读写分离。
主节点进行写操作,从节点进行读操作。主节点进行写操作后,将数据同步到从节点
主从数据同步原理
主从全量同步
1.执行replicao命令,建立连接
2.请求数据同步
3.判断是否是第一次同步
4.是第一次,返回master的数据版本信息 replid offset
5.保存版本信息
6.执行bgsave,生成RDB文件
7.发送RDB文件
8.清空本地数据,加载RDB文件
9.记录RDB期间的所有命令
10.发送repl_baklog中的命令
11.执行接收到的命令
主从增量同步
1.slave重启
2.psync repild offset
3.判断请求replid是否一致
4.不是第一次,回复continue
5.去repl_baklog获取offset后的数据
6.发送offset后的数据
7.执行命令
12.哨兵模式(保证redis高并发可用)
监控:Sentinel会不断检查 master和slave是否按预期工作。
自动故障恢复:如果master故障,sentinel会自动将一个slave提升为master。故障恢复后,也以新的master为主。
通知:被监控的 Redis 实例出现问题时,Sentinel 可以通过 API(如发送邮件、调用脚本)通知管理员。
Sentinel基于心跳机制检测服务状态,每隔1秒向集群的每个实例发送ping命令
1.主观下线:Sentinel节点发现某实例为在规定时间响应,则认为该实例 主观下线
2.客观下线:超过指定数量(quorim)的sentinel都认为该实例主观下线,则认为该实例 客观下线
选主规则:
1.首先判断主与从节点断开时间长短,超过指定值就排除该从节点
2.然后判断从节点的slave-prioity,越小优先级越高
3.如果slave-prioity一样,则判断slave节点的offset值,越大优先级越高
4.最后判断slave的运行id大小哦,越小优先级越高
脑裂问题
集群脑裂是由于主节点和从节点和sentinel处于不同的网络分区,使得sentinel没有能够心跳感知到主节点,所以通过选举的方式提升了一个从节点为主,这样就存在了两个master,就像大脑分裂了一样,这样会导致客户端还在老的主节点那里写入数据,新节点无法同步数据,当网络恢复后,sentinel会将老的主节点降为从节点,这时再从新master同步数据,就会导致数据丢失。
解决:我们可以修改redis的配置,可以设置最少的从节点数量以及缩短主从数据同步的延迟时间,达不到要求就拒绝请求,就可以避免大量的数据丢失。
13.分片集群
主从和哨兵可以解决高可用,高并读的问题。但是依旧存在 两个问题。
海量数据存储问题。高并发写的问题。
集群中有多个master,每个master保存不同数据。
每个master都可以有多个slave节点
master通过ping检测彼此健康状态
客户端请求可以访问集群任意节点,最终都会被转发到正确节点
分片集群引入了哈希槽的概念,redis集群有16384个哈希槽,每个key通过crc16校验后 对16384进行取模。决定放在哪个槽,每个节点负责一部分的hash槽
14.redis是单线程,为什么执行速度很快
redis是纯内存操作,执行素的非常快
采用单线程,避免不必要的上下文切换可竞争条件,多线程还要考虑安全问题
采用I/O多路复用模型,非阻塞IO
I/O多路复用
redis是纯内存操作,执行速度非常快,它的性能瓶颈是网络延迟而不是执行素的,I/O多路复用就是实现了高效的网络请求
利用单个线程同时监听多个Socket,可读可写时得到通知,充分利用CPU资源
监听方式:select poll epoll
| 特性 | select/poll | epoll |
|---|---|---|
| 通知方式 | 告诉你“有连接就绪” | 告诉你“哪个连接就绪” |
| 需要操作 | 用户需遍历所有 fd 查看哪个就绪 | 直接返回就绪的 fd 列表 |
| 效率 | O(n) 遍历开销大 | O(1) 或 O(k),k 是就绪数 |
redis网络模型
IO多路复用+事件派送
Redis 使用一个 事件循环(event loop) 来处理两类事件:
-
文件事件(File Events)
- 对应网络 I/O:客户端连接、读请求、写响应等
- 底层通过
epoll(Linux)、kqueue(macOS/BSD)或select(兼容模式)实现 I/O 多路复用
-
时间事件(Time Events)
- 定时任务:如过期键清理、持久化触发(RDB/AOF)、集群心跳等

浙公网安备 33010602011771号