🏠 Redis原理面试题|高频考点深度解析 | 📅 当前时间: | 📧 技术支持:contact@kaoacca.cn

Redis原理面试题|高频考点深度解析|程序员必知核心机制

从底层数据结构到高可用架构,系统梳理Redis技术栈核心原理,结合真实面试场景与源码级解析,助你构建完整的Redis知识体系,从容应对技术面试挑战。

一、核心数据结构与底层实现原理

1.1 SDS(Simple Dynamic String):Redis字符串的底层实现

Redis没有直接使用C语言的字符串(以空字符结尾的字符数组),而是实现了自己的简单动态字符串(Simple Dynamic String, SDS)。SDS的设计解决了C字符串的多个固有缺陷,是Redis高性能和安全性的基石之一。

typedef struct sdshdr { int len; // 已使用字节数 int free; // 未使用字节数 char buf[]; // 柔性数组,实际存储字符串内容 } sdshdr;

SDS相比C字符串的三大优势:

  • 常数时间获取长度:通过len字段直接返回,O(1)时间复杂度;C字符串需遍历计算,O(n)
  • 避免缓冲区溢出:修改前检查剩余空间,不足时自动扩展,防止内存越界
  • 二进制安全:不以\0结尾,可存储任意二进制数据(如图片、JSON)
⚠️ 面试高频点:Redis字符串最大支持512MB,而C字符串受限于内存连续性与\0终止符。

SDS的空间预分配策略

当SDS需要扩展时,Redis采用“空间预分配”避免频繁重分配:

  • 若修改后长度 < 1MB,则新分配空间 = 原长度(即free = len)
  • 若修改后长度 ≥ 1MB,则新分配空间 = 1MB(即free = 1MB)

这种策略使连续追加操作(如 APPEND)摊还时间复杂度为O(1),大幅提升写入性能。

惰性空间释放(Lazy Space Release)

当SDS缩短内容时,并不立即回收多余内存,而是更新free字段,保留空间供下次使用。这减少了内存分配/释放的开销,尤其适用于频繁增删的场景。

1.2 字典(Dict)与哈希表(Hash Table)

Redis的字典是其所有键值对存储的核心结构,底层基于哈希表实现,采用链地址法解决哈希冲突。其设计兼顾了动态扩容、渐进式rehash与内存效率。

typedef struct dict { dictType *type; void *privdata; dictht ht[2]; // 两个哈希表,用于渐进式rehash long rehashidx; // rehash进度,-1表示未在rehash int iterators; // 当前活跃迭代器数量 } dict; typedef struct dictht { dictEntry **table; // 哈希桶数组 unsigned long size; // 桶数组大小(2的幂) unsigned long sizemask; // size - 1,用于哈希索引 unsigned long used; // 已使用节点数 } dictht;

哈希冲突处理:链地址法

每个哈希桶指向一个链表(dictEntry),新节点插入链表头部(头插法),保证插入O(1)。链表节点结构:

typedef struct dictEntry { void *key; // 键 union { void *val; uint64_t u64; int64_t s64; double d; } v; // 值(联合体支持多种类型) struct dictEntry *next; // 下一个节点(链表指针) } dictEntry;

渐进式rehash(Incremental Rehash)

为避免一次性rehash导致主线程阻塞,Redis采用渐进式策略:

  1. 初始化:创建新哈希表ht[1],设置rehashidx=0
  2. 增量迁移:每次执行增删改查时,额外迁移ht[0][rehashidx]中的链表到ht[1]
  3. 后台迁移:定时任务(serverCron)每毫秒迁移100个桶
  4. 完成:ht[0].used=0后,释放ht[0],ht[1]成为新ht[0],rehashidx=-1
⚠️ 关键特性:rehash期间,查询可能需遍历两个哈希表(先查ht[1],再查ht[0]),但增删改仅操作ht[1],确保数据一致性。

哈希负载因子与自动扩容

状态 负载因子(used/size) 触发条件 扩容策略
正常状态 < 1.0 无压力 维持原大小
负载过高 >= 1.0(且未BGSAVE/BGREWRITEAOF) 主动触发rehash size × 2
负载极高 >= 5.0 强制触发rehash size × 2(即使有子进程)

扩容后size为2的幂(如16→32→64…),sizemask = size - 1,便于通过位运算快速计算索引:index = hash & sizemask。

1.3 Redis五大数据结构的底层映射

Redis对外提供5种基础数据类型,但底层仅使用7种数据结构,通过编码优化实现高效存储:

Redis类型 底层数据结构 触发条件 典型编码
String SDS、int、embstr 整数且长度≤20;短字符串(≤39字节) int / embstr / raw
List quicklist(双向链表+压缩列表) 长度≥2或元素≥512字节 quicklist
Set intset(整数集合)或hashtable 全为整数且数量≤512 → intset intset / hashtable
Hash ziplist(压缩列表)或hashtable 字段数≤512且值≤64字节 → ziplist ziplist / hashtable
ZSet ziplist或skiplist+dict 元素数≤128且值≤64字节 → ziplist ziplist / skiplist
⚠️ 重点提示:ziplist是Redis的紧凑内存布局,相邻节点无指针开销,但修改可能导致连锁更新(级联淘汰),性能不稳定。因此Redis 6.0后逐步用listpack替代(仅单向链接)。

二、持久化机制:RDB与AOF深度对比

2.1 RDB(Redis Database)快照持久化

RDB是Redis的二进制快照文件,记录某一时刻的全部数据。其核心在于fork子进程完成持久化,避免阻塞主线程。

RDB触发方式

  • SAVE:阻塞主线程,同步生成RDB(生产环境禁用)
  • BGSAVE:fork子进程异步持久化(默认方式)
  • 自动触发:根据save配置(如900秒1次修改)
# redis.conf 配置示例 save 900 1 # 900秒内至少1次修改 save 300 10 # 300秒内至少10次修改 save 60 10000 # 60秒内至少10000次修改

RDB文件结构

包含:REDIS魔数 → 版本号 → DB内容(键值对)→ EOR(EOF标记)→ CRC64校验和。

优点与缺点

优势 劣势
文件紧凑,恢复速度快 可能丢失最后一次快照后的数据
适合冷备份与灾难恢复 fork子进程时内存翻倍(写时复制COW)
兼容性强,跨版本迁移稳定 大实例生成慢(秒级阻塞fork)

2.2 AOF(Append Only File)日志持久化

AOF以命令日志形式记录写操作,通过重放命令恢复数据,提供更高数据安全性。

AOF写入策略

# redis.conf 配置 appendonly yes appendfsync everysec # 推荐:每秒同步一次 # appendfsync always # 同步写磁盘(慢) # appendfsync no # 依赖OS(不安全)

重写机制(AOF Rewrite)

为避免AOF文件无限增长,Redis支持重写:生成新文件,仅保留最小命令集(如 SET k v 替代多次 APPEND)。

重写触发条件:

  • 手动触发:BGREWRITEAOF
  • 自动触发(满足任一):
  • 当前AOF文件大小 > auto-aof-rewrite-min-size(默认64MB)
  • 且增长比例 > auto-aof-rewrite-percentage(默认100%)
⚠️ 关键点:重写过程不阻塞主线程!fork子进程写入新文件,主线程继续处理请求,新写入命令暂存缓冲区,重写完成后追加到新文件末尾。

AOF重写流程

  1. 主线程fork子进程,子进程遍历当前数据库生成新AOF
  2. 主线程将新写入命令暂存aof_rewrite_buf
  3. 子进程写完后,主线程将aof_rewrite_buf追加到新文件
  4. 原子替换旧AOF文件

2.3 RDB vs AOF:选择策略

官方推荐组合使用:RDB用于快速恢复+AOF用于数据安全。

场景 推荐方案 理由
缓存型应用 RDB为主 数据可重建,无需强一致性
计数/订单系统 AOF+everysec 丢失数据≤1秒,性能与安全平衡
混合部署集群 主节点AOF,从节点RDB 主节点保障数据安全,从节点快速同步

混合持久化(Redis 4.0+)

开启 aof-use-rdb-preamble yes 后,AOF文件开头插入RDB快照,后续追加增量命令。优势:

  • 重启速度提升50%以上(RDB部分快速加载)
  • 数据丢失窗口缩小至1秒(AOF部分补偿)
  • 兼容纯AOF恢复流程

三、内存管理与淘汰策略

3.1 内存分配器:jemalloc vs malloc

Redis默认使用jemalloc(5.0+),替代早期malloc,原因如下:

  • 减少内存碎片:通过 arenas 与 bin 管理小内存分配
  • 支持内存回收:主动调用 malloc_trim 归还内存给OS
  • 多线程友好:无全局锁,提升并发性能
⚠️ 内存碎片率:info memory 中的 fragmentation_ratio = used_memory_rss / used_memory。>1.5 表示碎片严重,需重启或手动触发 MEMORY PURGE。

3.2 过期键删除策略

Redis采用定期删除 + 惰性删除组合策略:

定期删除(Active Expiration)

serverCron任务每100ms执行一次,随机检查20个键的过期时间,删除已过期键。若过期键比例高,会增加检查数量,避免内存堆积。

惰性删除(Lazy Deletion)

访问键时检查是否过期,若过期立即删除并返回空。确保不会读到已过期数据。

内存不足时的淘汰策略

策略名称 适用场景 算法说明
noeviction 写操作返回错误 不淘汰,OOM时写操作失败(默认)
allkeys-lru 通用缓存 从所有键中淘汰最近最少使用的 Redis 3.0后使用近似LRU(随机采样5个键,淘汰最久未访问)
allkeys-lfu 热点数据优先 基于访问频率(LFU),计数衰减机制 每分钟衰减一次,避免冷数据误淘汰
volatile-ttl 预设过期时间的数据 仅从设置了TTL的键中淘汰,TTL最小优先
# 配置示例 maxmemory allkeys-lru maxmemory-samples 10 # LRU采样数,默认5,越大越准但耗CPU

3.3 内存回收:对象共享与延迟释放

共享对象池(Shared Objects):Redis启动时创建0~9999的整数对象,所有相同整数键共享同一对象,节省内存。

⚠️ 仅整数键支持共享(如 SET k 100),字符串键需满足 OBJECT ENCODING == embstr 且长度≤39字节。

延迟释放(Lazy Free):Redis 4.0引入,异步释放大对象(如大Key、慢查询),避免阻塞主线程:

  • UNLINK key:异步删除
  • FLUSHALL ASYNC / FLUSHDB ASYNC
  • 配置项:lazyfree-lazy-eviction yes

四、高可用方案:主从复制与哨兵

4.1 主从复制原理与全量/部分同步

Redis复制基于 PSYNC 命令实现,支持全量复制与部分复制。

全量复制(Full Resynchronization)

触发条件:首次复制或主节点历史复制偏移量丢失(buffer过小)。

  1. 从节点发送 PSYNC ? -1(问号表示未知runid,-1表示未知offset)
  2. 主节点返回 FULLRESYNC <runid> <offset>,并启动BGSAVE
  3. 从节点接收RDB文件,清空自身数据,加载RDB
  4. 主节点发送缓冲区中的写命令(复制buffer)

部分复制(Partial Resynchronization)

触发条件:网络短暂中断,主节点保留足够复制偏移量。

  1. 从节点发送 PSYNC <runid> <offset>
  2. 主节点检查runid匹配且offset在复制buffer范围内
  3. 返回 +CONTINUE,仅发送缺失部分数据
⚠️ 复制buffer大小:配置 repl-backlog-size(默认1MB)。若写入量大,需调大避免全量同步。

4.2 哨兵(Sentinel)高可用架构

Sentinel实现自动故障转移,核心功能:

  • 监控:每秒向主/从节点发送PING,超时(down-after-milliseconds)标记为ODOWN
  • 通知:通过发布订阅通知客户端
  • 故障转移:选举从节点升级为主节点
  • 配置更新:客户端通过 SENTINEL GET-MASTER-ADDR-BY-NAME 获取新主节点

故障转移流程

  1. 主节点SDOWN(主观下线)→ 多个Sentinel确认ODOWN(客观下线)
  2. 选举领头Sentinel(Raft算法)
  3. 领头Sentinel选择最优从节点(优先级、复制offset、运行时间)
  4. 从节点执行 SLAVEOF NO ONE 升级为主节点
  5. 其他从节点执行 SLAVEOF <新主IP> <端口>
  6. 原主节点恢复后,自动降级为从节点
# sentinel.conf 示例 sentinel monitor mymaster 127.0.0.1 6379 2 sentinel down-after-milliseconds mymaster 5000 sentinel failover-timeout mymaster 60000 sentinel parallel-syncs mymaster 1

五、集群架构:数据分片与一致性

5.1 Redis Cluster数据分片原理

Redis Cluster采用虚拟槽(slot)分片,共16384个槽(0~16383),每个键通过 CRC16(key) & 16383 映射到槽。

槽分配方式

  • 手动分配:CLUSTER ADDSLOTS <slot> ...
  • 自动分配:redis-cli --cluster create

节点通信机制

节点间通过Gossip协议交换信息:

  • 每秒向随机5个节点发送PING
  • 收到PING回复后,若发现节点信息陈旧,更新本地元数据
  • 故障检测:60%以上节点标记节点为PFAIL → OFAIL(客观下线)
⚠️ 槽迁移:集群扩容时,源节点发送 MIGRATE <目标节点> "" <key> 0 5000,目标节点接收后返回OK,源节点删除键。

5.2 集群高可用与脑裂问题

集群通过主从副本实现高可用,但存在脑裂风险:

  • 脑裂场景:主节点网络分区,从节点被提升为主,客户端写入新主;网络恢复后,原主数据丢失
  • 解决方案:
  • 设置 min-replicas-to-write 1:主节点至少有1个从节点才接受写
  • 设置 min-replicas-max-lag 10:从节点延迟超过10秒不参与选举
# 防脑裂配置示例 min-replicas-to-write 1 min-replicas-max-lag 10

六、线程模型与网络I/O

6.1 单线程为何高性能?

Redis单线程指命令执行线程,但I/O、持久化、集群通信由其他线程处理。其高性能源于:

  • 纯内存操作:无磁盘I/O瓶颈
  • 非阻塞I/O:epoll/kqueue事件驱动
  • 避免线程切换:无锁竞争,无上下文切换开销
  • 简单协议:RESP协议解析高效
⚠️ Redis 6.0引入多线程I/O(默认关闭),用于加速网络读写,命令执行仍单线程。

6.2 网络模型:Reactor模式实现

Redis使用基于libevent/libev的事件库,核心结构:

typedef struct aeEventLoop { aeFileEvent events[AE_SETSIZE]; // 文件事件数组 aeFiredEvent fired[AE_SETSIZE]; // 就绪事件数组 aeTimeEvent *timeEventHead; // 时间事件链表 int maxfd; // 最大文件描述符 aeApiState *apidata; // 后端I/O多路复用实例 } aeEventLoop;

事件循环流程:

  1. 处理时间事件(如过期键检查、cluster心跳)
  2. 调用 aeApiPoll(epoll_wait)阻塞等待I/O事件
  3. 处理就绪的文件事件(读/写)
  4. 循环执行
斌知网
蜀ICP备2026017620号-5