redis面试题

Redis

基础概念:

1、Redis 是什么?为什么称之为高性能数据库

Redis 是一个开源、基于内存、键值对类型的非关系型数据库(NoSQL),支持数据持久化,同时提供主从复制、哨兵、集群等高可用方案,既可以当做缓存中间件,也可以承担部分业务数据存储。

高性能来源于 4 个核心因素:

  1. 绝大部分数据操作在内存完成,内存读写速度远高于磁盘;
  2. 命令执行采用单线程模型,规避多线程锁竞争、线程上下文切换的开销;
  3. 使用 IO 多路复用模型,单线程高效管理海量客户端连接;
  4. 底层全部采用经过高度优化的数据结构。

2、Redis 为什么快?四个核心原因

  1. 内存操作:所有热点数据存放内存,内存访问属于纳秒级,不存在磁盘 IO 耗时;
  2. 单线程执行命令:不需要加锁,没有多线程之间切换带来的 CPU 开销;
  3. IO 多路复用(epoll):单线程可以监听成千上万个客户端 socket 连接,没有空闲等待,最大化利用网络 IO;
  4. 底层高效数据结构: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 常用应用场景

  1. 热点缓存:减轻数据库压力,加速查询;
  2. 计数器:点赞数、浏览量,利用 String 原子自增;
  3. 分布式锁:跨服务进程之间互斥控制;
  4. 排行榜:ZSet 实现热搜、积分排行榜;
  5. 签到打卡:Bitmap 位图;
  6. UV 独立访客统计:HyperLogLog;
  7. 地理位置服务:附近的人、距离计算,Geo;
  8. 消息队列:List、Stream 实现异步解耦;
  9. 会话 Session 存储、限流

基础数据结构:

1、String 底层结构、最大容量、使用场景

String 的底层实现是 SDS(Simple Dynamic String,简单动态字符串) ,是 Redis 自定义的字符串结构,源码定义在 sds.h 中。 SDS 结构由头部和数据区组成:头部包含 len(已使用字节数)、alloc(总分配字节数)、flags(编码类型标记)字段;尾部 buf 数组存储实际数据。根据字符串长度不同,SDS 会自动选择 sdshdr8、sdshdr16、sdshdr32、sdshdr64 等不同大小的头部,最大化节省内存。

相比 C 原生字符串,SDS 的核心优势:

  1. O (1) 获取长度 :直接读取 len 字段,无需遍历到结束符;
  2. 二进制安全 :不以 \0 作为字符串结束标志,可存储图片、序列化字节等二进制数据;
  3. 减少内存重分配:支持空间预分配(小于 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 元素。

这种设计是对内存占用和操作性能的折中平衡:

  1. 相比纯双向链表:消除了大量节点指针的内存开销,减少内存碎片,内存利用率更高;
  2. 相比纯 ziplist:避免了元素过多时,插入删除触发大规模内存拷贝的性能问题;
  3. 同时保留了双向链表头尾增删 O (1) 的优势,也通过分段 ziplist 保证了随机访问的性能。

可通过配置项 list-max-ziplist-size 控制每个节点中 ziplist 的最大大小,以此平衡内存占用和读写效率。List 常用来实现简单消息队列、时间轴列表、最新消息流等场景。

3、Hash 底层结构、为什么适合存对象

Hash 的底层有两种编码结构,会根据元素数量和 value 大小自动切换:

  1. ziplist(压缩列表) :当哈希的字段数 ≤ 512 个(hash-max-ziplist-entries 默认值),且每个 value 长度 ≤ 64 字节(hash-max-ziplist-value 默认值)时使用,采用连续内存紧凑存储,内存效率极高。
  2. dict(哈希表 / 字典):当字段数量或 value 长度超过阈值时,自动切换为哈希表实现,通过数组 + 链表解决哈希冲突,增删查改平均时间复杂度 O (1)。

Hash 适合存储对象的核心原因:支持字段级独立操作 。 如果用 String 存储对象,通常是存入序列化后的 JSON 字符串,只要修改一个字段,就需要把整个字符串读出、反序列化、修改、再序列化写回 Redis,全量操作开销很大。 而 Hash 可以直接对单个字段执行 HSETHGET,只操作目标字段,不需要读写整个对象,开销更小、更灵活;小数据量下 ziplist 编码还能带来比 String 更高的内存利用率,非常适合存储用户信息、商品属性这类多字段的结构化数据。

4、Set 底层、特点、应用场景

Set 的底层采用双编码自动切换机制:

  1. intset(整数集合) :当集合内所有元素都是整数,且元素数量 ≤ 512 个(set-max-intset-entries 默认值)时使用,用连续数组存储,内存占用极低。
  2. 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 选择跳表而不用红黑树,主要有三个原因:

  1. 范围查询性能更优:ZSet 最核心的使用场景是范围遍历(比如排行榜取 TopN、分数区间查找)。跳表找到左边界后,只需沿底层链表向后线性遍历即可,实现简单且缓存局部性好;红黑树需要中序遍历 + 回溯,实现复杂且连续访问效率更低。
  2. 实现与维护更简单:跳表的插入、删除只需要修改相邻节点的指针,不需要红黑树的旋转、变色等平衡操作,代码实现、调试和维护成本更低。
  3. 单点查找的时间复杂度同为 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 可以实现:

  1. 计算两个位置之间的距离;
  2. 查询指定坐标半径范围内的所有地点(附近的人、附近商家);
  3. 按距离对地点进行排序等地理位置服务。

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、缓存穿透是什么?产生原因 + 四种解决方案

缓存穿透是指客户端查询的数据在缓存和数据库中都不存在,导致请求永远无法命中缓存,直接穿透缓存层打到数据库;

大量这类无效请求持续涌入,会快速压垮数据库。

产生原因:

  1. 恶意攻击:爬虫、攻击者批量传入不存在的 ID、非法参数,刻意打穿缓存;
  2. 业务异常:数据被物理删除、数据未同步,且缓存中没有对应空值兜底;
  3. 批量查询:批量传入大量非法主键,绕过缓存直接访问数据库。

四种解决方案:

  1. 空值缓存:数据库查询为空时,在缓存中写入一个短过期时间的空值,后续相同请求直接命中缓存,避免重复打库;
  2. 布隆过滤器:提前将所有合法主键存入布隆过滤器,请求先经过过滤器校验,不存在的 key 直接拦截,不访问缓存和数据库;
  3. 入口参数校验:在接口层对参数做合法性校验,比如 ID 为负数、格式非法的请求直接返回,不进入后续流程;
  4. 黑名单限流:对频繁发起非法请求的 IP、用户标识进行限流或拉黑,从源头减少穿透攻击。

2、缓存击穿是什么?产生原因 + 解决方案(互斥锁、热点永不过期)

回答: 缓存击穿是指单个高并发访问的热点 Key,在缓存过期的瞬间,大量并发请求同时涌入,缓存全部失效,请求瞬间全部打到数据库,导致数据库压力骤增,甚至宕机。

产生原因: 热点 Key 设置了固定过期时间,过期时刻恰好有大量请求访问;或是热点流量突发,而缓存刚好到期失效。

两种核心解决方案:

  1. 互斥锁方案:缓存失效时,只允许一个线程获取分布式锁去查询数据库并更新缓存,其余线程等待缓存刷新完成后再读取数据。优点是保证数据库不会被打穿,缺点是会增加请求延迟,降低吞吐量。
  2. 热点 Key 逻辑永不过期:物理层面不设置缓存过期时间,由业务代码维护逻辑过期;后台启动异步线程定时刷新热点数据,缓存永远不会出现失效瞬间,从根源避免击穿。

3、缓存雪崩是什么?产生原因 + 事前 / 事中 / 事后解决方案

回答: 缓存雪崩是指大量缓存 Key 在同一时间集中过期失效,或者 Redis 服务整体宕机,导致海量请求全部直接打到数据库,引发数据库压力过载甚至崩溃,造成服务连锁故障。

产生原因:

  1. 过期时间集中:批量设置缓存时使用了相同的过期时间,到期同时失效;
  2. Redis 故障:Redis 节点宕机、集群网络分区,缓存服务整体不可用;
  3. 流量突增:大促、活动导致缓存访问量骤增,过期后集中击穿数据库。

分阶段解决方案:

  • 事前预防:
    1. 过期时间打散:给缓存过期时间增加随机偏移值,避免大量 key 同时过期;
    2. 高可用架构:部署 Redis 哨兵 / 集群,避免单点故障,保障缓存服务可用性;
    3. 热点数据永不过期:核心热点 key 设置逻辑永不过期,后台异步刷新;
    4. 多级缓存:本地缓存 + Redis 缓存二级架构,降低 Redis 故障的影响。
  • 事中控制:
    1. 服务限流熔断:接入层开启限流、熔断、降级机制,控制数据库的请求量;
    2. 分布式锁保护:通过分布式锁控制数据库并发访问数,避免数据库被打垮。
  • 事后补救:
    1. 缓存预热:Redis 重启后,提前批量加载热点数据到缓存,快速恢复服务;
    2. 故障恢复:快速重启 Redis 集群,通过持久化文件恢复数据,逐步恢复流量。

4、Reds大key问题(没啥招数,其实就是做拆分)

大 key 指单个 Key 对应的 Value 体积过大:比如 String 类型 value 超过 100KB,集合类型(List/Hash/Set/ZSet)包含数万甚至数十万元素。

核心危害: 删除、过期、迁移大 key 时会阻塞 Redis 单线程,导致其他命令卡顿超时;集群中大 key 集中在单个节点,会造成内存倾斜、节点负载不均;读取大 key 还会占用大量带宽,引发网络拥塞。

核心解决方案:拆分

  1. 大字符串拆分:将大对象按业务维度拆分为多个小 key 分块存储,读取时按块拼接;
  2. 大集合拆分:将一个大集合按 ID 哈希、时间分片等维度拆分为多个小集合,分散存储压力;
  3. 渐进式处理:删除大集合时,使用 SCAN 命令分批遍历删除,避免一次性 DEL 阻塞主线程;
  4. 业务优化:禁止一次性读取整个大集合,统一使用分页、分批查询。

5、Redis热key问题(去搜京东hotkey探测机制)

热 key 指单个 Key 在短时间内收到超高并发访问,访问压力集中在 Redis 集群的单个节点上,导致该节点 CPU、网卡打满,成为整个集群的性能瓶颈。

京东 HotKey 探测机制(JD-HotKey)

京东开源了毫秒级热 key 探测框架,历经多次 618、双十一大促验证,核心是「客户端采集 + Worker 端计算 + etcd 全局同步 + 本地缓存兜底」的四层架构。

  1. 核心架构组成
    • etcd 集群:全局配置中心,存储热 key 判定规则、Worker 节点地址、已探测出的热 key 名单;提供监听订阅能力,保证配置和热 key 信息在所有服务节点间一致。
    • Client 端 Jar 包:嵌入业务服务,本地按时间窗口累加 key 访问次数,默认每 500ms 批量上报给 Worker;同时监听热 key 通知,收到后将热点数据缓存到本地 Caffeine 内存中。
    • Worker 端集群:独立部署的计算节点,接收各客户端上报的 key,通过滑动窗口统计访问频率;达到阈值则判定为热 key,推送给所有相关客户端并同步到 etcd。单台 16 核机器可稳定处理每秒 30 万以上的 key 探测。
    • Dashboard 控制台:可视化配置规则、查看热 key 列表、支持人工增删热 key。
  2. 判定逻辑:支持按业务维度配置差异化规则,比如商品 ID 1 秒内访问超 100 次、用户 ID 2 秒内超 20 次即判定为热 key。
  3. 热 key 处理方案
    • 本地二级缓存:热 key 数据缓存到应用本地内存,请求直接命中本地,绕过 Redis;
    • 多副本分流:将热点 key 复制多份分布到集群不同节点,分散访问压力;
    • 请求打散:同一热 key 生成多个副本 key,请求哈希到不同节点,均衡流量。

缓存与数据库一致性

1、缓存和数据库双写一致性几种策略

缓存与数据库双写一致性,核心是解决数据更新时,缓存和数据库两个独立存储之间的数据不同步问题。业界主流有四种策略,绝大多数业务场景下缓存只保证最终一致性,强一致性实现成本极高,不适用于缓存架构。

四种主流策略分别是:

  1. 先更新数据库,再删除缓存(业界最主流方案)
  2. 先删除缓存,再更新数据库
  3. 延时双删策略
  4. 基于 Canal 订阅 binlog 的异步缓存更新方案

2、先更新数据库再删缓存、先删缓存再更新数据库 优缺点

方案一:先更新数据库,再删除缓存(业界推荐)

优点:

  1. 并发脏数据概率极低。唯一可能产生脏数据的极端场景:缓存刚好失效时,读线程读到旧数据正要写回缓存,此时写线程更新完数据库并删除缓存,读线程随后把旧值写入缓存。但写数据库速度远慢于读,该场景发生概率极低。
  2. 实现简单,步骤少,业务侵入性低,出错概率小。

缺点:

  1. 如果删除缓存的操作失败,缓存会残留旧数据,导致持续不一致;
  2. 高并发写场景下频繁删除缓存,会导致缓存命中率下降。

方案二:先删除缓存,再更新数据库

优点:

  1. 逻辑直观,先让缓存失效,再更新底层数据,理论上后续读请求会自动拉取最新值。

缺点:

  1. 并发脏数据概率高。线程 A 先删除缓存,还未更新完数据库;此时线程 B 查询,缓存未命中,读取数据库旧值并写回缓存;等线程 A 更新完数据库,缓存中就会长期留存旧数据,不一致窗口非常大。
  2. 无法解决数据库更新期间的并发读问题,数据不一致风险远高于前者。

3、延时双删策略原理

延时双删是对「先删缓存,再更新数据库」方案的优化,目的是降低并发脏数据的概率,属于最终一致性方案

执行步骤

  1. 第一步:先删除缓存;
  2. 第二步:更新数据库;
  3. 第三步:线程休眠一小段时间(通常几百毫秒到 1 秒,根据业务读耗时设定),然后第二次删除缓存

核心原理

休眠时间需要略大于一次普通业务读请求的耗时。 目的是等待「数据库更新过程中,其他并发线程读取旧数据并写入缓存」这个过程完成;第二次删除缓存,就可以把这段时间内产生的脏缓存清理掉,避免脏数据长期停留在缓存中。

注意点

  • 只能降低不一致概率,无法完全消除;如果第二次删除失败,依然会残留脏数据;
  • 同步休眠会阻塞写线程、降低吞吐量,生产环境通常异步执行第二次删除。

4、如何保证强一致性?Canal 订阅 binlog 方案

首先明确:缓存天生不适合严格强一致性场景。如果要求绝对强一致,最可靠的方式是直接读写数据库,不走缓存;缓存架构通常只追求秒级以内的最终一致性。

Canal 订阅 binlog 方案原理

Canal 是阿里巴巴开源的 MySQL binlog 订阅工具,它模拟 MySQL 从节点协议,实时接收并解析数据库的二进制日志。

完整方案流程:

  1. 业务服务只负责操作数据库,不直接操作缓存,业务代码与缓存逻辑完全解耦;
  2. Canal 服务实时监听 MySQL 的 binlog 日志,解析出数据的增删改变更;
  3. 消费端接收到变更事件后,异步去删除或更新对应的 Redis 缓存。

方案优势

  1. 业务层无感知,缓存更新逻辑和业务解耦,避免业务代码里到处穿插缓存操作;
  2. 基于 binlog 保证变更事件不丢失,缓存更新的可靠性更高;
  3. 一致性延迟低,通常在毫秒到秒级,是工业界非常成熟的最终一致性方案。

关于强一致性的补充

如果必须实现缓存与数据库的强一致,需要引入分布式事务(如 2PC、TCC),同时包裹数据库操作和缓存操作,保证二者同时成功或同时回滚;但性能损耗极大、复杂度极高,几乎不会在普通缓存场景使用。

过期/淘汰策略

1、Redis 键过期删除三种策略:定时删除、惰性删除、定期删除

Redis 针对设置了过期时间的 key,设计了三种过期删除策略,最终采用「惰性删除 + 定期删除」的组合方案。

  1. 定时删除 为每个过期 key 创建定时器,过期时间一到立刻删除 key 释放内存。 优点:内存释放及时; 缺点:大量定时器会占用大量 CPU 资源,严重影响 Redis 主流程性能,Redis 没有采用该策略。
  2. 惰性删除 key 过期后不主动删除,等客户端访问这个 key 时,再检查是否过期,过期则删除并返回空。 优点:几乎不占用 CPU 额外开销,对性能影响极小; 缺点:如果过期 key 永远不被访问,就会一直占用内存,造成内存浪费。
  3. 定期删除 每隔固定时间,从过期 key 集合中随机抽取一批 key 检查,删除其中过期的 key。 优点:通过控制执行频率和时长,折中平衡 CPU 开销和内存回收效率; 缺点:无法精准控制过期 key 的内存回收速度。

👉 Redis 实际采用:惰性删除 + 定期删除结合,既保证主线程性能,又尽可能回收过期内存。

2、Redis 内存满后八大淘汰策略分别是什么?

当 Redis 内存占用达到 maxmemory 上限,再写入新数据时会触发内存淘汰,共 8 种策略,分为「全量 key 范围」和「仅过期 key 范围」两大类。

  1. noeviction(默认策略):不淘汰任何 key,新的写请求直接返回错误,只读操作正常执行。
  2. allkeys-lru:在所有 key 中,淘汰最近最少使用(Least Recently Used)的 key。
  3. volatile-lru:只在设置了过期时间的 key 中,淘汰最近最少使用的 key。
  4. allkeys-lfu:在所有 key 中,淘汰访问频次最低(Least Frequently Used)的 key。
  5. volatile-lfu:只在设置了过期时间的 key 中,淘汰访问频次最低的 key。
  6. allkeys-random:在所有 key 中,随机挑选 key 淘汰。
  7. volatile-random:只在设置了过期时间的 key 中,随机挑选淘汰。
  8. 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 既没被访问、也没被定期抽样抽到,就会一直占用内存空间。

主从间的过期同步:

主从架构中,过期删除的主动权完全在主节点

  1. 主节点检测到 key 过期并删除后,会向所有从节点发送一条 DEL 命令,同步删除从节点上的对应 key;
  2. 从节点自身不会主动删除过期 key,即使键已经过期,从节点也不会自行处理,必须等待主节点的同步命令;
  3. 客户端访问从节点时,Redis 会检查键是否过期,过期则返回空结果,但不会在从节点本地删除该键。

持久化 RDB & AOF(超级高频

1、RDB 是什么?原理、触发方式、优缺点

RDB 是 Redis 的快照式持久化 机制,指在某个指定时间点,将 Redis 内存中的全量数据生成一份二进制格式的快照文件(默认文件名 dump.rdb)写入磁盘,实现数据落盘持久化。

核心原理:生成内存数据的完整二进制镜像,以紧凑的格式存储,恢复时直接加载到内存即可完成数据还原。

触发方式分为四类:

  1. save 命令:主线程阻塞式执行快照生成,期间无法响应客户端请求,生产环境禁用。
  2. bgsave 命令:fork 出一个子进程,由子进程后台生成 RDB 文件,主进程继续处理请求,是最常用的触发方式。
  3. 配置自动触发:通过 save m n 配置规则,m 秒内发生 n 次写操作则自动触发 bgsave。
  4. 被动触发:Redis 正常关闭(shutdown)、主从复制全量同步时,都会自动生成 RDB 快照。

优缺点

  • 优点:文件体积小、数据恢复速度极快;适合冷备份、全量数据迁移。
  • 缺点:两次快照间隔内宕机会丢失全部增量数据;fork 子进程时会短暂阻塞主线程,内存越大阻塞时间越长。

2、AOF 是什么?日志刷写策略三种

AOF(Append Only File)是 Redis 的增量日志式持久化 机制,以文本形式记录每一条对数据有修改的写命令,追加写入到 appendonly.aof 文件中;宕机后通过逐条回放日志中的命令恢复数据。

三种日志刷盘策略(appendfsync

  1. always:每执行一条写命令,就立刻调用 fsync 刷写到磁盘。 特点:数据安全性最高,最多丢失一条命令的数据;但磁盘 IO 开销极大,性能最差。
  2. everysec(默认配置):每秒执行一次 fsync 刷盘。 特点:性能与安全性的折中,最多丢失 1 秒的数据,是生产环境的默认选择。
  3. 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 日志格式追加到文件末尾。

核心优势

  1. 恢复速度快:开头的 RDB 部分可直接加载,无需逐条回放全量命令,恢复速度接近纯 RDB;
  2. 数据安全性高:增量部分保留 AOF 日志,最多丢失 1 秒数据,安全性与纯 AOF 一致; 兼顾了恢复效率和数据可靠性,是目前生产环境推荐的持久化方案。

6、Redis 宕机后数据恢复流程

Redis 重启恢复数据时,会按照「优先 AOF、次之 RDB」的优先级执行:

  1. 首先检查是否开启了 AOF 持久化:
    • 如果开启了 AOF,优先加载 AOF 文件恢复数据;因为 AOF 的数据完整性更高,丢失的数据更少。
  2. 如果未开启 AOF,才会加载 RDB 快照文件恢复数据。
  3. 如果 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 自动释放,此时其他客户端可以获取到锁,引发并发安全问题;同时原业务线程执行结束后,还可能误删除其他线程的锁。

解决方案:

  1. 合理预估过期时间:根据业务执行耗时设置过期时间,预留一定冗余,降低超时概率;
  2. 锁续约机制:引入后台定时线程,业务未执行完成时自动延长锁的过期时间(即看门狗机制);
  3. 解锁身份校验:解锁时必须校验锁的持有者标识,只能释放自己加的锁,避免误删他人的锁;
  4. 业务兜底校验:锁超时后,业务执行结果需要做并发校验,避免脏数据写入。

3、锁续约 / 续命怎么做?Redisson 看门狗原理

锁续约的核心是通过后台定时线程,动态延长锁的过期时间,保证业务执行完成前锁不会提前释放。Redisson 中的标准实现就是看门狗(WatchDog)机制

看门狗原理:

  1. 加锁成功后,Redisson 会启动一个后台定时线程(看门狗线程);
  2. 默认锁的过期时间为 30 秒,看门狗每隔 1/3 过期时间(即 10 秒) 检查一次:如果当前线程仍然持有锁,就重新设置锁的过期时间为 30 秒,完成续命;
  3. 当业务执行完成、主动释放锁后,看门狗线程会被停止,不再续期。

作用:彻底解决业务执行耗时超过锁过期时间导致的锁提前释放问题,无需人工精准预估超时时间。

4、主从架构下分布式锁锁失效问题

在 Redis 主从 + 哨兵架构下,存在锁失效的典型场景: 客户端在 Master 主节点加锁成功后,锁数据还未同步到 Slave 从节点,此时 Master 宕机;哨兵执行故障转移,将一个 Slave 升级为新的 Master;由于新主节点上没有这把锁的数据,其他客户端就可以再次加锁成功,导致同一时间存在两把锁,锁失效。

解决方案:

  1. Redlock 红锁算法:部署多个相互独立、无主从关系的 Redis 节点,客户端向所有节点请求加锁,超过半数节点加锁成功才算真正获取锁;故障时单个节点宕机不影响整体锁状态,可靠性更高,但性能开销大。
  2. 容忍最终一致:如果业务可以接受极低概率的锁冲突,主从架构的锁失效概率很低,可通过业务兜底校验兼容。
  3. 集群架构同理:Redis Cluster 集群每个槽位同样有主从,锁失效逻辑一致,强一致场景同样推荐红锁方案。

5、Redisson 可重入锁原理

可重入锁指同一个线程可以多次获取同一把锁,不会出现自己阻塞自己的情况。Redisson 可重入锁底层基于 Redis Hash 结构 实现。

实现原理:

  1. Hash 的 key 是锁的名称,Hash 的 field 是锁持有者的唯一标识(线程 ID + 客户端 UUID),value 是该线程的重入次数。
  2. 加锁逻辑:
    • 锁不存在:直接加锁成功,重入次数设为 1,同时启动看门狗;
    • 锁已存在且 field 匹配当前线程:重入次数 +1,重置锁过期时间;
    • 锁已存在且持有者不是当前线程:加锁失败,阻塞等待。
  3. 解锁逻辑:
    • 校验持有者身份,匹配成功则重入次数 -1;
    • 当重入次数递减到 0 时,真正删除锁的 key,停止看门狗线程。

主从复制 & 集群

1、Redis 主从复制原理、全量同步 / 增量同步

回答: 主从复制是 Redis 高可用的基础,主节点负责写,从节点负责读,实现读写分离和数据备份。复制分为全量同步和增量同步两种模式。

全量同步:一般发生在从节点初次连接、或断线后无法增量同步的场景。

  1. 从节点向主节点发送同步请求;
  2. 主节点执行 bgsave 生成 RDB 快照文件,同时将快照期间的新写命令写入复制缓冲区;
  3. 主节点将 RDB 文件发送给从节点,从节点清空本地数据并加载 RDB;
  4. 主节点再把缓冲区的增量命令发送给从节点,从节点执行命令补齐数据,同步完成。

增量同步:正常运行阶段的持续同步。 主节点每执行一条写命令,就会异步将命令发送给所有从节点,从节点执行命令,保证主从数据一致。

2、主从复制复制积压缓冲区作用

复制积压缓冲区(repl_backlog)是主节点维护的一个固定大小的环形缓冲区,用来存储最近一段时间的写命令以及对应的复制偏移量。

核心作用:

  1. 支持断线增量同步:主从短暂断线重连后,不需要重新全量同步,只需要根据从节点上报的偏移量,从缓冲区中取出缺失的命令补发即可,大幅提升断线恢复效率。
  2. 缓冲复制延迟:从节点网络延迟、阻塞时,命令可以暂存在缓冲区,避免主节点阻塞。

缓冲区大小由 repl-backlog-size 配置,太小会导致断线后经常触发全量同步;太大则浪费内存。

3、主从同步断线后怎么增量同步

主从断线重连后的增量同步流程如下:

  1. 从节点重新连接主节点后,向主节点上报自己当前的复制偏移量
  2. 主节点校验该偏移量是否还存在于复制积压缓冲区中;
  3. 如果偏移量仍在缓冲区内:主节点将偏移量之后的所有写命令发送给从节点,从节点执行后完成增量同步;
  4. 如果偏移量已经被新数据覆盖(缓冲区是环形的,旧数据被覆盖):说明断线时间过长,无法增量同步,主节点会触发全量同步流程。

4、哨兵 Sentinel 作用、原理、故障转移流程

哨兵是独立运行的进程,组成哨兵集群,实现 Redis 主从的自动故障转移,解决主节点单点故障问题。

三大核心作用

  1. 监控:持续检测所有主、从节点是否存活;
  2. 通知:节点故障时通知管理员和客户端;
  3. 自动故障转移:主节点宕机后,自动选举一个从节点升级为新主节点,保证服务可用。

故障转移完整流程

  1. 单个哨兵检测到主节点无响应,标记为主观下线
  2. 多个哨兵都确认主节点下线,达成多数共识后,标记为客观下线
  3. 哨兵集群内部投票选举出一个领头哨兵,负责执行故障转移;
  4. 领头哨兵从存活的从节点中选出最优节点,升级为新的主节点;
  5. 命令其余从节点切换到新主节点进行复制;
  6. 通知客户端主节点变更,同时将旧主节点标记为下线,恢复后自动变为新主的从节点。

5、哨兵怎么选主、投票机制

哨兵选举分为两部分:领头哨兵选举新主节点选举

领头哨兵选举(基于 Raft 协议思想)

  1. 哨兵发现主节点客观下线后,会发起选举,拉票请求其他哨兵投票给自己;
  2. 每个哨兵在一轮选举中只能投一票,先到先得;
  3. 获得超过半数哨兵投票的节点,成为领头哨兵,负责执行故障转移;
  4. 如果选举失败,等待超时后开启新一轮选举。

新主节点选举规则(优先级从高到低)

  1. 从节点优先级slave-priority 配置值越小,优先级越高;
  2. 复制偏移量:偏移量越大,数据越完整越新,优先选;
  3. 运行 ID:以上都相同时,选 runid 更小的从节点。

6、Redis Cluster 集群原理、哈希槽 16384

Redis Cluster 是无中心的分布式分片集群,解决单机内存上限问题,实现数据分布式存储和高可用。

核心原理是哈希槽分片

  1. 整个集群预划分 16384 个哈希槽,编号 0 ~ 16383;
  2. 每个主节点负责管理其中一部分哈希槽;
  3. 写入数据时,对 key 做 CRC16(key) % 16384 计算,得到对应的槽号,直接路由到负责该槽的主节点执行。

每个主节点可以挂载多个从节点,主节点故障时,集群自动将对应从节点升级为主,实现分片级别的高可用。

7、集群槽位分配、分片规则

分片规则: 数据不直接绑定节点,而是通过哈希槽做中间层。key → CRC16 → 哈希槽 → 节点,实现数据和节点的解耦,方便后续扩容缩容、槽位迁移。

注意:多键批量操作(如 MGET、MSET)要求所有 key 必须在同一个哈希槽,否则会报错。

槽位分配规则

  1. 集群初始化时,槽位可以自动或手动分配,尽量均匀分布到所有主节点,保证各节点数据量均衡;
  2. 每个哈希槽有且仅属于一个主节点,不允许交叉;
  3. 只有全部 16384 个槽都有节点负责,集群才处于正常可用状态。

8、集群扩容、缩容原理

回答: 集群扩容和缩容的核心都是哈希槽迁移,数据跟随槽位移动,过程中集群持续对外提供服务。

扩容原理

  1. 新的主节点加入集群,成为空节点;
  2. 从现有各个主节点上,分别迁移一部分哈希槽到新节点;
  3. 迁移过程中,数据逐步移动,槽位归属逐步更新;
  4. 所有槽迁移完成后,新节点正式承担读写服务,扩容完成。

缩容原理

  1. 将待下线主节点的所有哈希槽,全部迁移到其他存活的主节点;
  2. 所有槽迁移完毕、数据完全转移后,将该节点从集群中移除;
  3. 缩容完成,剩余节点继续提供服务。

9、为什么是 16384 个哈希槽,不是更大?

主要有两个核心原因:

  1. 心跳包大小控制 集群节点之间的心跳消息会携带完整的槽位图,槽位数量对应位图大小。16384 个 bit = 2KB,心跳包体积很小,网络开销低;如果扩大到 65536 个槽,位图就变成 8KB,节点越多心跳带宽占用越高,得不偿失。
  2. 集群规模匹配 Redis 集群官方推荐节点数量上限约 1000 个,16384 个槽已经足够让上百个节点均匀分片,槽数量过多没有实际收益,反而增加管理开销。

补充:Redis 作者也明确说明,16k 槽对于 1000 节点规模的集群已经完全够用,是性能和分片粒度的最佳平衡。

10、集群高可用、脑裂问题怎么解决

集群高可用实现: 每个主节点都配置至少一个从节点作为备份;当主节点故障下线时,集群通过类似哨兵的故障转移机制,自动将对应从节点升级为新主节点,接管该分片的所有槽位,继续对外提供服务,实现分片级高可用。

脑裂问题与解决方案 脑裂指网络分区后,旧主节点和集群其他节点断开,但自身仍在运行,客户端继续向其写入数据;网络恢复后,旧主节点变为从节点,分区期间写入的数据会全部丢失。

解决方案:通过配置 min-replicas-to-write 参数。 设置主节点必须至少拥有 N 个在线的从节点,才能接收写请求。网络分区后,主节点失去从节点连接,数量不满足阈值,就会自动拒绝写入,避免脑裂产生脏数据,牺牲部分可用性保证数据一致性。

网络模型 & 底层原理

1、Redis 多路复用 IO 模型:epoll 原理

Redis 是单线程事件驱动模型,核心依靠 IO 多路复用(epoll) 同时监听成千上万条客户端连接,实现高性能网络 IO。

epoll 原理

epoll 是 Linux 下多路复用模型,核心思想:单线程监听多个文件描述符(客户端 socket),只在连接就绪(可读 / 可写)的时候才去处理事件,不需要轮询遍历所有连接。 分为三步:

  1. epoll_create:创建一个 epoll 实例;
  2. epoll_ctl:把所有客户端 socket 连接注册添加到 epoll 监听列表;
  3. epoll_wait:阻塞等待事件就绪;当某个客户端发来请求、socket 可读,epoll 就主动返回就绪的连接列表。

Redis 使用 epoll 带来的好处

  1. 不需要开辟大量线程去处理每一个客户端,避免线程切换开销;
  2. 连接即使空闲,也几乎不消耗 CPU;
  3. 支持海量并发连接,所以 Redis 可以做到几万客户端同时连接。

Redis 单线程只是执行命令是单线程,网络 IO 靠 epoll 多路复用来管理大量连接。

2、Redis 6.0 多线程做了什么?网络多线程、命令还是单线程

Redis6.0 引入的多线程仅仅用来处理网络 IO,执行命令仍然是单线程串行执行

6.0 多线程分工

  1. IO 线程(多线程):负责网络数据的读写。
    • 从 socket 中读取客户端发来的请求数据;
    • 将命令执行后的响应结果写回 socket;
    • 多线程并发分担网络 IO 读写的压力。
  2. 主线程(单线程) :拿到解析好的命令,串行执行所有 Redis 命令。 SET、GET、LPOP 等操作依然只能主线程跑,命令执行不会并发。

为什么命令不做多线程?

  1. Redis 命令很多操作都是对共享数据修改,多线程就要加锁,锁竞争反而降低性能;
  2. 单线程无锁,代码简单,不容易出现并发安全问题。

一句话总结:IO 多线程负责收包发包;命令执行依旧单线程。

3、Pipeline 管道作用、原理、适用场景

回答: Pipeline(管道)用来批量一次性发送多条 Redis 命令,减少多次网络往返 RTT 延迟

原理

普通模式:客户端发一条命令 → 等待 Redis 返回结果,一次网络往返。多条命令就要多次网络来回,网络开销很大。 Pipeline:客户端一次性把 N 条命令打包,发给 Redis;Redis 收到全部命令,依次串行执行,最后一次性把所有结果打包返回客户端。

Pipeline 不保证原子性,命令之间可以被其他客户端命令插队。

作用

大幅降低网络往返次数,提升批量操作吞吐量。

适用场景

  1. 大批量简单读写操作,比如一次性写入上千条数据;
  2. 不需要上一条命令返回结果,才能执行下一条;
  3. 离线批量导入数据。

不适用场景

  1. Redis‑Cluster 集群环境,key 跨不同槽位,Pipeline 无法使用;
  2. 需要多条命令原子执行(该场景选 Lua 脚本)。

4、Lua 脚本作用、原子性、批量操作

Redis 支持执行 Lua 脚本,把多条命令写到一段 Lua 代码里发送给 Redis 服务端执行。

核心特性:原子性

Redis 执行 Lua 脚本期间,不会插入其他客户端的命令,整个脚本一次性串行跑完,具备原子性。 脚本执行没结束,其他请求全部阻塞等待,要么脚本所有命令全部成功,要么失败。

注意:脚本执行中报错,已经执行过的命令不会回滚,Redis 没有事务回滚机制。

作用

  1. 原子批量操作:多条命令需要一起执行、不可被打断,比如分布式锁的释放;
  2. 减少网络开销:多条逻辑命令封装成一次请求;
  3. 实现复杂业务逻辑:服务端完成判断 + 修改的组合逻辑;

经典使用场景

  1. 安全释放分布式锁(先判断 value,再删除 key,两条命令原子执行);
  2. 限流;
  3. 复杂的批量原子操作。

5、Lua 脚本 vs Pipeline 对比

对比项 Pipeline 管道 Lua 脚本
原子性 不原子,命令可被插队 原子执行
执行位置 客户端打包发送,redis 逐条跑 Redis 服务端执行
能否依赖上一条结果 不能 可以,脚本内部可以拿到前一条命令返回值
相关推荐
小小龙学IT20 分钟前
Qt 元对象系统(Meta-Object System)深度解析:从 MOC 到反射式编程
数据库·qt
chen<>37 分钟前
C++ 六种 memory_order:从原子性到线程间可见性.md
java·linux·开发语言·c++
十年Java程序媛41 分钟前
SpringBoot3 + JDK17 实战复盘:HikariCP 连接耗尽,虚拟线程场景额外注意事项
java·spring boot
想带你从多云到转晴1 小时前
MySQL重点梳理
数据库·mysql
胖头鱼的鱼缸(尹海文)1 小时前
胖头鱼的技术专栏-465 数据库能力上移还是下沉:库变弱了,应用就变重了(20260828)
数据库
二十雨辰1 小时前
[Java]-JVM面试题
java·开发语言·jvm
2601_962181961 小时前
Redis Redis介绍、安装 - Redis客户端
数据库·redis·postman
cfm_29141 小时前
ConcurrentHashMap 线程安全机制与 JDK 1.8 演进
java·开发语言·安全
tqs_123451 小时前
值传递与引用传递
java·开发语言·python