以下是 Redis 技术场景压力面问题的详细解答,覆盖核心原理、策略对比与场景适配要点:
1. Redis 单线程支撑高并发的原因及单线程模型细节
-
单线程支撑高并发的核心原因:
-
基于内存操作:所有数据存于内存,读写速度远快于磁盘 IO,减少了线程阻塞的场景。
-
IO 多路复用 :采用
epoll/kqueue等 IO 多路复用技术,单线程可同时处理多个客户端连接的网络 IO,避免了多线程的上下文切换开销。 -
命令执行高效:Redis 命令大多是简单的内存操作,执行耗时极短,单线程能快速处理大量命令。
-
-
"单线程" 是否只用一个 CPU 核心:
❌ 并非如此。Redis 的核心命令执行线程是单线程,但持久化(RDB/AOF)、异步删除、集群同步等操作由独立的后台线程处理,会占用其他 CPU 核心;Redis 6.0 后还引入了IO 多线程,专门处理网络读写,进一步利用多核 CPU。
2. Redis 数据结构及底层实现
| 数据结构 | 底层实现 | 关键细节 |
|---|---|---|
| String | int (整数值)、embstr (短字符串,<44 字节)、raw(长字符串,≥44 字节) | embstr 是连续内存分配,减少内存碎片;raw 是分开的对象头和字符串存储 |
| Hash | ziplist (压缩列表,元素少且小)、hashtable(哈希表,元素多或大) | ziplist 切换为 hashtable 的条件:哈希元素数 > 512 或 单个元素值 > 64 字节(可配置) |
| List | ziplist (元素少且小)、quicklist(Redis 3.2 后,ziplist 组成的双向链表) | quicklist 结合了 ziplist 的内存紧凑和链表的高效增删特性 |
| Set | intset (整数集合,元素为整数且数量少)、hashtable(哈希表,非整数或数量多) | intset 切换为 hashtable 的条件:元素数 > 512 或 出现非整数值(可配置) |
| Zset | ziplist (元素少且小)、skiplist+hashtable(跳表 + 哈希表,元素多或大) | ziplist 存储时,member 和 score 紧挨存放;跳表保证范围查询高效,哈希表保证元素查找高效 |
| Bitmap | 基于 String 的位操作 | 按位存储,节省内存,适用于布尔型数据统计(如用户签到) |
| HyperLogLog | 基于基数估算算法 | 用极小内存统计海量数据的基数,存在一定误差(约 0.81%) |
| Geo | 基于 Zset 的 score 存储经纬度(geohash 编码) | 利用 Zset 的有序性实现地理位置的距离计算、范围查询 |
3. Redis 过期键删除策略及内存淘汰策略区别
-
过期键删除策略:
-
定时删除:创建定时器,键过期时立即删除。优点是内存释放及时;缺点是定时器占用大量 CPU 资源,影响 Redis 性能。
-
惰性删除:键过期后不主动删除,只有访问时才检查并删除。优点是 CPU 开销小;缺点是过期键长期不访问会导致内存泄漏。
-
定期删除 :Redis 每隔一段时间(默认 100ms)随机抽取部分过期键删除,并限制删除操作的耗时。平衡了定时删除和惰性删除的优缺点,是 Redis 的默认策略。
-
-
不用单一策略的原因:定时删除耗 CPU,惰性删除漏内存,定期删除能在 CPU 和内存之间取平衡。
-
内存淘汰策略 vs 过期删除策略:
-
过期删除策略:针对已设置过期时间的键,处理其过期后的删除逻辑。
-
内存淘汰策略:当 Redis 内存达到
maxmemory时,从 ** 所有键(含未过期键)** 中选择部分键删除,释放内存,核心是解决内存溢出问题(如volatile-lru、allkeys-lru等)。
-
4. Redis 持久化方式(RDB vs AOF)
| 特性 | RDB | AOF |
|---|---|---|
| 核心原理 | 定时将内存中的数据快照写入二进制文件 | 记录 Redis 的每一条写命令,重启时重放命令恢复数据 |
| 优点 | 文件体积小,恢复速度快;对性能影响小(子进程执行) | 数据安全性更高,可配置不同刷盘策略;日志文件易读懂 |
| 缺点 | 数据安全性低,两次快照间的数据易丢失;大数据集下,fork 子进程会阻塞主线程 | 文件体积大,恢复速度慢;写命令频繁时,AOF 重写会有性能开销 |
| 适用场景 | 适合做冷备份、数据恢复要求不高的场景;集群主从同步的基础 | 适合对数据安全性要求高(如金融场景)、需最小化数据丢失的场景 |
-
持久化方案选择:
-
追求性能和恢复速度:仅用 RDB。
-
追求数据安全性:仅用 AOF(配置
everysec刷盘)。 -
兼顾性能与安全性:开启 RDB+AOF(Redis 4.0 后支持混合持久化,AOF 文件中包含 RDB 快照和增量命令,兼顾体积与恢复速度)。
-
5. AOF 刷盘策略及重写机制
-
AOF 三种刷盘策略的性能与安全性权衡:
策略 刷盘时机 数据安全性 性能 always 每执行一条写命令,立即刷盘到磁盘 最高,仅丢失故障瞬间的命令 最低,磁盘 IO 频繁,性能损耗大 everysec 每秒刷盘一次 较高,最多丢失 1 秒的数据 适中,平衡安全性与性能,是默认策略 no 由操作系统决定刷盘时机 最低,可能丢失大量数据 最高,Redis 仅将命令写入缓冲区,不干预磁盘 IO -
AOF 重写的触发条件:
-
手动触发 :执行
bgrewriteaof命令。 -
自动触发:满足两个条件:
① 当前 AOF 文件大小超过
基准大小(初始为 AOF 创建时的大小)的指定百分比(默认 100%);
② 当前 AOF 文件大小超过
最小尺寸(默认 64MB)(可通过
auto-aof-rewrite-percentage和
auto-aof-rewrite-min-size配置)。
-
-
AOF 重写的核心逻辑:
Redis 创建子进程,遍历当前内存中的数据,将其转化为对应的写命令(如用
hmset替代多条
hset),生成新的 AOF 文件;新文件写入完成后,将重写期间的增量命令追加到新文件,最后替换旧 AOF 文件。重写后的 AOF 文件体积更小,命令更简洁,恢复速度更快。
6. Redis 启动加载持久化文件的逻辑
-
加载流程:
-
Redis 启动时,先检查是否有 AOF 文件,若有则优先加载 AOF 文件恢复数据;
-
若没有 AOF 文件,再加载 RDB 文件恢复数据;
-
加载过程中,Redis 会阻塞客户端请求,直到数据恢复完成。
-
-
优先级 :AOF 优先于 RDB,因为 AOF 的数据完整性更高。
-
加载失败的影响:
-
若 RDB/AOF 文件损坏,Redis 启动失败并打印错误日志,需修复或删除损坏的持久化文件;
-
若文件存在但数据格式异常,可能导致部分数据丢失,需结合备份文件恢复。
-
7. Redis 内存限制配置及淘汰策略
-
maxmemory 的作用:限制 Redis 使用的最大内存,防止 Redis 耗尽服务器内存导致系统 OOM 或 Redis 进程被杀死。
-
达到内存上限后的淘汰策略执行逻辑:
策略类型 核心逻辑 适用场景 LRU(最近最少使用) volatile-lru:从设置过期时间的键中淘汰最近最少使用的键;allkeys-lru:从所有键中淘汰最近最少使用的键绝大多数常规场景,兼顾热点数据留存 LFU(最不经常使用) volatile-lfu/allkeys-lfu:淘汰访问频率最低的键,比 LRU 更精准识别冷数据访问频率差异大的场景(如电商商品详情缓存) TTL(剩余过期时间) volatile-ttl:从设置过期时间的键中淘汰剩余 TTL 最短的键对数据过期时间敏感的场景(如限时优惠缓存) 随机淘汰 volatile-random/allkeys-random:随机淘汰键无明显热点数据的场景,仅作兜底 不淘汰(默认) noeviction:拒绝新的写命令,返回错误对数据完整性要求极高的场景(如金融交易)
8. Redis 缓存穿透、击穿、雪崩的场景与解决方案
| 问题类型 | 场景描述 | 核心解决方案 |
|---|---|---|
| 缓存穿透 | 请求不存在的 key,缓存和数据库均无数据,请求直击数据库 | 1. 缓存空值(设置短期过期时间);2. 布隆过滤器提前拦截不存在的 key;3. 接口层增加参数校验 |
| 缓存击穿 | 热点 key 突然过期,大量请求同时击中数据库 | 1. 热点 key 设置永不过期;2. 互斥锁(如 Redis 的 setnx),保证只有一个请求更新缓存;3. 热点数据预热 |
| 缓存雪崩 | 大量 key 同时过期,或 Redis 集群宕机,请求全部涌入数据库 | 1. 过期时间添加随机值,避免集中过期;2. Redis 集群做高可用(主从 + 哨兵 / 集群);3. 服务熔断 / 降级 / 限流;4. 多级缓存(本地缓存 + Redis) |
9. Redis 实现分布式锁
-
核心实现方式:
-
基础命令 :
SET key value EX seconds NX(原子性设置 key,不存在则创建,同时设置过期时间),替代setnx + expire(非原子操作,易出现死锁)。 -
释放锁:用 Lua 脚本原子性校验 value 并删除(防止误删其他客户端的锁),脚本逻辑:
if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end
-
-
核心命令作用:
-
setnx:key 不存在时设置,实现 "加锁",保证互斥性; -
set ex nx:原子性完成 "加锁 + 过期时间设置",避免死锁。
-
-
需考虑的问题:
-
死锁:设置过期时间兜底,业务执行超时则自动释放锁;
-
锁超时:若业务执行时间超过锁过期时间,可通过 "续期"(如 Redisson 的 watchdog 机制)延长锁时间;
-
主从切换:主节点加锁后宕机,从节点未同步锁数据,导致锁失效,可采用 Redlock 算法或 ZooKeeper 实现分布式锁。
-
10. Redlock 算法
-
核心逻辑:
-
向多个独立的 Redis 节点(至少 3 个)发送加锁请求(
SET key value EX nx); -
计算加锁成功的节点数,若成功数≥N/2+1(N 为节点数)且总耗时≤锁过期时间,判定加锁成功;
-
释放锁时,向所有节点发送释放请求。
-
-
解决的问题 :传统单 Redis 节点的分布式锁存在主从切换导致的锁失效问题,Redlock 通过多节点投票实现高可用。
-
生产是否推荐使用:
不推荐。原因:
-
Redis 的异步复制特性仍可能导致锁数据不一致;
-
算法对时钟同步要求高,节点时钟偏移会导致锁判断错误;
-
实现复杂,性能开销大,不如 ZooKeeper 的分布式锁可靠。
-
11. Redis 实现限流的方案
-
常见限流方案:
-
固定窗口计数 :用 Redis 的
incr统计单位时间内的请求数,超过阈值则限流; -
滑动窗口计数:用 Redis 的 Zset 存储请求时间戳,统计窗口内的请求数,剔除过期时间戳;
-
漏桶算法:用 Redis 模拟漏桶,控制请求的流出速率,平滑突发流量;
-
令牌桶算法:用 Redis 生成令牌,请求需获取令牌才能执行,支持突发流量。
-
-
漏桶 vs 令牌桶:
特性 漏桶算法 令牌桶算法 流量控制 严格控制流出速率,无法处理突发流量 可根据令牌生成速率和桶容量,处理合理的突发流量 核心逻辑 请求先进入桶中,桶按固定速率放水,桶满则拒绝请求 按固定速率生成令牌放入桶中,请求需获取令牌才能执行 -
Redis + Lua 实现高性能限流:将限流逻辑(如令牌桶 / 滑动窗口)写入 Lua 脚本,Redis 原子性执行脚本,减少网络往返,示例(固定窗口限流):
local key = KEYS[1] local limit = tonumber(ARGV[1]) local expire = tonumber(ARGV[2]) local current = tonumber(redis.call('incr', key)) if current == 1 then redis.call('expire', key, expire) end if current > limit then return 0 else return 1 end
12. Redis 中的 Lua 脚本
-
作用:
-
实现复杂的原子性操作,避免多命令执行的竞态条件;
-
减少客户端与 Redis 的网络交互次数,提升性能;
-
自定义扩展 Redis 的命令逻辑。
-
-
保证原子性的原因:Redis 执行 Lua 脚本时,会阻塞其他命令的执行,直到脚本运行完成,确保脚本内的所有命令作为一个整体执行,无中间中断。
-
性能注意事项:
-
避免编写耗时过长的脚本(如循环遍历大量数据),会阻塞 Redis 主线程;
-
脚本体积不宜过大,减少网络传输和解析开销;
-
避免在脚本中执行
keys *等耗时命令; -
利用 Redis 的脚本缓存(
script load),避免重复编译脚本。
-
13. Redis实现延迟队列:List+Brpoplpush、Sorted Set+Zrangebyscore
| 方案 | 核心实现 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| List + Brpoplpush | 将延迟任务存入List,用brpoplpush阻塞读取;需额外线程定时将到期任务移入消费List |
实现简单、原生命令支持、阻塞读取无轮询空耗 | 无法精准按时间排序、不支持重复任务、单List易堆积 | 延迟时间固定、任务量小、对时间精度要求低的场景(如简单重试) |
| Sorted Set + Zrangebyscore | 以时间戳 为score,任务ID/内容为member;定时用zrangebyscore获取到期任务并删除 |
时间精度高、支持按时间排序、可去重、易扩展 | 需定时轮询(可能有空轮询)、高并发下轮询易有性能损耗 | 对时间精度要求高、任务量中大型、需灵活设置延迟时间的场景(如订单超时、定时通知) |
14. Redis缓存与数据库一致性保证
-
先更数据库,再删缓存
-
问题:更新数据库后,若删缓存失败,会导致缓存存旧数据,数据库存新数据,数据不一致。
-
优化:增加缓存删除重试机制(如消息队列)、设置缓存过期时间兜底。
-
-
先删缓存,再更数据库
-
问题:高并发下,删缓存后数据库更新完成前,其他请求会从数据库加载旧数据回写缓存,导致不一致。
-
优化:延迟双删(先删缓存→更新数据库→延迟1~5秒再删一次缓存)、加分布式锁保证更新与查询的互斥。
-
-
终极方案:分布式事务(如Seata)、Canal监听数据库binlog异步更新缓存。
15. 高并发下Redis缓存预热核心思路
-
核心思路:在系统上线/重启前,提前将热点数据从数据库加载到Redis,避免高并发时大量请求直击数据库。
-
避免压垮数据库/Redis的措施:
-
分批加载:按数据热度分批次、分时段预热,避免一次性全量加载。
-
限流控速:控制预热请求的QPS,防止数据库连接池耗尽。
-
降级兜底:预热中若数据库/Redis压力过大,触发降级(如返回默认值)。
-
主从分离:用数据库从库加载数据,避免影响主库业务。
-
本地缓存辅助:预热过程中,用应用本地缓存暂存部分数据,减少Redis压力。
-
16. Redis缓存降级
-
触发条件:Redis集群宕机/超时、缓存穿透/击穿/雪崩、Redis达到最大内存限制、数据库压力过高。
-
降级策略分类:
-
熔断:短期切断Redis请求,直接返回默认值/空数据,适用于Redis突发故障。
-
限流:限制Redis的请求QPS,超出部分走降级逻辑,适用于Redis高负载。
-
静态化:将热点数据预生成静态文件(如JSON),请求直接读取静态文件,适用于Redis长期故障。
-
降级查询:简化查询逻辑(如只查部分字段),减少Redis/数据库压力。
-
-
降级开关设计:
-
基于配置中心(如Nacos/Apollo)做动态开关,支持一键降级/恢复。
-
按业务模块分粒度设计开关(如商品详情、订单列表单独降级)。
-
结合监控指标自动触发降级(如Redis超时率>50%自动熔断)。
-
增加降级日志与告警,便于追踪降级原因与恢复。
-
17. Redis实现分布式Session
-
实现方式:
-
将Session数据序列化后存入Redis,以
sessionId为key,设置过期时间。 -
客户端(浏览器)用Cookie存储
sessionId,每次请求携带该ID从Redis获取Session。
-
-
需考虑的问题:
-
Session过期:设置合理的Redis key过期时间,支持客户端刷新Session时重置过期时间。
-
多集群共享:Redis采用主从+哨兵/集群模式,保证Session数据跨节点可用;跨机房场景用Redis集群同步。
-
安全性 :
sessionId设置复杂随机值,防止伪造;Cookie开启HttpOnly和Secure属性,防止XSS和CSRF;Session数据加密存储。
-
-
与Spring Session结合的核心逻辑 : Spring Session通过重写
HttpSession接口,将Session的创建、读取、更新、销毁操作委托给Redis;通过过滤器(SpringSessionRepositoryFilter)拦截请求,从Redis中获取/存储Session数据,实现分布式Session的透明化使用。
18. Redis Pipeline
-
作用:批量发送多个Redis命令,减少客户端与Redis的网络往返次数,提升批量操作的性能。
-
核心原理:客户端将多个命令缓存到本地缓冲区,一次性发送给Redis;Redis依次执行命令后,将所有结果批量返回给客户端。
-
与事务(Multi/Exec)的区别:
特性 Pipeline 事务(Multi/Exec) 执行逻辑 命令按顺序执行,无原子性保证 命令入队后原子性执行(全部执行或全部不执行) 响应时机 批量发送后批量返回结果 Exec后才执行并返回所有结果 依赖关系 不支持命令间的依赖(如后一条用前一条的结果) 支持命令入队时的依赖判断 -
最佳使用场景:
-
批量执行无依赖 的读/写命令(如批量设置
set、批量获取get)。 -
高并发下的批量数据插入/查询(如数据迁移、统计报表生成)。
-
网络延迟较高的场景(如跨机房Redis调用),减少网络开销。
-
完。