缓存穿透/缓存击穿/缓存雪崩
缓存穿透:
查询的某个key,在redis中没有,mysql也没有,这个key也不会被更新到redis中
这样的数据查多少次都没有,如果还存在很多还反复查询,一样也会给mysql带来很大压力
改进业务和加强监控报警是补救措施
而靠谱的方案:
- 参数校验:对非法参数直接拦截
- 接口限流:对同一IP和同一key的频繁查询做限流
- 如果发现这个key在两者都不存在,仍然写入redis,value设成一个非法值
- 引入布隆过隆器,每次查询redis/mysql之前先判定一下key是否可能不存在,如果是布隆过滤器说"不存在"则直接返回,若存在再查
缓存击穿:
相当于是缓存雪崩的特殊情况,针对的是"热点key"
解决方案:
- 基于统计的方式发现热点key并设置永不过期
- 互斥锁:只允许一个线程去查数据库重建缓存
- 补充方案:服务降级,类似于省电模式,在特定情况下适当的关闭一些不重要的功能,只保留核心功能
访问数据库的时候使用分布式锁,限制同时请求数据库的并发量
缓存雪崩:
由于短时间内,redis上大规模的key失效,导致缓存命中率陡然下降,mysql压力迅速上升
可能原因:
1.redis直接挂了 解决方案:加强监控报警,加强redis集群可用性的保证
2.redis正常,但是之前短时间设置了很多key的过期时间是相同的
解决方案:
- 不给key设置过期时间或者设置时添加随机的因子(避免同一时刻过期)
- 多级缓存:本地缓存(Caffeine)+Redis缓存,本地缓存扛第一波
- 限流降级:使用Sentinel对数据库访问做限流
- 数据预热:提前把热点数据加载到Redis,避免启动后瞬间打满
在redis的集群中,如何根据键定位到对应的节点
redis集群把数据分布到16384个哈希槽上,每个key通过CRC16算法算出哈希值,在对16384取模得到槽位编号
为啥是16384个槽
- 心跳包大小 :集群节点之间通过心跳包同步槽信息,16384 个槽的位图正好是 2KB(16384/8 = 2048 字节)
- 集群规模:集群不太可能超过1000个节点,这个大小刚好
- CRC16 算法:CRC16 生成 16 位结果,范围 0-65535,取 16384 是 2^14,适合位图存储
Redis集群中节点怎样通信
通过Gossip协议在集群中传播节点状态,故障信息,槽位分配等,使得集群的拓扑信息能快速的传播至所有节点,实现了去中心化,
什么是MOVED重定向?它和ASK重定向有什么区别?
MOVED重定向表示这个槽永久性地属于另一个节点,客户端应更新本地槽映射并以后直接目标节点。而ASK重定向表示这个槽正在迁移中,数据可能在源节点也可能在目标节点,这次去目标节点问一下,但不用更新本地缓存
MOVED错误:比如客户端要找的key不在这个节点上,这个节点就会返回一个MOVED错误,里面带着相应节点的ip和端口
ASK的ASKING请求:因为迁移还没完成,目标节点理论上还没正式接管这个槽,相当于时一个临时授权,如果不发的话,目标节点可能会拒绝管理
跳跃表的结构以及与红黑树相比它的优势在哪里?
跳跃表时一种基于有序链表的多层索引结构,每一层都是下一层的的快速通道,查找时从最高层开始,逐步缩小范围,最后在底层找到目标,平均时间复杂符o(log n)。
redis作者解释过选择跳跃表而不是红黑树基于以下几点
- redis的设计哲学是keep it Simple,Stupid。跳跃表的实现比红黑树简单的多,代码只要几百行,而红黑树需要几千行且容易出错
- 跳跃表的结构更容易可视化和调试,出现问题,则打印每一层链表就能定位
- redis的Sorted Set的需求是范围查询直接利用底层链表,排名计算则利用跨度字段(它记录当前层到下一个节点的距离,用于计算元素的排名),跳跃表完美符合。红黑树实现复杂且性能也差
- 跳跃表虽然比红黑树更占内存,但对内存数据库来说,这点开销完全能接收,换来的是代码的简洁性和可维护性
为什么要用redis,用场景举例,优化了什么
- redis数据存储在内存中读写速度时百万级QPS,而mysql是基于磁盘,读写是千级QPS,内存比磁盘快1000倍以上
- redis核心处理是单线程的,避免了多线程的竞争和锁开销,加上epoll这样的IO复用机制,使得在高并发下非常稳定
场景1:电商页面
未用redis,每次请求都去查mysql,mysql在高并发下出现磁盘i/o延迟,索引丢失和单表数据量过大导致查询性能骤降,使用后并发骤降
场景2:分布式锁
秒杀系统,只有很少的库存然后几十万人同时抢,多个请求读到还剩一件,容易出现超卖,而mysql本身的锁性能很差,使用setnx实现分布式锁实现了高并发下的数据一致性
既然redis是单线程那它为什么还能这么快?
redis的核心瓶颈从来不是cpu而是网络I0,之所以单线程就能支撑如此高的并发是因为它IO多路复用,让单个线程同时监听多个客户端,当有事情线程采取处理,没有任务就阻塞或处理其他任务。单线程就是解决网络IO的高效方法之一,因为它避免了多线程下的复杂加锁机制
那为什么这几年单线程设计被打破了
因为随着硬件的发展,网络IO的延迟被降低了,然后redis存的数据越来越复杂了,它引入的多线程并不是将读写命令变成多线程,而是只用于处理网络IO的读写,核心的执行命令依然是单线程
瓶颈是什么
搞垮redis的不是QPS,而是慢查询:特别复杂的命令,这样会使单线程的redis陷入长时间的阻塞
redis混合持久化断电时一般丢多少数据
混合持久化是AOF重写的格式优化,数据丢失量由AOF的appendfsync的策略决定,
与RDB无关,always:最多丢1条,everysec:最多丢1秒,no:丢失不可控,约30秒
混合持久化只加快恢复速度,不提升数据安全性
如何平衡redis的缓存命中率和安全性
一、核心矛盾
| 目标 | 做法 | 副作用 |
|---|---|---|
| 提高命中率 | 延长过期时间、不淘汰热点 key | 数据可能变脏(不一致) |
| 提高安全性 | 缩短过期时间、频繁更新缓存 | 命中率下降,数据库压力增大 |
二、具体平衡策略
1. 过期时间策略
- 热点 key 设置永不过期 + 后台异步更新
- 命中率最高,但数据可能短暂不一致
- 适合对一致性要求不高的场景(如商品详情页、排行榜)
- 非热点 key 设置合理的 TTL + 随机偏移
- 避免大量 key 同时过期导致雪崩
- 命中率适中,安全性较好
2. 淘汰策略的选择
| 策略 | 命中率 | 安全性 | 适用场景 |
|---|---|---|---|
| allkeys-lru | 高 | 中 | 通用场景,淘汰最久未使用的 |
| volatile-lru | 中 | 高 | 只淘汰设置了过期时间的 key |
| allkeys-lfu | 高 | 中 | 访问频率差异大的场景 |
| noeviction | 低 | 高 | 不允许丢数据的场景(如分布式锁) |
平衡做法 :使用 volatile-lru,只对设置了过期时间的 key 做淘汰,永不过期的热点 key 不受影响。
3. 缓存更新模式
-
Cache Aside Pattern(旁路缓存)
- 读:先查缓存,miss 则查 DB 并回写
- 写:先更新 DB,再删除缓存(延迟双删可提高安全性)
- 命中率中等,安全性较高
-
Read Through / Write Through
- 缓存层代理 DB 操作,保证一致性
- 命中率高,但实现复杂
-
异步刷新(推荐)
- 热点 key 永不过期,后台定时刷新
- 命中率最高,安全性通过版本号 或最后修改时间来保证
4. 多级缓存架构
客户端 → 本地缓存(Caffeine) → Redis 缓存 → 数据库
- 本地缓存:命中率极高(毫秒级),但容量小,适合热点数据
- Redis 缓存:命中率中等,容量大,适合温数据
- 数据库:兜底
平衡点:本地缓存设置较短的过期时间(秒级),Redis 设置较长的过期时间(分钟级),既保证命中率,又能在数据更新后快速失效。
5. 数据一致性保证
| 场景 | 做法 | 命中率影响 | 安全性 |
|---|---|---|---|
| 读多写少 | 缓存永不过期 + 后台刷新 | 高 | 中(短暂不一致) |
| 读写均衡 | Cache Aside + 延迟双删 | 中 | 高 |
| 写多读少 | 直接查 DB,不缓存 | 低 | 最高 |
四、总结
平衡的核心是按数据分级:
| 数据等级 | 命中率要求 | 安全性要求 | 策略 |
|---|---|---|---|
| L1(热点) | 极高 | 低 | 永不过期 + 异步刷新 |
| L2(温数据) | 高 | 中 | 较长 TTL + 随机偏移 |
| L3(冷数据) | 中 | 高 | 较短 TTL + Cache Aside |
| L4(敏感数据) | 低 | 极高 | 不缓存或短 TTL + 强一致性 |
redis的锁什么时候会失效
- 持有锁的线程执行时间超过过期时间:合理预估+看门狗机制
- 主从切换导致锁丢失:使用Redlock或者Zookeeper
- 解锁时误删其他线程的锁:用Lua脚本保证原子性,value使用唯一标识
- 网络异常导致锁无法续约:使用fencing token
(每次加锁生成递增的token,写入数据库时校验token是否有效)
什么是脑裂问题?
脑裂:是指在分布式系统中,由于网络分区或节点故障,集群被分裂成两个或多个独立的子集群,每个子集群都认为自己是"主的",各自独立运行,导致数据不一致。
常见三个场景:
- 网络分区:主节点与哨兵/集群断开,但与客户端连接。导致客户端继续写旧的主节点,数据丢失
- 主节点假死:主节点负载过高,响应慢,被误判为下线。同上
- 集群中的部分节点故障:形成两个独立的集群。数据分片不一样
解决方案:
- 哨兵模式的配置参数;
最少需要多少个哨兵节点同意才能进行故障转移(quorum机制,多数派决策)
主节点和哨兵断连多少时间才会判定为主观下线
故障转移超时时间
Raft算法:发生故障转移的时候,通过Raft算法率先获得半数投票的哨兵称为Leader - Redis集群的配置参数
集群节点间通信超时时间
当集群中任何节点不可用时,整个服务停止服务(默认yes,改成no) - 最关键的参数:min-replicas-to-write和min-replicas-max-lag
min-replicas-to-write 1 min-replicas-max-lag 10
主节点至少要有几个从节点连接,且延迟不超过几秒,才接受写请求 作用:当发生脑裂时,旧主节点与从节点断开连接,to-write会拒绝写请求,而避免数据丢失
代价:牺牲了可用性,换来了数据一致性