Redis
基础概念:
1、Redis 是什么?为什么称之为高性能数据库
Redis 是一个开源、基于内存、键值对类型的非关系型数据库(NoSQL),支持数据持久化,同时提供主从复制、哨兵、集群等高可用方案,既可以当做缓存中间件,也可以承担部分业务数据存储。
高性能来源于 4 个核心因素:
- 绝大部分数据操作在内存完成,内存读写速度远高于磁盘;
- 命令执行采用单线程模型,规避多线程锁竞争、线程上下文切换的开销;
- 使用 IO 多路复用模型,单线程高效管理海量客户端连接;
- 底层全部采用经过高度优化的数据结构。
2、Redis 为什么快?四个核心原因
- 内存操作:所有热点数据存放内存,内存访问属于纳秒级,不存在磁盘 IO 耗时;
- 单线程执行命令:不需要加锁,没有多线程之间切换带来的 CPU 开销;
- IO 多路复用(epoll):单线程可以监听成千上万个客户端 socket 连接,没有空闲等待,最大化利用网络 IO;
- 底层高效数据结构:SDS、哈希表、跳表、压缩列表等结构,时间复杂度低,内存利用率高。
Redis6.0 引入的多线程,只用于 socket 网络数据的读写,命令的执行依旧是单线程串行执行,不存在并发安全问题。
3、单线程为什么还能扛住高并发?
首先分清两个概念:并发和并行不一样。
- 并行:多个任务同一时刻在多个 CPU 上同时运行;
- 并发:多个任务交替执行,宏观上同时处理。
Redis 单线程依靠 IO 多路复用,能够同时监听大量客户端连接。大部分请求瓶颈并不是 CPU 计算,而是网络等待。当某个客户端还没有发来数据的时候,线程不会阻塞等待,转而去处理其他就绪的客户端请求。 Redis 的命令执行速度极快,一条命令执行耗时非常短,可以快速处理完一个请求再处理下一个,因此单线程就可以支撑上万 QPS。
4、Redis 支持的数据类型
5 大基础类型 String、List、Hash、Set、ZSet(Sorted‑Set)
3 大特殊类型 Bitmap(位图)、HyperLogLog、Geo
高级类型 Stream(Redis5.0 推出,专门用于消息队列)
5、Redis 常用应用场景
- 热点缓存:减轻数据库压力,加速查询;
- 计数器:点赞数、浏览量,利用 String 原子自增;
- 分布式锁:跨服务进程之间互斥控制;
- 排行榜:ZSet 实现热搜、积分排行榜;
- 签到打卡:Bitmap 位图;
- UV 独立访客统计:HyperLogLog;
- 地理位置服务:附近的人、距离计算,Geo;
- 消息队列:List、Stream 实现异步解耦;
- 会话 Session 存储、限流。
基础数据结构:
1、String 底层结构、最大容量、使用场景
String 的底层实现是 SDS(Simple Dynamic String,简单动态字符串) ,是 Redis 自定义的字符串结构,源码定义在 sds.h 中。 SDS 结构由头部和数据区组成:头部包含 len(已使用字节数)、alloc(总分配字节数)、flags(编码类型标记)字段;尾部 buf 数组存储实际数据。根据字符串长度不同,SDS 会自动选择 sdshdr8、sdshdr16、sdshdr32、sdshdr64 等不同大小的头部,最大化节省内存。
相比 C 原生字符串,SDS 的核心优势:
- O (1) 获取长度 :直接读取
len字段,无需遍历到结束符; - 二进制安全 :不以
\0作为字符串结束标志,可存储图片、序列化字节等二进制数据; - 减少内存重分配:支持空间预分配(小于 1MB 时翻倍扩容,大于 1MB 时每次扩容 1MB)和惰性释放,避免频繁内存申请与拷贝。
最大容量 :单个 String 类型 value 的最大长度为 512MB,由源码硬性限制,超过该长度会直接报错。
典型使用场景:
- 业务数据缓存,如 JSON 格式的用户信息、配置项;
- 原子计数器,通过
INCR/DECR实现浏览量、点赞数统计; - 分布式锁,基于
SET key value NX EX原子命令实现; - 会话 Session 存储、接口限流令牌等。
2、List 底层 quicklist 原理
Redis 3.2 版本之后,List 类型的底层统一由 quicklist(快速列表) 实现,替代了之前 ziplist 和双向链表二选一的编码方案。
quicklist 的本质是**双向链表 + 压缩列表(ziplist)**的混合结构:
- quicklist 本身是一个双向链表,链表的每个节点(quicklistNode)不存放单个元素,而是存放一个 ziplist 结构;
- 每个 ziplist 是一块连续的内存空间,内部批量存储多个 List 元素。
这种设计是对内存占用和操作性能的折中平衡:
- 相比纯双向链表:消除了大量节点指针的内存开销,减少内存碎片,内存利用率更高;
- 相比纯 ziplist:避免了元素过多时,插入删除触发大规模内存拷贝的性能问题;
- 同时保留了双向链表头尾增删 O (1) 的优势,也通过分段 ziplist 保证了随机访问的性能。
可通过配置项 list-max-ziplist-size 控制每个节点中 ziplist 的最大大小,以此平衡内存占用和读写效率。List 常用来实现简单消息队列、时间轴列表、最新消息流等场景。
3、Hash 底层结构、为什么适合存对象
Hash 的底层有两种编码结构,会根据元素数量和 value 大小自动切换:
- ziplist(压缩列表) :当哈希的字段数 ≤ 512 个(
hash-max-ziplist-entries默认值),且每个 value 长度 ≤ 64 字节(hash-max-ziplist-value默认值)时使用,采用连续内存紧凑存储,内存效率极高。 - dict(哈希表 / 字典):当字段数量或 value 长度超过阈值时,自动切换为哈希表实现,通过数组 + 链表解决哈希冲突,增删查改平均时间复杂度 O (1)。
Hash 适合存储对象的核心原因:支持字段级独立操作 。 如果用 String 存储对象,通常是存入序列化后的 JSON 字符串,只要修改一个字段,就需要把整个字符串读出、反序列化、修改、再序列化写回 Redis,全量操作开销很大。 而 Hash 可以直接对单个字段执行 HSET、HGET,只操作目标字段,不需要读写整个对象,开销更小、更灵活;小数据量下 ziplist 编码还能带来比 String 更高的内存利用率,非常适合存储用户信息、商品属性这类多字段的结构化数据。
4、Set 底层、特点、应用场景
Set 的底层采用双编码自动切换机制:
- intset(整数集合) :当集合内所有元素都是整数,且元素数量 ≤ 512 个(
set-max-intset-entries默认值)时使用,用连续数组存储,内存占用极低。 - dict(哈希表):当出现非整数元素,或者元素数量超过阈值时,自动切换为哈希表实现;元素作为哈希表的 key,value 统一为 null,利用哈希表的特性实现自动去重。
Set 的核心特点:元素无序、自动去重,原生支持交集、并集、差集等集合运算。
典型应用场景:
- 数据去重,比如用户访问去重、标签去重;
- 社交场景,比如计算两个用户的共同好友、共同关注(交集运算);
- 抽奖活动,利用去重特性保证用户不会重复中奖;
- 用户标签系统,通过集合运算筛选具备特定标签的用户。
5、ZSet 底层跳表 skiplist 原理、为什么用跳表不用红黑树
ZSet(有序集合)同样有两种编码:小数据量时使用 ziplist 紧凑存储;当元素数量超过 128 个(zset-max-ziplist-entries 默认值)或 member 长度超过 64 字节时,切换为 跳表(skiplist)+ 哈希表(dict) 的组合结构。 其中哈希表存储 member 到 score 的映射,实现 O (1) 时间查询成员分数;跳表负责按照 score 排序,支撑高效的排序、范围查询。
跳表原理: 跳表是一种多层有序链表结构。最底层(第 0 层)是完整的原始数据链表,包含全部元素;每往上一层都是下一层的索引层,节点数量逐层减少。查找时从顶层索引开始,逐层向下定位,类似二分查找,时间复杂度可达 O (log n)。
Redis 选择跳表而不用红黑树,主要有三个原因:
- 范围查询性能更优:ZSet 最核心的使用场景是范围遍历(比如排行榜取 TopN、分数区间查找)。跳表找到左边界后,只需沿底层链表向后线性遍历即可,实现简单且缓存局部性好;红黑树需要中序遍历 + 回溯,实现复杂且连续访问效率更低。
- 实现与维护更简单:跳表的插入、删除只需要修改相邻节点的指针,不需要红黑树的旋转、变色等平衡操作,代码实现、调试和维护成本更低。
- 单点查找的时间复杂度同为 O (log n),性能差距不大,但 ZSet 的核心诉求是有序范围操作,跳表在该场景下优势更明显。
特殊数据结构:
1、HyperLogLog 作用、误差范围、适用场景
HyperLogLog 是 Redis 提供的概率型基数统计数据结构,核心作用是统计集合中不重复元素的数量(基数),它不会存储原始元素本身,只维护统计结构,因此内存占用极低。
- 误差范围 :标准估算误差约为 0.81%,统计结果是近似值,并非精确值。
- 内存特性:单个 HyperLogLog 键只需要约 12KB 内存,就可以统计亿级别的基数,内存消耗和数据量无关。
- 适用场景:海量数据场景下的独立访客(UV)统计、页面独立访问数统计等;只需要总数、不需要获取具体元素列表,且可以接受微小误差的场景。
2、Bitmap 位图原理、签到、统计活跃用户
Bitmap 本质上是 String 类型的扩展,它以二进制 bit 位为单位存储状态,每个 bit 位对应一个唯一标识(如用户 ID),bit 值为 0 代表未发生、1 代表已发生,是一种极致节省空间的存储方式。
- 签到场景实现 :以用户 ID 作为 bit 位的偏移量,使用
SETBIT sign:日期 用户ID 1即可标记用户当日签到;统计当日签到总人数直接通过BITCOUNT命令统计 bit 为 1 的数量。 - 统计活跃用户 :按时间维度生成不同的 Bitmap 键(如
active:20260827),当日活跃用户对应 bit 位置 1;统计多日连续活跃用户可以通过位与(AND)运算,统计周期总活跃用户可以通过位或(OR)运算,效率极高。
3、Geo 地理位置实现原理
Redis Geo 的底层基于 ZSet(有序集合) 实现。 核心原理是通过 Geohash 编码算法,将经纬度坐标编码成一个 52 位的整数,把这个整数作为 ZSet 的 score 值,ZSet 的 member 存储位置标识。
借助 ZSet 本身按 score 排序、范围查询的能力,Geo 可以实现:
- 计算两个位置之间的距离;
- 查询指定坐标半径范围内的所有地点(附近的人、附近商家);
- 按距离对地点进行排序等地理位置服务。
4、Stream 消息队列原理、消费组、偏移量
Stream 是 Redis 5.0 推出的专业持久化消息队列结构,弥补了 List 做队列的功能缺陷。
- 基本原理:Stream 以消息流的形式持久化存储消息,每条消息都有全局唯一的递增 ID(由时间戳 + 序号组成);消息消费后不会自动删除,会长期保留。
- 消费组(Consumer Group): 可以将多个消费者划分到同一个消费组,同一条消息在组内只会分配给一个消费者,实现消费端的负载均衡;不同消费组之间相互独立,可以各自完整消费同一条消息流。
- 偏移量(Offset): 每个消费组会维护一个消费偏移量,记录当前组已经消费到的消息位置;消费者重启后,可以从偏移量位置继续向后消费,实现断点续传,保证消息不丢失。 同时配套 ACK 确认机制,消费者处理完消息后需手动确认,消息才会标记为已处理。
5、BitMap 和 HyperLogLog 区别
二者都是用于统计的轻量化结构,但核心定位和能力差异很大:
| 维度 | Bitmap | HyperLogLog |
|---|---|---|
| 统计精度 | 精确统计,状态完全准确 | 估算统计,标准误差约 0.81% |
| 内存占用 | 和最大 ID 正相关,ID 越大内存越高 | 内存固定极低,单 key 约 12KB,与数据量无关 |
| 能力边界 | 可查询单个元素的状态,支持位运算做多维度统计 | 只能统计基数总数,无法查询单个元素是否存在,无法获取原始数据 |
| 适用场景 | 签到打卡、用户状态标记、小范围精确统计 | 海量数据基数统计,如亿级 UV 统计,不需要明细的场景 |
6、Redis 布隆过滤器原理、优缺点、误判问题、使用场景
布隆过滤器是一种概率型数据结构,核心由一个大的位数组 + 多个独立的哈希函数组成,用于快速判断一个元素是否存在于集合中。
- 工作原理: 写入元素时,元素经过多个哈希函数计算,得到多个数组下标,将位数组中对应下标的 bit 位全部置为 1; 查询元素时,用同样的哈希函数计算下标,检查所有对应 bit 位是否都为 1。
- 误判问题 : 不存在反向误判:只要有一个 bit 位为 0,元素一定不存在 ; 存在正向误判:如果所有 bit 位都为 1,元素可能存在(是其他元素哈希碰撞导致的误判);元素越多,误判率越高。
- 优缺点: 优点:内存占用极低、插入和查询都是常数级时间,性能极高; 缺点:存在一定误判率;传统布隆过滤器不支持删除元素。
- 使用场景: 缓存穿透防护(提前存入所有合法 key,拦截不存在的请求); 黑名单校验、爬虫 URL 去重、垃圾邮件过滤等。
缓存经典问题(必问)
1、缓存穿透是什么?产生原因 + 四种解决方案
缓存穿透是指客户端查询的数据在缓存和数据库中都不存在,导致请求永远无法命中缓存,直接穿透缓存层打到数据库;
大量这类无效请求持续涌入,会快速压垮数据库。
产生原因:
- 恶意攻击:爬虫、攻击者批量传入不存在的 ID、非法参数,刻意打穿缓存;
- 业务异常:数据被物理删除、数据未同步,且缓存中没有对应空值兜底;
- 批量查询:批量传入大量非法主键,绕过缓存直接访问数据库。
四种解决方案:
- 空值缓存:数据库查询为空时,在缓存中写入一个短过期时间的空值,后续相同请求直接命中缓存,避免重复打库;
- 布隆过滤器:提前将所有合法主键存入布隆过滤器,请求先经过过滤器校验,不存在的 key 直接拦截,不访问缓存和数据库;
- 入口参数校验:在接口层对参数做合法性校验,比如 ID 为负数、格式非法的请求直接返回,不进入后续流程;
- 黑名单限流:对频繁发起非法请求的 IP、用户标识进行限流或拉黑,从源头减少穿透攻击。
2、缓存击穿是什么?产生原因 + 解决方案(互斥锁、热点永不过期)
回答: 缓存击穿是指单个高并发访问的热点 Key,在缓存过期的瞬间,大量并发请求同时涌入,缓存全部失效,请求瞬间全部打到数据库,导致数据库压力骤增,甚至宕机。
产生原因: 热点 Key 设置了固定过期时间,过期时刻恰好有大量请求访问;或是热点流量突发,而缓存刚好到期失效。
两种核心解决方案:
- 互斥锁方案:缓存失效时,只允许一个线程获取分布式锁去查询数据库并更新缓存,其余线程等待缓存刷新完成后再读取数据。优点是保证数据库不会被打穿,缺点是会增加请求延迟,降低吞吐量。
- 热点 Key 逻辑永不过期:物理层面不设置缓存过期时间,由业务代码维护逻辑过期;后台启动异步线程定时刷新热点数据,缓存永远不会出现失效瞬间,从根源避免击穿。
3、缓存雪崩是什么?产生原因 + 事前 / 事中 / 事后解决方案
回答: 缓存雪崩是指大量缓存 Key 在同一时间集中过期失效,或者 Redis 服务整体宕机,导致海量请求全部直接打到数据库,引发数据库压力过载甚至崩溃,造成服务连锁故障。
产生原因:
- 过期时间集中:批量设置缓存时使用了相同的过期时间,到期同时失效;
- Redis 故障:Redis 节点宕机、集群网络分区,缓存服务整体不可用;
- 流量突增:大促、活动导致缓存访问量骤增,过期后集中击穿数据库。
分阶段解决方案:
- 事前预防:
- 过期时间打散:给缓存过期时间增加随机偏移值,避免大量 key 同时过期;
- 高可用架构:部署 Redis 哨兵 / 集群,避免单点故障,保障缓存服务可用性;
- 热点数据永不过期:核心热点 key 设置逻辑永不过期,后台异步刷新;
- 多级缓存:本地缓存 + Redis 缓存二级架构,降低 Redis 故障的影响。
- 事中控制:
- 服务限流熔断:接入层开启限流、熔断、降级机制,控制数据库的请求量;
- 分布式锁保护:通过分布式锁控制数据库并发访问数,避免数据库被打垮。
- 事后补救:
- 缓存预热:Redis 重启后,提前批量加载热点数据到缓存,快速恢复服务;
- 故障恢复:快速重启 Redis 集群,通过持久化文件恢复数据,逐步恢复流量。
4、Reds大key问题(没啥招数,其实就是做拆分)
大 key 指单个 Key 对应的 Value 体积过大:比如 String 类型 value 超过 100KB,集合类型(List/Hash/Set/ZSet)包含数万甚至数十万元素。
核心危害: 删除、过期、迁移大 key 时会阻塞 Redis 单线程,导致其他命令卡顿超时;集群中大 key 集中在单个节点,会造成内存倾斜、节点负载不均;读取大 key 还会占用大量带宽,引发网络拥塞。
核心解决方案:拆分
- 大字符串拆分:将大对象按业务维度拆分为多个小 key 分块存储,读取时按块拼接;
- 大集合拆分:将一个大集合按 ID 哈希、时间分片等维度拆分为多个小集合,分散存储压力;
- 渐进式处理:删除大集合时,使用 SCAN 命令分批遍历删除,避免一次性 DEL 阻塞主线程;
- 业务优化:禁止一次性读取整个大集合,统一使用分页、分批查询。
5、Redis热key问题(去搜京东hotkey探测机制)
热 key 指单个 Key 在短时间内收到超高并发访问,访问压力集中在 Redis 集群的单个节点上,导致该节点 CPU、网卡打满,成为整个集群的性能瓶颈。
京东 HotKey 探测机制(JD-HotKey)
京东开源了毫秒级热 key 探测框架,历经多次 618、双十一大促验证,核心是「客户端采集 + Worker 端计算 + etcd 全局同步 + 本地缓存兜底」的四层架构。
- 核心架构组成
- etcd 集群:全局配置中心,存储热 key 判定规则、Worker 节点地址、已探测出的热 key 名单;提供监听订阅能力,保证配置和热 key 信息在所有服务节点间一致。
- Client 端 Jar 包:嵌入业务服务,本地按时间窗口累加 key 访问次数,默认每 500ms 批量上报给 Worker;同时监听热 key 通知,收到后将热点数据缓存到本地 Caffeine 内存中。
- Worker 端集群:独立部署的计算节点,接收各客户端上报的 key,通过滑动窗口统计访问频率;达到阈值则判定为热 key,推送给所有相关客户端并同步到 etcd。单台 16 核机器可稳定处理每秒 30 万以上的 key 探测。
- Dashboard 控制台:可视化配置规则、查看热 key 列表、支持人工增删热 key。
- 判定逻辑:支持按业务维度配置差异化规则,比如商品 ID 1 秒内访问超 100 次、用户 ID 2 秒内超 20 次即判定为热 key。
- 热 key 处理方案
- 本地二级缓存:热 key 数据缓存到应用本地内存,请求直接命中本地,绕过 Redis;
- 多副本分流:将热点 key 复制多份分布到集群不同节点,分散访问压力;
- 请求打散:同一热 key 生成多个副本 key,请求哈希到不同节点,均衡流量。
缓存与数据库一致性
1、缓存和数据库双写一致性几种策略
缓存与数据库双写一致性,核心是解决数据更新时,缓存和数据库两个独立存储之间的数据不同步问题。业界主流有四种策略,绝大多数业务场景下缓存只保证最终一致性,强一致性实现成本极高,不适用于缓存架构。
四种主流策略分别是:
- 先更新数据库,再删除缓存(业界最主流方案)
- 先删除缓存,再更新数据库
- 延时双删策略
- 基于 Canal 订阅 binlog 的异步缓存更新方案
2、先更新数据库再删缓存、先删缓存再更新数据库 优缺点
方案一:先更新数据库,再删除缓存(业界推荐)
优点:
- 并发脏数据概率极低。唯一可能产生脏数据的极端场景:缓存刚好失效时,读线程读到旧数据正要写回缓存,此时写线程更新完数据库并删除缓存,读线程随后把旧值写入缓存。但写数据库速度远慢于读,该场景发生概率极低。
- 实现简单,步骤少,业务侵入性低,出错概率小。
缺点:
- 如果删除缓存的操作失败,缓存会残留旧数据,导致持续不一致;
- 高并发写场景下频繁删除缓存,会导致缓存命中率下降。
方案二:先删除缓存,再更新数据库
优点:
- 逻辑直观,先让缓存失效,再更新底层数据,理论上后续读请求会自动拉取最新值。
缺点:
- 并发脏数据概率高。线程 A 先删除缓存,还未更新完数据库;此时线程 B 查询,缓存未命中,读取数据库旧值并写回缓存;等线程 A 更新完数据库,缓存中就会长期留存旧数据,不一致窗口非常大。
- 无法解决数据库更新期间的并发读问题,数据不一致风险远高于前者。
3、延时双删策略原理
延时双删是对「先删缓存,再更新数据库」方案的优化,目的是降低并发脏数据的概率,属于最终一致性方案。
执行步骤
- 第一步:先删除缓存;
- 第二步:更新数据库;
- 第三步:线程休眠一小段时间(通常几百毫秒到 1 秒,根据业务读耗时设定),然后第二次删除缓存。
核心原理
休眠时间需要略大于一次普通业务读请求的耗时。 目的是等待「数据库更新过程中,其他并发线程读取旧数据并写入缓存」这个过程完成;第二次删除缓存,就可以把这段时间内产生的脏缓存清理掉,避免脏数据长期停留在缓存中。
注意点
- 只能降低不一致概率,无法完全消除;如果第二次删除失败,依然会残留脏数据;
- 同步休眠会阻塞写线程、降低吞吐量,生产环境通常异步执行第二次删除。
4、如何保证强一致性?Canal 订阅 binlog 方案
首先明确:缓存天生不适合严格强一致性场景。如果要求绝对强一致,最可靠的方式是直接读写数据库,不走缓存;缓存架构通常只追求秒级以内的最终一致性。
Canal 订阅 binlog 方案原理
Canal 是阿里巴巴开源的 MySQL binlog 订阅工具,它模拟 MySQL 从节点协议,实时接收并解析数据库的二进制日志。
完整方案流程:
- 业务服务只负责操作数据库,不直接操作缓存,业务代码与缓存逻辑完全解耦;
- Canal 服务实时监听 MySQL 的 binlog 日志,解析出数据的增删改变更;
- 消费端接收到变更事件后,异步去删除或更新对应的 Redis 缓存。
方案优势
- 业务层无感知,缓存更新逻辑和业务解耦,避免业务代码里到处穿插缓存操作;
- 基于 binlog 保证变更事件不丢失,缓存更新的可靠性更高;
- 一致性延迟低,通常在毫秒到秒级,是工业界非常成熟的最终一致性方案。
关于强一致性的补充
如果必须实现缓存与数据库的强一致,需要引入分布式事务(如 2PC、TCC),同时包裹数据库操作和缓存操作,保证二者同时成功或同时回滚;但性能损耗极大、复杂度极高,几乎不会在普通缓存场景使用。
过期/淘汰策略
1、Redis 键过期删除三种策略:定时删除、惰性删除、定期删除
Redis 针对设置了过期时间的 key,设计了三种过期删除策略,最终采用「惰性删除 + 定期删除」的组合方案。
- 定时删除 为每个过期 key 创建定时器,过期时间一到立刻删除 key 释放内存。 优点:内存释放及时; 缺点:大量定时器会占用大量 CPU 资源,严重影响 Redis 主流程性能,Redis 没有采用该策略。
- 惰性删除 key 过期后不主动删除,等客户端访问这个 key 时,再检查是否过期,过期则删除并返回空。 优点:几乎不占用 CPU 额外开销,对性能影响极小; 缺点:如果过期 key 永远不被访问,就会一直占用内存,造成内存浪费。
- 定期删除 每隔固定时间,从过期 key 集合中随机抽取一批 key 检查,删除其中过期的 key。 优点:通过控制执行频率和时长,折中平衡 CPU 开销和内存回收效率; 缺点:无法精准控制过期 key 的内存回收速度。
👉 Redis 实际采用:惰性删除 + 定期删除结合,既保证主线程性能,又尽可能回收过期内存。
2、Redis 内存满后八大淘汰策略分别是什么?
当 Redis 内存占用达到 maxmemory 上限,再写入新数据时会触发内存淘汰,共 8 种策略,分为「全量 key 范围」和「仅过期 key 范围」两大类。
noeviction(默认策略):不淘汰任何 key,新的写请求直接返回错误,只读操作正常执行。allkeys-lru:在所有 key 中,淘汰最近最少使用(Least Recently Used)的 key。volatile-lru:只在设置了过期时间的 key 中,淘汰最近最少使用的 key。allkeys-lfu:在所有 key 中,淘汰访问频次最低(Least Frequently Used)的 key。volatile-lfu:只在设置了过期时间的 key 中,淘汰访问频次最低的 key。allkeys-random:在所有 key 中,随机挑选 key 淘汰。volatile-random:只在设置了过期时间的 key 中,随机挑选淘汰。volatile-ttl:只在设置了过期时间的 key 中,淘汰剩余存活时间最短的 key。
3、LRU 底层实现原理、Redis 近似 LRU 怎么做的
传统 LRU 原理
标准 LRU(最近最少使用)算法底层是双向链表 + 哈希表:链表头部是最近访问的节点,尾部是最久未访问的节点;每次访问 key 就把节点移到头部;内存满时删除尾部节点。 缺点:需要维护完整的链表结构,额外内存开销大,节点移动操作频繁,对 Redis 性能影响较大。
Redis 近似 LRU 实现
Redis 没有实现精确 LRU,而是采用采样淘汰的近似方案: 每次淘汰时,随机采样 5~10 个 key,计算每个 key 的闲置时间,只淘汰采样集合中闲置最久的 key。 通过多次采样迭代,最终淘汰效果接近精确 LRU,但性能开销远低于标准 LRU,牺牲极小的准确率换取极高的执行性能。
4、过期键会不会主动占用内存?主从间过期怎么同步
过期键的内存占用:
过期键会继续占用内存。 因为 Redis 采用惰性 + 定期删除策略:只有 key 被访问触发惰性删除、或者被定期删除抽中时,才会被真正删除释放内存;如果过期 key 既没被访问、也没被定期抽样抽到,就会一直占用内存空间。
主从间的过期同步:
主从架构中,过期删除的主动权完全在主节点:
- 主节点检测到 key 过期并删除后,会向所有从节点发送一条
DEL命令,同步删除从节点上的对应 key; - 从节点自身不会主动删除过期 key,即使键已经过期,从节点也不会自行处理,必须等待主节点的同步命令;
- 客户端访问从节点时,Redis 会检查键是否过期,过期则返回空结果,但不会在从节点本地删除该键。
持久化 RDB & AOF(超级高频
1、RDB 是什么?原理、触发方式、优缺点
RDB 是 Redis 的快照式持久化 机制,指在某个指定时间点,将 Redis 内存中的全量数据生成一份二进制格式的快照文件(默认文件名 dump.rdb)写入磁盘,实现数据落盘持久化。
核心原理:生成内存数据的完整二进制镜像,以紧凑的格式存储,恢复时直接加载到内存即可完成数据还原。
触发方式分为四类:
save命令:主线程阻塞式执行快照生成,期间无法响应客户端请求,生产环境禁用。bgsave命令:fork 出一个子进程,由子进程后台生成 RDB 文件,主进程继续处理请求,是最常用的触发方式。- 配置自动触发:通过
save m n配置规则,m 秒内发生 n 次写操作则自动触发 bgsave。 - 被动触发:Redis 正常关闭(shutdown)、主从复制全量同步时,都会自动生成 RDB 快照。
优缺点
- 优点:文件体积小、数据恢复速度极快;适合冷备份、全量数据迁移。
- 缺点:两次快照间隔内宕机会丢失全部增量数据;fork 子进程时会短暂阻塞主线程,内存越大阻塞时间越长。
2、AOF 是什么?日志刷写策略三种
AOF(Append Only File)是 Redis 的增量日志式持久化 机制,以文本形式记录每一条对数据有修改的写命令,追加写入到 appendonly.aof 文件中;宕机后通过逐条回放日志中的命令恢复数据。
三种日志刷盘策略(appendfsync):
always:每执行一条写命令,就立刻调用 fsync 刷写到磁盘。 特点:数据安全性最高,最多丢失一条命令的数据;但磁盘 IO 开销极大,性能最差。everysec(默认配置):每秒执行一次 fsync 刷盘。 特点:性能与安全性的折中,最多丢失 1 秒的数据,是生产环境的默认选择。no:不主动执行刷盘,完全交由操作系统自行决定刷盘时机。 特点:性能最高,但数据丢失风险最大、不可控。
3、AOF 重写机制原理、为什么要重写
为什么要重写
随着 Redis 运行时间增长,AOF 日志文件会不断膨胀,文件中存在大量冗余命令(比如对同一个 key 多次 set、多次 incr),既占用大量磁盘空间,也会大幅拖慢宕机后的数据恢复速度。AOF 重写的核心目的就是压缩 AOF 文件体积,提升恢复效率。
重写原理
AOF 重写不会读取和解析旧的 AOF 文件 ,而是直接读取当前 Redis 内存中的数据,为每一个 key 生成一条对应的最简写命令,写入新的 AOF 文件。 重写执行期间,新的写命令会同时写入旧 AOF 文件 和重写缓冲区;重写完成后,将缓冲区中的增量命令追加到新 AOF 文件末尾,最后用新文件原子替换旧的 AOF 文件。
重写可通过 bgrewriteaof 手动触发,也可通过配置文件大小阈值自动触发。
4、RDB 和 AOF 对比区别、各自适用场景
| 对比维度 | RDB | AOF |
|---|---|---|
| 存储形式 | 全量二进制快照 | 增量写命令日志 |
| 数据完整性 | 较差,丢失两次快照间隔的所有数据 | 较好,默认最多丢失 1 秒数据 |
| 恢复速度 | 极快,直接加载二进制镜像 | 较慢,需要逐条回放所有命令 |
| 文件体积 | 小,二进制压缩存储 | 大,重写后可大幅压缩 |
| 性能开销 | 低,仅快照时 fork 子进程 | 相对较高,持续写日志 + 定期刷盘 |
适用场景
- RDB:适合全量冷备份、数据迁移、可接受少量数据丢失的场景,常作为辅助持久化手段。
- AOF:适合数据安全性要求高、不能丢失大量业务数据的场景,是生产环境的主流持久化方案。
5、混合持久化原理(RDB+AOF 混合)
回答: 混合持久化是 Redis 4.0 推出的特性,结合了 RDB 和 AOF 各自的优势,在 AOF 重写阶段生效。
核心原理: AOF 重写时,不再生成纯命令格式的日志,而是先将当前内存中的全量数据以 RDB 二进制格式写入新 AOF 文件的开头,后续新增的写命令再以 AOF 日志格式追加到文件末尾。
核心优势:
- 恢复速度快:开头的 RDB 部分可直接加载,无需逐条回放全量命令,恢复速度接近纯 RDB;
- 数据安全性高:增量部分保留 AOF 日志,最多丢失 1 秒数据,安全性与纯 AOF 一致; 兼顾了恢复效率和数据可靠性,是目前生产环境推荐的持久化方案。
6、Redis 宕机后数据恢复流程
Redis 重启恢复数据时,会按照「优先 AOF、次之 RDB」的优先级执行:
- 首先检查是否开启了 AOF 持久化:
- 如果开启了 AOF,优先加载 AOF 文件恢复数据;因为 AOF 的数据完整性更高,丢失的数据更少。
- 如果未开启 AOF,才会加载 RDB 快照文件恢复数据。
- 如果 AOF 和 RDB 文件都不存在,则 Redis 以空数据库启动。
补充:若开启了混合持久化,加载 AOF 文件时会先解析开头的 RDB 部分快速加载全量数据,再回放后面的增量 AOF 命令补齐数据。
并发 & 分布式锁
1、Redis 分布式锁基础实现原理(SET NX EX)
Redis 分布式锁的核心是利用 SET key value NX EX seconds 原子命令实现,保证「加锁 + 设置过期时间」操作的原子性。
- 核心参数含义:
NX:只有当 key 不存在时才能设置成功,保证同一时刻只有一个客户端能加锁成功;EX:给 key 设置过期时间,防止客户端异常崩溃后锁永远无法释放,避免死锁。
- value 设计:value 一般设为唯一标识(如 UUID + 线程 ID),解锁时校验 value 是否匹配,避免误删除其他客户端持有的锁。
- 为什么不用 SETNX + EXPIRE 两条命令:两条命令不具备原子性,如果 SETNX 成功后客户端宕机,来不及设置过期时间,锁就会永久存在,造成死锁。
2、分布式锁超时释放问题怎么解决?
回答: 超时释放问题指:业务执行时长超过了锁的过期时间,锁被 Redis 自动释放,此时其他客户端可以获取到锁,引发并发安全问题;同时原业务线程执行结束后,还可能误删除其他线程的锁。
解决方案:
- 合理预估过期时间:根据业务执行耗时设置过期时间,预留一定冗余,降低超时概率;
- 锁续约机制:引入后台定时线程,业务未执行完成时自动延长锁的过期时间(即看门狗机制);
- 解锁身份校验:解锁时必须校验锁的持有者标识,只能释放自己加的锁,避免误删他人的锁;
- 业务兜底校验:锁超时后,业务执行结果需要做并发校验,避免脏数据写入。
3、锁续约 / 续命怎么做?Redisson 看门狗原理
锁续约的核心是通过后台定时线程,动态延长锁的过期时间,保证业务执行完成前锁不会提前释放。Redisson 中的标准实现就是看门狗(WatchDog)机制。
看门狗原理:
- 加锁成功后,Redisson 会启动一个后台定时线程(看门狗线程);
- 默认锁的过期时间为 30 秒,看门狗每隔 1/3 过期时间(即 10 秒) 检查一次:如果当前线程仍然持有锁,就重新设置锁的过期时间为 30 秒,完成续命;
- 当业务执行完成、主动释放锁后,看门狗线程会被停止,不再续期。
作用:彻底解决业务执行耗时超过锁过期时间导致的锁提前释放问题,无需人工精准预估超时时间。
4、主从架构下分布式锁锁失效问题
在 Redis 主从 + 哨兵架构下,存在锁失效的典型场景: 客户端在 Master 主节点加锁成功后,锁数据还未同步到 Slave 从节点,此时 Master 宕机;哨兵执行故障转移,将一个 Slave 升级为新的 Master;由于新主节点上没有这把锁的数据,其他客户端就可以再次加锁成功,导致同一时间存在两把锁,锁失效。
解决方案:
- Redlock 红锁算法:部署多个相互独立、无主从关系的 Redis 节点,客户端向所有节点请求加锁,超过半数节点加锁成功才算真正获取锁;故障时单个节点宕机不影响整体锁状态,可靠性更高,但性能开销大。
- 容忍最终一致:如果业务可以接受极低概率的锁冲突,主从架构的锁失效概率很低,可通过业务兜底校验兼容。
- 集群架构同理:Redis Cluster 集群每个槽位同样有主从,锁失效逻辑一致,强一致场景同样推荐红锁方案。
5、Redisson 可重入锁原理
可重入锁指同一个线程可以多次获取同一把锁,不会出现自己阻塞自己的情况。Redisson 可重入锁底层基于 Redis Hash 结构 实现。
实现原理:
- Hash 的 key 是锁的名称,Hash 的 field 是锁持有者的唯一标识(线程 ID + 客户端 UUID),value 是该线程的重入次数。
- 加锁逻辑:
- 锁不存在:直接加锁成功,重入次数设为 1,同时启动看门狗;
- 锁已存在且 field 匹配当前线程:重入次数 +1,重置锁过期时间;
- 锁已存在且持有者不是当前线程:加锁失败,阻塞等待。
- 解锁逻辑:
- 校验持有者身份,匹配成功则重入次数 -1;
- 当重入次数递减到 0 时,真正删除锁的 key,停止看门狗线程。
主从复制 & 集群
1、Redis 主从复制原理、全量同步 / 增量同步
回答: 主从复制是 Redis 高可用的基础,主节点负责写,从节点负责读,实现读写分离和数据备份。复制分为全量同步和增量同步两种模式。
全量同步:一般发生在从节点初次连接、或断线后无法增量同步的场景。
- 从节点向主节点发送同步请求;
- 主节点执行
bgsave生成 RDB 快照文件,同时将快照期间的新写命令写入复制缓冲区; - 主节点将 RDB 文件发送给从节点,从节点清空本地数据并加载 RDB;
- 主节点再把缓冲区的增量命令发送给从节点,从节点执行命令补齐数据,同步完成。
增量同步:正常运行阶段的持续同步。 主节点每执行一条写命令,就会异步将命令发送给所有从节点,从节点执行命令,保证主从数据一致。
2、主从复制复制积压缓冲区作用
复制积压缓冲区(repl_backlog)是主节点维护的一个固定大小的环形缓冲区,用来存储最近一段时间的写命令以及对应的复制偏移量。
核心作用:
- 支持断线增量同步:主从短暂断线重连后,不需要重新全量同步,只需要根据从节点上报的偏移量,从缓冲区中取出缺失的命令补发即可,大幅提升断线恢复效率。
- 缓冲复制延迟:从节点网络延迟、阻塞时,命令可以暂存在缓冲区,避免主节点阻塞。
缓冲区大小由 repl-backlog-size 配置,太小会导致断线后经常触发全量同步;太大则浪费内存。
3、主从同步断线后怎么增量同步
主从断线重连后的增量同步流程如下:
- 从节点重新连接主节点后,向主节点上报自己当前的复制偏移量;
- 主节点校验该偏移量是否还存在于复制积压缓冲区中;
- 如果偏移量仍在缓冲区内:主节点将偏移量之后的所有写命令发送给从节点,从节点执行后完成增量同步;
- 如果偏移量已经被新数据覆盖(缓冲区是环形的,旧数据被覆盖):说明断线时间过长,无法增量同步,主节点会触发全量同步流程。
4、哨兵 Sentinel 作用、原理、故障转移流程
哨兵是独立运行的进程,组成哨兵集群,实现 Redis 主从的自动故障转移,解决主节点单点故障问题。
三大核心作用:
- 监控:持续检测所有主、从节点是否存活;
- 通知:节点故障时通知管理员和客户端;
- 自动故障转移:主节点宕机后,自动选举一个从节点升级为新主节点,保证服务可用。
故障转移完整流程:
- 单个哨兵检测到主节点无响应,标记为主观下线;
- 多个哨兵都确认主节点下线,达成多数共识后,标记为客观下线;
- 哨兵集群内部投票选举出一个领头哨兵,负责执行故障转移;
- 领头哨兵从存活的从节点中选出最优节点,升级为新的主节点;
- 命令其余从节点切换到新主节点进行复制;
- 通知客户端主节点变更,同时将旧主节点标记为下线,恢复后自动变为新主的从节点。
5、哨兵怎么选主、投票机制
哨兵选举分为两部分:领头哨兵选举 和 新主节点选举。
领头哨兵选举(基于 Raft 协议思想)
- 哨兵发现主节点客观下线后,会发起选举,拉票请求其他哨兵投票给自己;
- 每个哨兵在一轮选举中只能投一票,先到先得;
- 获得超过半数哨兵投票的节点,成为领头哨兵,负责执行故障转移;
- 如果选举失败,等待超时后开启新一轮选举。
新主节点选举规则(优先级从高到低)
- 从节点优先级 :
slave-priority配置值越小,优先级越高; - 复制偏移量:偏移量越大,数据越完整越新,优先选;
- 运行 ID:以上都相同时,选 runid 更小的从节点。
6、Redis Cluster 集群原理、哈希槽 16384
Redis Cluster 是无中心的分布式分片集群,解决单机内存上限问题,实现数据分布式存储和高可用。
核心原理是哈希槽分片:
- 整个集群预划分 16384 个哈希槽,编号 0 ~ 16383;
- 每个主节点负责管理其中一部分哈希槽;
- 写入数据时,对 key 做
CRC16(key) % 16384计算,得到对应的槽号,直接路由到负责该槽的主节点执行。
每个主节点可以挂载多个从节点,主节点故障时,集群自动将对应从节点升级为主,实现分片级别的高可用。
7、集群槽位分配、分片规则
分片规则: 数据不直接绑定节点,而是通过哈希槽做中间层。key → CRC16 → 哈希槽 → 节点,实现数据和节点的解耦,方便后续扩容缩容、槽位迁移。
注意:多键批量操作(如 MGET、MSET)要求所有 key 必须在同一个哈希槽,否则会报错。
槽位分配规则:
- 集群初始化时,槽位可以自动或手动分配,尽量均匀分布到所有主节点,保证各节点数据量均衡;
- 每个哈希槽有且仅属于一个主节点,不允许交叉;
- 只有全部 16384 个槽都有节点负责,集群才处于正常可用状态。
8、集群扩容、缩容原理
回答: 集群扩容和缩容的核心都是哈希槽迁移,数据跟随槽位移动,过程中集群持续对外提供服务。
扩容原理:
- 新的主节点加入集群,成为空节点;
- 从现有各个主节点上,分别迁移一部分哈希槽到新节点;
- 迁移过程中,数据逐步移动,槽位归属逐步更新;
- 所有槽迁移完成后,新节点正式承担读写服务,扩容完成。
缩容原理:
- 将待下线主节点的所有哈希槽,全部迁移到其他存活的主节点;
- 所有槽迁移完毕、数据完全转移后,将该节点从集群中移除;
- 缩容完成,剩余节点继续提供服务。
9、为什么是 16384 个哈希槽,不是更大?
主要有两个核心原因:
- 心跳包大小控制 集群节点之间的心跳消息会携带完整的槽位图,槽位数量对应位图大小。16384 个 bit = 2KB,心跳包体积很小,网络开销低;如果扩大到 65536 个槽,位图就变成 8KB,节点越多心跳带宽占用越高,得不偿失。
- 集群规模匹配 Redis 集群官方推荐节点数量上限约 1000 个,16384 个槽已经足够让上百个节点均匀分片,槽数量过多没有实际收益,反而增加管理开销。
补充:Redis 作者也明确说明,16k 槽对于 1000 节点规模的集群已经完全够用,是性能和分片粒度的最佳平衡。
10、集群高可用、脑裂问题怎么解决
集群高可用实现: 每个主节点都配置至少一个从节点作为备份;当主节点故障下线时,集群通过类似哨兵的故障转移机制,自动将对应从节点升级为新主节点,接管该分片的所有槽位,继续对外提供服务,实现分片级高可用。
脑裂问题与解决方案 脑裂指网络分区后,旧主节点和集群其他节点断开,但自身仍在运行,客户端继续向其写入数据;网络恢复后,旧主节点变为从节点,分区期间写入的数据会全部丢失。
解决方案:通过配置 min-replicas-to-write 参数。 设置主节点必须至少拥有 N 个在线的从节点,才能接收写请求。网络分区后,主节点失去从节点连接,数量不满足阈值,就会自动拒绝写入,避免脑裂产生脏数据,牺牲部分可用性保证数据一致性。
网络模型 & 底层原理
1、Redis 多路复用 IO 模型:epoll 原理
Redis 是单线程事件驱动模型,核心依靠 IO 多路复用(epoll) 同时监听成千上万条客户端连接,实现高性能网络 IO。
epoll 原理:
epoll 是 Linux 下多路复用模型,核心思想:单线程监听多个文件描述符(客户端 socket),只在连接就绪(可读 / 可写)的时候才去处理事件,不需要轮询遍历所有连接。 分为三步:
epoll_create:创建一个 epoll 实例;epoll_ctl:把所有客户端 socket 连接注册添加到 epoll 监听列表;epoll_wait:阻塞等待事件就绪;当某个客户端发来请求、socket 可读,epoll 就主动返回就绪的连接列表。
Redis 使用 epoll 带来的好处:
- 不需要开辟大量线程去处理每一个客户端,避免线程切换开销;
- 连接即使空闲,也几乎不消耗 CPU;
- 支持海量并发连接,所以 Redis 可以做到几万客户端同时连接。
Redis 单线程只是执行命令是单线程,网络 IO 靠 epoll 多路复用来管理大量连接。
2、Redis 6.0 多线程做了什么?网络多线程、命令还是单线程
Redis6.0 引入的多线程仅仅用来处理网络 IO,执行命令仍然是单线程串行执行。
6.0 多线程分工
- IO 线程(多线程):负责网络数据的读写。
- 从 socket 中读取客户端发来的请求数据;
- 将命令执行后的响应结果写回 socket;
- 多线程并发分担网络 IO 读写的压力。
- 主线程(单线程) :拿到解析好的命令,串行执行所有 Redis 命令。 SET、GET、LPOP 等操作依然只能主线程跑,命令执行不会并发。
为什么命令不做多线程?
- Redis 命令很多操作都是对共享数据修改,多线程就要加锁,锁竞争反而降低性能;
- 单线程无锁,代码简单,不容易出现并发安全问题。
一句话总结:IO 多线程负责收包发包;命令执行依旧单线程。
3、Pipeline 管道作用、原理、适用场景
回答: Pipeline(管道)用来批量一次性发送多条 Redis 命令,减少多次网络往返 RTT 延迟。
原理
普通模式:客户端发一条命令 → 等待 Redis 返回结果,一次网络往返。多条命令就要多次网络来回,网络开销很大。 Pipeline:客户端一次性把 N 条命令打包,发给 Redis;Redis 收到全部命令,依次串行执行,最后一次性把所有结果打包返回客户端。
Pipeline 不保证原子性,命令之间可以被其他客户端命令插队。
作用
大幅降低网络往返次数,提升批量操作吞吐量。
适用场景
- 大批量简单读写操作,比如一次性写入上千条数据;
- 不需要上一条命令返回结果,才能执行下一条;
- 离线批量导入数据。
不适用场景
- Redis‑Cluster 集群环境,key 跨不同槽位,Pipeline 无法使用;
- 需要多条命令原子执行(该场景选 Lua 脚本)。
4、Lua 脚本作用、原子性、批量操作
Redis 支持执行 Lua 脚本,把多条命令写到一段 Lua 代码里发送给 Redis 服务端执行。
核心特性:原子性
Redis 执行 Lua 脚本期间,不会插入其他客户端的命令,整个脚本一次性串行跑完,具备原子性。 脚本执行没结束,其他请求全部阻塞等待,要么脚本所有命令全部成功,要么失败。
注意:脚本执行中报错,已经执行过的命令不会回滚,Redis 没有事务回滚机制。
作用
- 原子批量操作:多条命令需要一起执行、不可被打断,比如分布式锁的释放;
- 减少网络开销:多条逻辑命令封装成一次请求;
- 实现复杂业务逻辑:服务端完成判断 + 修改的组合逻辑;
经典使用场景
- 安全释放分布式锁(先判断 value,再删除 key,两条命令原子执行);
- 限流;
- 复杂的批量原子操作。
5、Lua 脚本 vs Pipeline 对比
| 对比项 | Pipeline 管道 | Lua 脚本 |
|---|---|---|
| 原子性 | 不原子,命令可被插队 | 原子执行 |
| 执行位置 | 客户端打包发送,redis 逐条跑 | Redis 服务端执行 |
| 能否依赖上一条结果 | 不能 | 可以,脚本内部可以拿到前一条命令返回值 |