八股-Redis
Redis 相关面试题
使用场景
缓存(穿透、击穿、雪崩、双写一致、持久化、数据过期、淘汰策略)
分布式锁(setnx+lua、redisson)
穿透无中生有 key,布隆过滤 null 隔离
缓存击穿过期 key,锁与非期解难题
雪崩大量过期 key,过期时间要随机
面试必考三兄弟,可用限流来保底
缓存穿透
缓存穿透 是指客户端请求查询一个 在缓存和数据库中都不存在 的数据。由于缓存中没有命中,请求会直接打到数据库层。如果这类请求量很大(例如恶意攻击者构造大量不存在的 key),就会导致数据库压力骤增,甚至可能宕机,这就好像缓存被“穿透”了,失去了保护后端数据源的作用
解决-缓存空值
方案描述: 当数据库查询不到数据时,仍然将一个特殊的值(比如
null
或者一个约定的空对象)缓存起来,并设置一个较短的过期时间(TTL)。这样,后续对同一个不存在 key 的请求就会直接从缓存命中这个“空值”,而不会再访问数据库优点:
- 实现简单,易于理解和部署
- 能够有效防止 同一个 不存在的 key 反复穿透缓存
缺点:
- 需要额外的缓存空间存储这些空值
- 缓存了空值后,如果数据库中后来又新增了这条数据,缓存层在过期之前无法感知到,可能导致短暂的数据不一致。需要较短的 TTL 来缓解
- 如果攻击者使用 大量不同 的不存在的 key 进行攻击,这种方法仍然会给数据库带来一定的压力(首次查询时),并且会占用大量缓存空间
⭐️解决-布隆过滤器 (Bloom Filter)
方案描述: 将所有 可能存在 的数据的 key 提前存储到一个布隆过滤器中。布隆过滤器是一种空间效率极高的概率型数据结构,它可以判断一个元素 一定不存在 或者 可能存在。在访问缓存之前,先通过布隆过滤器检查 key 是否存在。如果布隆过滤器判断 key 不存在,则直接拒绝请求或返回空,不再查询缓存和数据库。如果判断 key 可能存在,则继续后续的缓存和数据库查询流程
优点:
- 查询效率高,空间占用小
- 能够有效拦截 绝大部分 对不存在 key 的请求,保护后端数据源
缺点:
- 存在一定的 误判率 (False Positive Rate) :布隆过滤器判断“可能存在”的 key,实际上也可能不存在,这时请求仍然会穿透到缓存和数据库(但这种情况比没有过滤器好很多)。它不会误判“不存在”(False Negative)。(误判率可以通过参数设置,一般 5% 以内,占用空间越大,误判率越小)
- 实现相对复杂一些,需要维护布隆过滤器的数据(例如,数据新增时需要同步更新过滤器)
- 布隆过滤器 不支持删除 元素(标准实现)。如果数据删除,过滤器无法同步移除,可能导致后续查询仍认为它“可能存在”
📒 布隆过滤器
工作原理
布隆过滤器的基本原理如下:
- 创建一个m位的位数组,初始值都为0
- 选择k个不同的哈希函数,每个函数可以将元素映射到位数组中的某一位
- 添加元素时,用k个哈希函数计算出k个位置,并将这些位置的比特位设为1
- 查询元素时,同样用k个哈希函数计算出k个位置,如果这些位置的值都为1,则认为元素可能存在;如果有任意一个位为0,则元素一定不存在
优点
- 空间效率高:相比于存储元素本身,布隆过滤器只需要很小的空间
- 查询速度快:O(k)的时间复杂度,k为哈希函数个数
- 没有假阴性:如果布隆过滤器说元素不存在,那它一定不存在
缺点
- 有假阳性:当布隆过滤器说元素存在时,实际上元素可能不存在(有一定的误判率)
- 不能删除元素:标准的布隆过滤器不支持删除操作
- 不能获取已存储元素:无法从布隆过滤器中获取已添加的元素列表
应用场景
- 大数据集合的快速查询:如爬虫URL去重
- 缓存穿透问题:防止查询不存在的数据导致数据库压力过大
- 垃圾邮件过滤:快速判断邮件是否为已知的垃圾邮件
- 黑名单过滤:如网站访问IP黑名单检查
- 分布式系统的去重:如在分布式爬虫中进行URL去重
参数选择
位数组大小(m)和哈希函数个数(k)的选择:
- 当已知预期元素数量n和期望的误判率p时:
- 最优位数组大小:m = -n * ln(p) / (ln(2)^2)
- 最优哈希函数个数:k = ln(2) * m / n
布隆过滤器的变种
- 计数布隆过滤器:支持删除操作,用计数器替代位数组中的单个比特位
- 稳定布隆过滤器:在有限空间内处理流式数据
- 可扩展布隆过滤器:支持动态调整大小
布隆过滤器因其高效的空间使用率和快速的查询能力,在大数据、分布式系统、缓存系统等领域有着广泛的应用。
解决-接口层校验
方案描述: 在应用接口层(如 Controller、API 网关)对请求参数进行校验。例如,用户 ID 必须是正整数、订单号必须符合特定格式等。对于不符合基本规则的请求,可以直接拦截,避免后续的缓存和数据库查询
优点:
- 实现简单,在请求入口处即可拦截非法请求
- 可以根据业务规则进行定制,非常灵活
- 几乎没有额外的存储开销
缺点:
- 只能拦截 格式明显不符 的非法请求。对于那些格式合法但数据库中确实不存在的 key(例如查询一个 ID 为 9999999 但实际不存在的用户),这种方法无效
- 适用范围有限,通常作为一种辅助手段
总结:
接口层校验 作为第一道防线,可以拦截明显非法的请求,但无法解决根本问题
缓存空值 简单易行,适合应对少量、重复的不存在 key 查询,但对大量不同 key 的攻击效果有限且有数据一致性风险
布隆过滤器 效果最好,能大幅减少无效查询,但实现稍复杂且有误判率和不支持删除的问题。它是应对缓存穿透的主流方案之一
在实际应用中,通常会将这些方案结合使用,比如同时使用接口层校验和布隆过滤器,或者接口层校验和缓存空值
缓存击穿
缓存击穿 指的是某一个热点 Key 在缓存中正好失效的瞬间,同时有大量并发请求访问这个 Key
由于缓存已失效,这些并发请求会全部穿透缓存,直接打到后端的数据库上,导致数据库压力瞬间剧增,甚至可能宕机
这和缓存穿透(查询不存在的数据)以及缓存雪崩(大量 Key 同时失效)有所不同,缓存击穿特指单个热点 Key 失效引发的问题
常见的解决方案有:
使用互斥锁 (Mutex Lock) / 分布式锁 (Distributed Lock)
方案描述: 当缓存未命中时(特别是针对热点 Key),并不是让所有请求都去查询数据库。而是先尝试获取一个锁(单机环境用
synchronized
或ReentrantLock
,分布式环境用 Redis 的SETNX
、Redisson 或 ZooKeeper 实现的分布式锁)- 只有成功获取到锁的那个请求(线程)才去查询数据库,并将查询结果写回缓存
- 其他未获取到锁的请求则可以选择等待一小段时间后重试(再次访问缓存),或者直接返回一个默认值/稍旧的数据(如果业务允许)
- 查询数据库并写回缓存的线程,在操作完成后需要释放锁
优点:
- 强一致性保证: 只允许一个请求访问数据库重建缓存,有效防止了大量请求同时冲击数据库
- 思路清晰,实现相对直接
缺点:
- 性能开销: 需要引入锁机制,无论是本地锁还是分布式锁,都会带来额外的性能开销和复杂度
- 阻塞等待: 未获取到锁的线程需要等待,增加了请求的响应时间。如果获取锁的线程执行数据库查询和缓存写入操作较慢,会影响吞吐量
- 死锁风险: 如果锁实现不当,可能存在死锁风险(尤其在分布式锁场景下,需要考虑锁超时、续约等问题)
image
设置"永不过期" / 逻辑过期 (Logical Expiration)
方案描述: 对于热点 Key,不设置物理上的过期时间 (TTL),或者设置一个非常长的 TTL。而是在缓存值中额外存储一个逻辑过期时间字段
当请求访问缓存时,获取到数据后,判断其逻辑过期时间是否已到
如果未过期: 直接返回数据
如果已逻辑过期:
- 不立即删除缓存
- 尝试获取一个锁(可以是轻量级的,甚至是非阻塞的尝试锁)
- 如果获取锁成功: 开启一个异步线程去执行数据库查询,更新缓存中的数据和新的逻辑过期时间。当前请求则仍然返回旧的(逻辑过期的)数据
- 如果获取锁失败: 说明已有其他线程在负责更新缓存,当前请求也直接返回旧的(逻辑过期的)数据
优点:
- 高可用性: 通过返回旧数据,避免了请求的长时间等待,保证了服务的可用性,用户体验较好
- 避免数据库冲击: 异步更新机制使得只有一个线程(或少数几个线程)会去访问数据库,有效保护了数据库
- 无锁等待: 大部分请求读取缓存时无需等待锁
缺点:
- 数据不一致: 在逻辑过期到缓存被异步更新完成的这段时间内,返回的是旧数据,存在短暂的数据不一致性。业务需要能容忍这种不一致
- 实现复杂度: 需要在缓存结构中增加逻辑时间戳,并实现异步刷新缓存的机制(如线程池、消息队列等),增加了系统的复杂度
- 需要额外的代码逻辑来处理“逻辑过期”的判断和异步更新的发起
image
总结:
- 互斥锁/分布式锁 方案更侧重于数据一致性,但会牺牲一定的可用性(等待)和性能(锁开销)
- 逻辑过期 方案更侧重于高可用性,能有效避免用户等待,但需要接受一定程度的数据不一致性,并且实现相对复杂
缓存雪崩
缓存雪崩 (Cache Avalanche) 是指在某个时间段内,缓存系统 大面积不可用 或者 大量缓存 Key 在同一时间集中过期失效,导致所有或大部分的请求都无法命中缓存,瞬间将巨大的流量直接打到后端的数据库上,造成数据库压力剧增,响应变慢,甚至宕机,从而引起整个系统应用的连锁反应,如同雪崩一样崩溃
缓存雪崩主要有两种触发情况:
- 缓存服务宕机或网络故障: 缓存中间件(如 Redis 集群)整体发生故障,无法提供服务
- 大量 Key 同时过期: 比如在系统启动预热时,将大量数据同时加载到缓存并设置了相同的过期时间;或者某些热点数据设置了相同的过期时间,在某一刻同时失效
针对缓存雪崩,常见的解决方案有以下几种:
过期时间添加随机值 (Randomize Expiration Time)
方案描述: 在设置缓存 Key 的过期时间 TTL 时,不是设定一个固定的值,而是在基础 TTL 上增加一个随机的时间偏移量(例如,
TTL = BaseTTL + random(0, 300)
秒)。这样可以避免大量 Key 在完全相同的时间点过期,将过期时间分散到一段时间内优点:
- 实现简单,对原有代码改动小
- 能有效避免因 Key 集中过期导致的雪崩
- 对缓存性能几乎无影响
缺点:
- 只是将过期时间点打散,并不能完全避免在某个较短时间窗口内仍有较多 Key 过期的情况(但已大大缓解)
- 无法解决因缓存服务本身宕机导致的雪崩
构建高可用的缓存集群 (High Availability Cache Cluster)
方案描述: 使用缓存中间件自身提供的高可用方案,如 Redis Sentinel(哨兵模式,主从复制+自动故障转移)或 Redis Cluster(分布式集群,数据分片+主从复制)。确保即使部分缓存节点宕机,整个缓存服务仍然可用
优点:
- 从根本上提高了缓存服务的可用性,能有效防止因单点故障或部分节点故障导致的雪崩
- 是生产环境中标准的缓存部署方式
缺点:
- 部署和维护复杂度增加,需要更多的硬件资源
- 无法解决因大量 Key 同时过期导致的雪崩
- 如果整个集群(例如所有主节点)都出现问题,仍然会发生雪崩
服务降级与熔断 (Service Degradation and Circuit Breaking)
方案描述: 在应用层面加入降级和熔断机制
- 降级: 当检测到缓存失效且数据库压力过大时,暂时牺牲一部分非核心功能或数据,让请求直接返回一个预设的默认值、静态页面、或者一个友好的错误提示,保证核心服务的可用性
- 熔断: 设置访问数据库的超时时间和错误率阈值。当请求数据库的失败率或超时次数达到阈值时,暂时“熔断”对数据库的访问,后续一段时间内的请求不再访问数据库,直接快速失败或执行降级逻辑,防止数据库被彻底压垮。待一段时间后(或半开启状态探测成功后)再恢复对数据库的调用。可以使用 Hystrix、Sentinel、Resilience4j 等熔断组件
- 限流 (Rate Limiting): 在网关层或应用层限制访问数据库的并发数或速率,避免瞬间涌入过多请求
优点:
- 是保护后端数据源(如数据库)的最后一道防线,无论雪崩原因是什么,都能在一定程度上保护下游系统
- 提高了系统的整体健壮性和容错能力
缺点:
- 会影响用户体验,因为部分请求会被拒绝或返回非实时/非精确数据
- 实现相对复杂,需要引入额外的组件和配置,并仔细调整降级策略和熔断阈值
添加多级缓存
- Guava 或 Caffeine,redis 作为二级缓存
缓存预热与后台续期 (Cache Warm-up and Background Refresh)
方案描述:
- 预热: 系统启动时,提前将热点数据加载到缓存中,并设置不同的过期时间(结合随机值)
- 后台续期/逻辑过期: 类似于解决缓存击穿的逻辑过期方案,对于热点数据不设置物理过期时间或设置很长,通过后台线程检测并异步更新,避免集中失效
优点:
- 预热可以减少系统启动初期的缓存穿透
- 后台续期可以避免热点 Key 集中失效,减少对数据库的冲击
缺点:
- 预热需要识别热点数据,并可能增加启动时间
- 后台续期实现复杂,可能存在短暂数据不一致
总结:
- 随机化过期时间 是最简单有效的防止 Key 集中过期的手段
- 高可用缓存集群 是保障缓存服务自身稳定性的基础
- 服务降级与熔断/限流 是保护后端系统、提高整体容错性的重要补充手段
- 缓存预热与后台续期 可以作为优化手段,减少冷启动和热点失效的影响
在实际生产环境中,通常会将以上多种方案组合使用,例如:部署高可用的 Redis Cluster + 给 Key 设置随机过期时间 + 在应用层配置合理的熔断、降级和限流策略,以最大限度地防止和减轻缓存雪崩带来的影响
如何构建高可用的缓存集群(llm)
好的,面试官您好。构建高可用的缓存集群主要是为了消除单点故障,确保即使部分缓存节点出现问题,整个缓存服务依然能够持续对外提供服务,从而有效防止因缓存服务宕机引发的缓存雪崩。以业界最常用的 Redis 为例,主要有两种主流的高可用集群方案:Redis Sentinel (哨兵模式) 和 Redis Cluster (集群模式)
1. Redis Sentinel (哨兵模式)
架构:
- 由一个 主节点 (Master) 和一个或多个 从节点 (Slave) 组成
- 写入操作只在主节点进行,然后主节点将数据变更同步给所有从节点(主从复制)
- 读取操作可以在主节点或从节点进行(取决于配置,可以分担读压力)
- 独立运行一组 哨兵 (Sentinel) 进程(通常建议至少 3 个哨兵节点以形成多数派,避免脑裂)。哨兵负责监控主从节点的状态
工作原理与高可用机制:
监控 (Monitoring): 哨兵进程会定期向所有主从节点发送 PING 命令,检测它们是否在线
故障检测 (Failure Detection): 如果一个主节点在规定时间内没有响应哨兵的 PING 命令(主观下线),哨兵会询问其他哨兵节点对该主节点的看法。如果足够数量的哨兵(达到法定数量 Quorum)都认为该主节点下线了,则标记其为客观下线
自动故障转移 (Automatic Failover):
- 当主节点被确认客观下线后,哨兵们会选举出一个领头哨兵 (Leader Sentinel)
- 领头哨兵负责执行故障转移:从该主节点下属的从节点中,按照一定规则(如优先级、复制偏移量、运行 ID 等)挑选一个最优的从节点
- 领头哨兵向被选中的从节点发送
SLAVEOF no one
命令,使其升级为新的主节点 - 然后,领头哨兵通知其他从节点,让它们转而复制新的主节点 (
SLAVEOF new_master_ip new_master_port
) - 同时,哨兵会更新内部记录的主节点信息
配置提供者 (Configuration Provider): 客户端连接 Redis 时,不是直接连接主节点地址,而是连接哨兵集群。哨兵会告知客户端当前主节点的地址。当发生故障转移后,哨兵会通知客户端新的主节点地址
优点:
- 实现了主节点的自动故障检测和转移,提高了可用性
- 架构相对 Redis Cluster 简单一些,易于理解和部署
- 客户端可以通过哨兵获取最新的主节点信息,对故障转移相对透明
缺点:
- 写性能瓶颈: 只有一个主节点处理写请求,写操作的吞吐量受限于单个主节点的性能
- 内存容量瓶颈: 整个数据集必须能存储在单个主节点的内存中
- 故障转移有短暂中断: 从主节点宕机到哨兵完成选举、提升新主、通知客户端,期间会有一小段时间服务不可用(通常是秒级)
- 哨兵本身也需要保证高可用,需要部署多个哨兵实例
2. Redis Cluster (集群模式)
架构:
- 采用去中心化的设计,没有像 Sentinel 那样的中心协调节点
- 数据通过分片 (Sharding) 的方式存储在多个主节点上。Redis Cluster 默认将整个数据集划分为 16384 个哈希槽 (Hash Slot)
- 每个主节点负责处理一部分哈希槽。例如,3 个主节点的集群,可能节点 A 负责 0-5460,节点 B 负责 5461-10922,节点 C 负责 10923-16383
- 每个主节点可以配置一个或多个从节点,用于备份主节点的数据和在主节点故障时进行替换(主从复制)
- 节点之间通过 Gossip 协议相互通信,交换状态信息(如节点存活、槽位分布等)
工作原理与高可用机制:
数据路由: 客户端连接集群中的任意一个节点。当客户端要操作某个 Key 时,会根据
CRC16(key) % 16384
计算出该 Key 属于哪个哈希槽- 如果该槽正好由当前连接的节点负责,则直接处理
- 如果该槽不由当前节点负责,节点会返回一个
MOVED
重定向错误,告诉客户端这个槽由哪个节点负责 (MOVED slot_number target_node_ip:port
) - Cluster-aware 的客户端(如 Jedis Cluster, Redisson)会自动处理
MOVED
指令,重新向正确的节点发送请求,并可能缓存槽位映射关系以优化后续请求
故障检测: 节点间通过 Gossip 协议 PING/PONG 消息检测其他节点状态。如果一个节点发现另一个节点长时间失联(
PFAIL
状态),它会通过 Gossip 协议询问其他节点,如果多数主节点都认为目标节点失联,则标记其为FAIL
状态(客观下线)自动故障转移:
- 当一个主节点被标记为
FAIL
后,其下属的从节点会等待一小段时间(与主节点失联时间、自身数据复制偏移量有关),然后尝试发起故障转移选举 - 它会向集群中其他所有主节点发送请求,请求投票
- 如果一个从节点获得了超过半数主节点的投票,它就赢得选举,升级为新的主节点
- 新主节点接管原主节点负责的哈希槽,并通过 Gossip 协议通知集群其他节点更新槽位信息
- 当一个主节点被标记为
优点:
- 水平扩展能力强: 可以通过增加主节点来扩展集群的存储容量和写吞吐量,突破单机瓶颈
- 高可用: 去中心化架构,部分主节点宕机(及其从节点也宕机)不会影响其他主节点负责的槽位的服务。每个分片内部实现了自动故障转移
- 分布式: 数据分散存储,负载更均衡
缺点:
- 实现更复杂: 部署、运维相对 Sentinel 更复杂
- 客户端要求: 客户端需要支持 Redis Cluster 协议(能处理
MOVED
和ASK
重定向) - 批量操作限制: 涉及多个 Key 的操作(如 MSET, MGET, 事务, Lua 脚本)如果这些 Key 分布在不同的槽位(即不同的主节点),则无法直接执行或需要特殊处理(如按节点分组执行),增加了使用的复杂性
- Gossip 协议信息交换有一定延迟
总结与选择:
- Redis Sentinel: 适合数据量不大(能放在单机内存)、写压力不是极端高的场景,对部署和运维复杂度要求相对较低的情况。它提供了基础的主从复制和自动故障转移能力
- Redis Cluster: 适合数据量巨大、需要水平扩展存储和写性能的场景。它提供了更彻底的分布式和高可用方案,但复杂性也更高
在构建高可用缓存集群时,除了选择合适的 Redis 模式,还需要考虑:
- 网络稳定性: 节点间的网络通信是高可用的基础
- 硬件资源: 足够的内存、CPU 和网络带宽
- 客户端配置: 客户端需要正确配置连接哨兵或集群节点,并能处理故障转移
- 监控与告警: 完善的监控体系能及时发现集群问题
- 云服务: 如果使用云服务商(如 AWS ElastiCache, Azure Cache for Redis, GCP Memorystore),它们通常提供了封装好的高可用选项,可以简化部署和管理
双写一致性(mysql 的数据如何与 redis 进行同步)
前提:介绍业务背景
一致性要求高 (Consistency, CP)
允许延迟一致 (Availability, AP)
双写一致性:指的是在更新数据时,同时更新数据库(如 MySQL)和缓存(如 Redis)这两个数据源,并保证它们之间数据状态最终一致的问题。这是一个在分布式系统中非常常见且重要的问题,因为直接进行双写操作很容易因为各种原因(如网络延迟、某个系统临时故障、并发冲突等)导致数据不一致
延迟双删

- 读操作:缓存命中,直接返回;缓存未命中查询数据库,写入缓存,设定 TTL
- 写操作:延迟双删

先删除缓存,再操作数据库
数据库值为 10
- 正常情况

- 特殊情况

先修改数据库,在删除缓存
- 正常情况

- 特殊情况

所以要删除两次缓存,减少脏数据出现的风险
为什么要延迟删除:因为数据库是主从模式,读写分离,需要等待主节点同步数据到从节点。但延迟时间不好控制,还是有脏数据的风险,做不到强一致性
互斥锁/分布式锁
使用读写锁,读加共享锁,写加排他锁
强一致
性能低

异步通知保证数据的最终一致性
消息队列

Canal

持久化
RDB (Redis Database Backup file)
Redis 数据备份文件,也叫 Redis 数据快照。把内存中的所有数据都记录到磁盘中。当Redis实例故障重启后,从磁盘读取快照文件,恢复数据
手动执行
redis-cli
SAVE
或 BGSAVE
(SAVE
会影响主进程, BGSAVE
创建子进程执行,对主进程的影响可以忽略不计)
自动执行
在redis.conf
配置文件中
- Redis 在指定的时间间隔内,将内存中的数据集快照写入磁盘的二进制文件(默认为 dump.rdb)
- 可以通过配置
save
参数来自动触发 RDB 快照,如save 900 1
(900秒内有至少1个键被修改)
# 自动保存条件
save 900 1 # 900秒内有1个键被更改则触发保存
save 300 10 # 300秒内有10个键被更改则触发保存
save 60 10000 # 60秒内有10000个键被更改则触发保存
# RDB文件名
dbfilename dump.rdb
# RDB文件保存路径
dir ./
执行原理
BGSAVE
的子进程通过拷贝页表来获得和主进程相同的内存空间映射

如何避免脏数据:
fork采用的是copy-on-write
技术:
当主进程执行读操作时,访问共享内存
当主进程执行写操作时,则会拷贝一份数据,执行写操作

AOF (Append Only File)
AOF 持久化记录服务器执行的所有写操作命令,并在服务器启动时通过重新执行这些命令来恢复数据
默认关闭,需要修改redis.conf
配置文件开启
# 是否开启AOF功能,默认是no
appendonly yes
#AOF文件的名称
appendfilename "appendonly.aof"
# AOF 记录频率
# 表示每执行一次写命令,立即记录到AOF文件
appendfsync always
# 写命令执行完先放入AOF缓冲区,然后表示每隔1秒将缓冲区数据写到AOF文件,是默认方案
appendfsync everysec
# 写命令执行完先放入AOF缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
appendfsync no
配置项 | 刷盘时机 | 优点 | 缺点 |
---|---|---|---|
Always | 同步刷盘 | 可靠性高,几乎不丢数据 | 性能影响大 |
everysec(默认) | 每秒刷盘 | 性能适中 | 最多丢失1秒数据 |
no | 操作系统控制 | 性能最好 | 可靠性较差,可能丢失大量数据 |
通过执行bgrewriteaof
命令,可以让AOF文件执行重写功能,用最少的命令达到相同效果

Redis也会在触发阈值时自动去重写AOF文件。阈值也可以在redis.conf
中配置:
# AOF重写触发条件
auto-aof-rewrite-percentage 100 # AOF文件增长率
auto-aof-rewrite-min-size 64mb # AOF文件最小体积触发重写
特性 | RDB | AOF |
---|---|---|
持久化方式 | 定时对整个内存做快照 | 记录每一次执行的命令 |
数据完整性 | 不完整,两次备份之间会丢失 | 相对完整,取决于刷盘策略 |
文件大小 | 会有压缩,文件体积小 | 记录命令,文件体积很大 |
启动恢复速度 | 很快 | 慢 |
数据恢复优先级 | 低,因为数据完整性不如AOF | 高,因为数据完整性更高 |
系统资源占用 | 高,大量CPU和内存消耗 | 低,主要是磁盘IO资源, 但AOF重写时会占用大量CPU和内存资源 |
使用场景 | 可以容忍数分钟的数据丢失,追求更快的启动速度 | 对数据安全性要求较高常见 |
混合持久化(Redis 4.0+)
从 Redis 4.0 开始,Redis 提供了 RDB 和 AOF 的混合持久化模式
工作原理:
- 在 AOF 重写时,不再单纯使用 AOF 命令记录,而是将重写时的内存数据以 RDB 格式写在 AOF 文件开头
- 重写之后的增量命令继续以 AOF 方式追加到文件末尾
优点:
- 结合了 RDB 的快速加载和 AOF 的高安全性
- 启动时可以先加载 RDB 部分,然后执行剩余的 AOF 命令
# 启用混合持久化(Redis 4.0+)
aof-use-rdb-preamble yes
实际应用中的选择
只用 RDB:
- 对数据丢失不敏感的场景(如纯缓存使用)
- 需要高性能,能接受一定程度的数据丢失
只用 AOF:
- 数据安全性要求高的场景
- 写操作频繁但可以接受稍低的性能
混合使用(推荐):
- 在 Redis 4.0+ 版本中,启用混合持久化是一种较好的平衡:
# redis.conf
aof-use-rdb-preamble yes
不启用持久化:
- 将 Redis 严格用作缓存,不关心数据丢失
- 通过主从复制和集群实现高可用,而不依赖持久化
数据过期(删除)策略
Redis的过期删除策略:惰性删除+定期删除两种策略进行配合使用
Redis 内部使用一个字典(dict)记录所有设置了过期时间的键,用以定期检查过期情况
如何设置过期时间:
EXPIRE key seconds # 设置key在seconds秒后过期
EXPIREAT key timestamp # 设置key在指定的Unix时间戳过期
PEXPIRE key milliseconds # 以毫秒为单位设置过期时间
TTL key # 查看key剩余的过期时间(秒)
PERSIST key # 移除key的过期时间
惰性删除(Lazy Deletion)
工作原理:
- Redis 不主动删除过期的键
- 仅在访问键时检查其是否过期
- 如果过期,则删除并返回空;未过期则正常返回
优点:
- 节省 CPU 资源,只在必要时才检查过期
- 实现简单
缺点:
- 如果大量过期键未被访问,会一直占用内存空间
定期删除(Periodic Deletion)
工作原理:
Redis 会定期(默认每 100ms)随机抽样一部分设置了过期时间的键
删除其中已过期的键
具体流程:
- 随机测试 20 个带过期时间的键
- 删除所有发现的已过期键
- 如果已过期键比例超过 25%,重复步骤 1
两种模式:
SLOW模式是定时任务,执行频率默认为
10hz
,每次不超过25ms
,以通过修改配置文件redis.conf
的hz选项来调整这个次数FAST模式执行频率不固定,但两次间隔不低于
2ms
,每次耗时不超过1ms
优点:
- 通过限制删除操作的时长和频率,减少对 CPU 的影响
- 弥补惰性删除可能导致的内存浪费
缺点:
- 难以确定删除操作执行的频率和时长
- 仍可能存在未被抽中的过期键占用内存
数据淘汰策略/内存淘汰策略(Maxmemory Policies)
当 Redis 内存使用达到 maxmemory
配置的限制时,Redis 会根据 maxmemory-policy
配置使用以下策略选择要删除的键:
1. 处理所有键的策略
- noeviction:不删除任何数据,拒绝写入操作并返回错误(默认策略)
- allkeys-lru:使用 LRU 算法删除最近最少使用的键
- allkeys-lfu:使用 LFU 算法删除访问频率最少的键(Redis 4.0+)
- allkeys-random:随机删除任意键
2. 仅处理过期键的策略
- volatile-lru:使用 LRU 算法删除已设置过期时间的最近最少使用的键
- volatile-lfu:使用 LFU 算法删除已设置过期时间的访问频率最少的键(Redis 4.0+)
- volatile-random:随机删除已设置过期时间的键
- volatile-ttl:删除剩余过期时间最短的键(即最快要过期的键)
LRU 与 LFU 算法
1. LRU(Least Recently Used)
- 选择最近最少使用的键进行删除
- Redis 采用的是近似 LRU 算法:通过随机采样一小部分键,然后淘汰其中最久未使用的
2. LFU(Least Frequently Used)
选择访问频率最低的键进行删除
Redis 4.0+ 引入,比 LRU 更精准地反映键的热度
包含两个参数控制:
-
lfu-decay-time
:访问频率计数器的衰减时间 -
lfu-log-factor
:计数器对数增长的因子
-
实际应用建议
通用场景:
volatile-lru
是一个安全的默认选择,只淘汰带有过期时间的最少使用键纯缓存场景:
allkeys-lru
或allkeys-lfu
是较好的选择,能最大化缓存命中率对过期键要求严格场景:
volatile-ttl
确保最早过期的键先被删除高可用场景:
noeviction
可以避免意外删除重要数据,但需要确保内存足够混合数据类型场景:Redis 4.0+ 中,
volatile-lfu
或allkeys-lfu
通常比 LRU 策略表现更好使用内存占用检测命令:
-
INFO memory
查看内存使用情况 -
MEMORY USAGE key
查看单个键占用的内存
-
配置示例:
# 设置内存上限为2GB
maxmemory 2gb
# 设置内存淘汰策略
maxmemory-policy volatile-lru
问题 1
数据库有1000万数据,Redis只能缓存20w数据,如何保证Redis中的数据都是热点数据?
使用allkeys-lru(挑选最近最少使用的数据淘汰)淘汰策略,留下来的都是经常访问的热点数据
问题 2
Redis的内存用完了会发生什么?
主要看数据淘汰策略是什么?如果是默认的配置(noeviction),会直接报错
分布式锁
使用场景
抢券
无锁场景

单机加锁

集群分布式锁

实现原理
Redis 实现分布式锁主要利用Redis的setnx
命令
setnx
setnx
是SET if not exists
(如果不存在,则SET)的简写
获取锁:
# 添加锁,NX是互斥,EX是设置超时时间
# 一条命令保证原子性
SET lock value NX EX 10
释放锁:
# 释放锁,删除即可
DEL key
不设置有效期可能会死锁!!!
redisson
底层使用 lua 脚本

实现的是可重入的分布式锁(根据线程 id 标识作为判断依据、同 java 可重入锁)
使用 hash 结构记录线程 id 和重入次数

redisson 分布式锁的主从一致性
主节点宕机后,从节点作为新的主节点,但是之前锁的数据丢失了,导致两个线程同时获得锁

RedLock(红锁):不能只在一个redis实例上创建锁,应该在多个redis实例上创建锁(n/2+1)
,避免在一
个redis实例上加锁(实现复杂、性能差、运维繁琐、不建议使用)

如果非要保证数据强一致性(cp思想)建议使用 zookeeper
集群方案
单节点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,就需要搭建主从集群,实现读写分离
一般都是一主多从,主节点负责写数据,从节点负责读数据
主从复制(Master-Slave Replication)
Replication Id(简称replid)
是数据集的标识符,ID相同即表明属于同一数据集。每个master节点都拥有唯一的replid,而slave节点则会继承master节点的replid
Offset(偏移量)
随着记录在repl baklog中的数据量逐渐增多而逐步增加。slave节点在完成同步时,也会记录当前同步的offset。若slave的offset小于master的offset,则表明slave的数据落后于master,需要进行更新
主从全量同步


id 一致,就不会生成 rdb 文件,而是只使用 baklog
主从增量同步(slave 重启或者后期数据变化)


哨兵模式(Sentinel)
Redis提供了哨兵(Sentinel)机制来实现主从集群的自动故障恢复
一般部署至少 3 台哨兵
服务状态监控
监控:Sentinel会不断检查您的master和slave是否按预期工作
自动故障恢复:如果master故障,Sentinels会将一个slave提升为master。当故障实例恢复后也以新的master为主
通知:Sentinel充当Redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给Redis的客户端

Sentine系统依托心跳机制对服务状态进行监测,每秒向集群内各实例发送ping命令:
- 主观下线:若某Sentinel节点发现某实例未在规定时间内响应,则判定该实例为主观下线
- 客观下线:当超过指定数量(quorum)的Sentinel节点均认为该实例为主观下线时,则该实例被视为客观下线。建议quorum值应超过Sentinel实例数量的一半

哨兵选主规则
首先,需评估主从节点断开时间的长短,若超过既定阈值,则淘汰该从节点
其次,根据从节点的slave-priority
值确定优先级,数值越低,优先级越高
若slave-priority
值相同,则依据从节点的offset
值排序,数值越大,优先级越高(数据最完整)
最终,比较从节点的运行ID
大小,数值越小,优先级越高
脑裂
由于网路问题,哨兵无法访问主节点
哨兵会选择从节点作为新的主节点
但是客户端仍然连接主节点向主节点写入数据
网络恢复后,旧的主节点会作为新主节点的从节点,并清空数据与主节点做同步
但这时就造成了数据的丢失

通过两个配置来避免:
min-replicas-to-write 1
表示最少的slave节点为1个(主节点必须要有这么多从节点才能接受数据,否则直接拒绝请求)
min-replicas-max-lag 5
表示数据复制和同步的延迟不能超过5秒
❓怎么保证 Redis 的高并发高可用
哨兵模式:实现主从集群的自动故障恢复(监控、自动故障恢复、通知)
❓使用的 redis 是单点还是集群,哪种集群
主从(1主1从)+哨兵就可以了。单节点不超过10G内存,如果Redis内存不足则可以给不同服务分配独立的Redis主从节点
❓redis 集群脑裂,如何解决
集群脑裂现象,源于主节点、从节点与sentinel分属不同网络分区,导致sentinel未能感知到主节点的心跳。为此,系统通过选举机制将某一从节点提升为主节点,从而形成两个master节点,如同大脑分裂。此状况将导致客户端持续向旧的主节点写入数据,而新节点因无法同步数据而陷入停滞。网络恢复后,sentinel会将旧主节点降级为从节点,进而从新master节点同步数据,极易引发数据丢失
解决方案:可通过调整Redis配置,设定最低从节点数量及缩短主从数据同步的延迟时间。若条件不满足,则拒绝请求,以此减少数据丢失的风险
分片集群 Redis Cluster(官方集群方案)
主从与哨兵机制能够有效应对高可用、高并发读的需求。然而,仍存在以下两个尚未解决的问题:
- 海量数据存储问题
- 高并发写问题
运用分片集群技术,可解决上述难题。分片集群具备以下特点:
集群中包含多个主节点(master),每个主节点存储着不同的数据
每个主节点均可配备多个从节点(slave)
主节点间通过ping命令监控彼此的健康状况
客户端请求可访问集群中的任意节点,请求最终都会被导向至相应的正确节点
数据读写
Redis分片集群引入了哈希槽这一概念。该集群共设有16384个哈希槽。每个键值通过CRC16校验,并对此16384进行取模运算,以确定其应放置的槽位。集群中的每个节点负责管理一部分哈希槽

❓redis 分片集群的作用
集群内分布着多个Master
节点,每个Master
负责存储独特的数据。
每个Master
节点均可配备多个Slave
节点。
Master
节点间通过ping
命令监测彼此的健康状况。
客户端的请求能够访问集群中的任何节点,并最终被导向至正确的处理节点
❓redis 分片集群中数据是怎么存储和读取的
Redis分片集群引入了哈希槽的概念,Redis集群包含16384个哈希槽。将16384个插槽分配至不同的实例。读写数据时,依据key的有效部分计算哈希值,对16384取余。有效部分,若key前有大括号,则大括号内的内容即为有效部分;若没有,则以key本身作为有效部分。余数作为插槽,进而寻找插槽所在的实例
❓redis 是单线程的,但是为什么还是那么快
Redis 从 6.0 版本开始引入了多线程机制,但这是一种有限的多线程实现,主要用于处理网络 IO 操作,而非全面的多线程改造
Redis 多线程的实现范围
Redis 6.0+ 的多线程实现有以下特点:
仅用于网络 IO 处理
- 多线程仅处理客户端交互的 socket 读写和协议解析
- 命令执行依然是单线程的,核心数据结构的操作仍在主线程中进行
保留单线程执行命令的模式
- 这避免了复杂的锁机制和同步问题
- 保持了 Redis 简单高效的特性,不引入并发数据访问的复杂性
为什么采用这种有限的多线程设计?
解决网络 IO 瓶颈
- 在高并发、高负载场景下,网络 IO 常成为性能瓶颈
- 多线程处理网络 IO 可以充分利用多核 CPU 资源
避免锁竞争的复杂性
- 命令执行保持单线程,不需要处理复杂的锁机制
- 减少了线程切换和锁竞争带来的开销
兼顾性能与简单性
- 这种设计在提升性能的同时,保持了代码的简洁和可维护性
- 减少了引入全面多线程可能带来的 bug 和复杂性
多线程配置方法
Redis 6.0+ 的多线程默认是禁用的,需要在配置文件中启用:
Code
# 启用 IO 多线程 io-threads-do-reads yes # 设置 IO 线程数(建议设置为 CPU 核心数) io-threads 4
注意事项:
- 线程数不应超过可用的 CPU 核心数
- 线程数过多可能导致线程切换开销增大
- 小型实例(QPS < 10000)可能不需要启用多线程
Redis是纯内存操作,执行速度非常快
采用单线程,避免不必要的上下文切换可竞争条件,多线程还要考虑线程安全问题
使用I/O多路复用模型,非阻塞IO
❓解释一下 I/O多路复用模型
回答:
I/O多路复用,即通过单个线程同时监听多个Socket,并在某个Socket可读或可写时获得通知,以此避免无效等待,最大化利用CPU资源。当前,所有I/O多路复用均采用epoll模式实现。该模式在通知用户进程Socket就绪的同时,将已就绪的Socket直接写入用户空间,无需逐一遍历Socket以判断其是否就绪,从而提升了性能
Redis的网络模型正是通过结合I/O多路复用与事件处理器来应对多个Socket请求。例如,它提供了连接应答处理器、命令回复处理器以及命令请求处理器
自Redis 6.0版本起,为了进一步提升性能,在命令回复处理器中引入了多线程来处理回复事件。在命令请求处理器中,对命令的转换也采用了多线程,以加快命令转换速度。然而,在命令执行阶段,依然保持单线程模式
+----------------------------+
| 单线程(EventLoop) |
+------------+--------------+
|
epoll_wait/select/kqueue
|
+--------------+--------------+
| | |
客户端1 客户端2 客户端3 ...
fd1 fd2 fd3
Rdis采用纯内存操作,执行速度极为迅速。其性能瓶颈主要在于网络延迟,而非执行速度。I/O多路复用模型主要实现了高效的网络请求。用户空间与内核空间。常见的IO模型包括:
- 阻塞IO(Blocking IO)
- 非阻塞IO(Non-blocking IO)
- IO多路复用(IO Multiplexing)
- Redis网络模型





Redis 网络模型
瓶颈是网络 IO,6.0 之后加入了多线程
