# 06.面试八股-Redis篇

# 🟢 第一问

面试官:Redis 支持哪些数据类型?分别用在什么场景?

  1. String

String底层是SDS简单动态字符串,结构包含len长度、free空闲空间、buf字节数组,相比于C原生的字符串来说,SDS具备效率更高(时间复杂度O(1),C为O(n))、二进制安全(不会以\0结束)、空间预分配(扩容时,多预存空间,防止下次追加时,立刻就分配空间,回收时,不会立即回收缓存,留着空闲用)、防止缓冲区溢出(自动扩容机制)四大优势。底层还分int(整数用)、embstr(redisObject+SDS放一起,<=44用)、raw(分开,>44用)三种编码,充分利用内存,最大支持存储512MB

redisObject (16 字节) + SDS 头 (3 字节) + 结束符\0(1 字节) = 20 字节,一块内存头64字节,64-20=44

典型场景:缓存对象,计数器,分布式锁

  1. Hash

Redis Hash底层有zipList 压缩列表和 hashTable 字典两种编码

当 Hash 字段数小于512并且 filed 和 value 都小于64字节时,采用压缩列表的形式存储,极其节省内存,但是查找需要遍历(O(n)),一旦超出阈值,就会升级为字典,支持O(1)快速查找,适合存储对象型数据,比String更节省内存,支持局部字段更新

应用场景:存储对象,分页数据等

zipList

基本结构

| field1 | value1 | field2 | value2 | field3 | value3 | ... | end |
1

每一个entry结构

+----------------+-----------+
| 前置长度 | 编码 | 数据内容 |
+----------------+-----------+ 
前项 < 254 字节占 1 字节,≥254 占 5 字节
1
2
3
4

hashTable每一个entry结构

+--------+--------+--------+
|  field | value | 下一个节点指针 |
+--------+--------+--------+
1
2
3

dict 结构体简化

struct dict {
    // 两张哈希表,平时只用ht[0]
    dictht ht[2];
    // rehash 进度标识,-1表示不在rehash
    int rehashidx;
};
1
2
3
4
5
6

dictht 哈希表结构

struct dictht {
    // 哈希桶数组
    dictEntry **table;
    // 数组容量
    unsigned long size;
    // 已存元素个数
    unsigned long used;
};
1
2
3
4
5
6
7
8

最小单元:dictEntry 节点

每个 field-value 就是一个 dictEntry:

struct dictEntry {
    // hash的field
    void *key;
    // hash的value
    void *val;
    // 哈希冲突:指向下一个节点,单链表
    dictEntry *next;
};
1
2
3
4
5
6
7
8

哈希桶数组 + 链表 直观结构图

table[] 哈希桶数组
  桶0  ──> dictEntry(name:lisi) → 冲突节点1 → 冲突节点2
  桶1  ──> dictEntry(age:18)
  桶2  ──> 空
  桶3  ──> dictEntry(city:beijing)
1
2
3
4
5
  1. List

Redis List 底层采用quicklist 快速列表,整体是一个双向链表,链表中的每个节点内部封装了一个ziplist 压缩列表;既利用ziplist 紧凑省内存,又利用双向链表头尾的高效操作,解决了内存开销大和ziplist增删慢的问题

第一层:quicklist 整体

struct quicklist {
    // 头节点
    quicklistNode *head;
    // 尾节点
    quicklistNode *tail;
    // 总元素个数
    long count;
    // 节点数量
    int nodes;
};
1
2
3
4
5
6
7
8
9
10

第二层:quicklistNode 双向链表节点

struct quicklistNode {
    // 前驱节点
    quicklistNode *prev;
    // 后继节点
    quicklistNode *next;
    // 指向内部的 ziplist
    unsigned char *zl;
    // 当前 ziplist 占用字节数
    unsigned int sz;
    // 当前 ziplist 里存了多少个元素
    unsigned int count : 16;
};
1
2
3
4
5
6
7
8
9
10
11
12

第三层:每个 node 内部的 ziplist

就是你刚才学的压缩列表

连续内存、紧凑存多条 list 元素:

[elem1][elem2][elem3][elem4]
1
  1. set

set的底层有intset整数集合和hashtable两种编码;全是整数并且元素数量少于512时使用intset,以连续有序数组存储、省内存、二分查找,一旦有非整数或是数量超过512就用hashtable,注意他只使用hashtable的key来去重,O(1)读写

intset内存结构(整块连续内存)

+--------------------------+
| encoding | length | contents[] |
+--------------------------+
1
2
3

字段解释:

1)encoding:整数类型大小

支持 16bit / 32bit / 64bit 自动升级

2)length:集合元素个数

3)contents[]有序、无重复 整数数组

  1. ZSet

zset底层使用了ziplist和skiplist+dict两种编码方式,当每个member长度小于64字节并且元素数量小于128时,使用ziplist节省内存;超出阈值,就会升级为跳表+字典的编码方式,跳表(O(logn))负责高效范围查询和排序,字典负责按成员极速精准查询。

# 快速记忆口诀

  1. String三编码,四四分界;
  2. List独有快速列表;
  3. Hash字段五一二、字节六四;
  4. Set纯数512用数组;
  5. ZSet一二八、六四,跳表加字典
类型 底层结构 典型场景
String SDS 缓存对象、计数器、分布式锁
Hash dict + ziplist 存储对象(如用户信息、购物车)
List quicklist 消息队列、最新消息列表
Set dict + intset 标签、抽奖、共同关注
ZSet skiplist + dict 排行榜、延迟队列、带权重的任务

# 🟡 第二问(追问)

面试官:String 底层用 SDS(简单动态字符串)而不是 C 字符串,有什么优势?

  • 二进制安全,不会以\0结束

  • 会自动扩容,不会缓冲区溢出

  • O(1)复杂度,不用遍历获取值

  • 内存预分配,减少了频繁修改字符串时的内存重分配次数。

# 🔴 第三问(深度追问)

面试官:ZSet 底层为什么用跳表而不是红黑树?

  • 跳表的实现比红黑树更简单

  • 红黑树需要记录父结点,左右结点,标记颜色,太浪费内存

  • 跳表是线性操作,范围查找效率更高,红黑树,还需要中序遍历

  • 增删操作时,红黑树需要调整结点位置实现自平衡,而跳表只需要通过随机概率生成层数,调整相邻结点的指针即可

# 🟢 第一问

面试官:什么是缓存穿透?怎么解决?

定义:查询一个不存在的数据直接打到数据库

解决:

  • 缓存一个空值

  • 布隆过滤器

补充知识:布隆过滤器

插入:当你加入一个元素时,他会将这个元素通过各种哈希运算,得到几个数字,将bitMap中这几个数字变为1

查询:当你查询是否有一个元素时,如果运算出来的哈希数字在bitMap中都为1,说明这个元素可能存在,为什么说可能,因为可能是其他几个元素运算出来的哈希,覆盖了这几个数字,如果这几个数字有一个不唯一则一定不存在。

# 🔴 第三问(区分题)

面试官:缓存击穿和雪崩的区别?分别怎么解决?

问题 定义 解决方案
击穿 一个热点 key 过期,大量请求打 DB 互斥锁(SETNX)、逻辑过期
雪崩 大量 key 同时过期,请求打 DB TTL 加随机值、多级缓存、熔断降级

互斥锁:当缓存失效时,只允许有一个去查询数据库,重建缓存,其余的等待重试,等待一段时间后,第一个缓存建立完成,从缓存中读取

逻辑过期:当前的缓存不设置物理过期时间,而是设置一个逻辑过期时间,当查询的缓存的逻辑过期时间过期了,就可以开启一个异步线程后台创建新缓存,当前请求仍返回旧数据

TTL+随机值:一个基础过期时间加上随机值(1-300s)

多级缓存:引入本地缓存(如 Caffeine、Guava)作为第一层缓存,Redis 作为第二层。

熔断降级:使用 Sentinel 等组件监控数据库的压力。当数据库 QPS 超过警戒线时,直接触发熔断,对非核心业务返回默认值或静态兜底数据,防止数据库被彻底压垮。

# 🟢 第一问

面试官:用 Redis 实现分布式锁,最简单的写法是什么?有什么问题?

简单写法

Boolean locked = redis.setnx("lock:order", "thread-1");
if (locked) {
 redis.expire("lock:order", 30);
}
// 释放锁
redis.del("lock:order");
1
2
3
4
5
6

问题

  • 加锁和设置过期不是原子操作(setnx 成功但设置过期前宕机 → 死锁)

# 🟡 第二问(追问)

面试官:如何防止自己的锁被别人释放?

问题:线程 A 持锁超时(30s),线程 B 拿到锁,线程 A 执行完调用 del 时删的是 B 的锁

  • 在加锁时给value设置一个唯一标识比如(UUID+线程id),然后在释放锁之前,先写一个lua脚本,脚本内容包括:加一个判断逻辑,锁是否时自己的,是自己的才能删除。防止先判断是自己的情况下,期间发生在删除之前过期,而另一个线程抢到了锁这种现象
-- KEYS[1] 是要释放的锁的 key,ARGV[1] 是当前客户端的唯一标识
if redis.call("get",KEYS[1] == ARGV [1]) then
    return redis.call("del",KEYS[1])
else
    ruturn 0
end
1
2
3
4
5
6

# 🔴 第三问(深度追问)

面试官:RedLock(红锁)是什么?为什么要用它?

💡 答案要点

问题:单机 Redis 锁在主从复制时可能丢(主节点加锁成功但未同步到从就宕机,从变主后锁丢失)

RedLock 思想(Redis 作者提出):

  • 在 N 个独立节点(N 通常为奇数,如 5)上都尝试加锁

  • 只有当超过半数(N/2+1)节点加锁成功,且总耗时 < 锁过期时间,才算加锁成功

争议:RedLock 依赖系统时钟,在时钟跳跃场景下仍有问题。一般业务用单机 Redis + 主从 + Lua 脚本就够了,没必要上 RedLock

# 🟢 第一问

面试官:RDB 和 AOF 的区别?

  • RDB和AOF的核心区别在于RDB是全量快照,文件小,恢复块,但是数据安全性差(两次快照之间redis宕机容易丢数据);AOF是增量日志(记录指令),数据安全性高,但是文件大,恢复缓慢,redis 4.0之后出现了混合持久化的方案,RDB快速恢复,AOF数据安全,是目前的最佳实践

# 🟡 第二问(追问)

面试官:AOF 重写(rewrite)是什么?为什么需要?

  • 随着AOF写的数据越来越多,文件也会变得越来越大,使得文件包含了一堆重复的指令,这时候AOF重写就派上用场了,他会fork出一个子进程,来去将文件的内容精简到一个新的文件中去,同时开启一个重写缓冲区,来记录新写过来的指令,当新文件重写完时,再将重写缓冲区的内容写进新文件里,实现文件压缩。

# 🟢 第一问

面试官:Redis 怎么删除过期 key?为什么不用定时器?

  • 惰性删除:每次访问时判断是否过期,过期就删除

  • 定时删除:每100ms抽取一部分key,判断是否过期

为什么不用定时器

  • 每个 key 一个定时器 → CPU 开销巨大

  • Redis 单线程,定时器会阻塞主线程

# 🟡 第二问(追问)

面试官:内存满了怎么办?8 种淘汰策略说几个重要的

策略名称 淘汰范围 核心淘汰逻辑
noeviction 不淘汰 写操作直接报错,不删除任何数据
allkeys-lru 所有键 淘汰最近最少使用的键(最常用)
allkeys-lfu 所有键 淘汰访问频率最低的键
allkeys-random 所有键 随机淘汰
volatile-lru 仅带TTL的键 淘汰带过期时间且最近最少使用的键
volatile-lfu 仅带TTL的键 淘汰带过期时间且访问频率最低的键
volatile-random 仅带TTL的键 在带过期时间的键中随机淘汰
volatile-ttl 仅带TTL的键 淘汰带过期时间且剩余时间最短的键

LRU vs LFU

  • LRU:最近最少使用(看时间)

  • LFU:最不经常使用(看频率 + 时间),更精准

推荐使用 allkeys-lfu 或 allkeys-lru

# 🟢 根据你的项目出的题

你简历里写了

  • Redis 分布式缓存

  • Redisson 分布式锁

  • Redisson RateLimiter 分布式限流

# 第一问

面试官:Redisson 的分布式锁和 SETNX + Lua 自己实现有什么优势?

  • Redisson 有看门狗机制,可实现锁的自动续期,防止其他线程释放非本线程资源,虽然自己也能实现防止其他线程乱释放资源的功能,但是续期实现相对麻烦,且浪费资源

  • Redisson支持可重入锁(同一个线程获取锁后,再次请求同一把锁不会阻塞。),而自己实现的话,可能会导致死锁,比如,执行方法A、B需要加锁,在A中调用B,相当于获取同一把锁,如果是自己实现,就会出现死锁

public void method1() {
    synchronized (this) {
        System.out.println("进入 method1");
        // 同一线程再次加锁,直接放行,不会死锁
        method2();
    }
}

public void method2() {
    synchronized (this) {
        System.out.println("进入 method2");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
  • Redisson支持高效的阻塞等待机制,当锁等待失败时,会进入阻塞状态,一旦释放锁,Redisson会立刻通过消息通知阻塞进程,来进行抢锁,既不浪费CPU又能高效响应。而自己实现只能是抢不到就放弃或者用while true自旋,严重浪费CPU

# 第二问

面试官:Redisson 的看门狗机制原理是什么?

  • 默认加锁过期时间30s

  • 锁到期剩10秒时,自动续期到30s

  • 如果宕机,锁过期自动释放

  • 业务执行完手动释放

# 第三问

面试官:你在项目里用 Redisson RateLimiter 实现了限流,原理是什么?

原理:通过令牌桶算法实现,有一个桶和一个生成器,生成器每一段时间往桶里加令牌,每次到达的请求必须有令牌才能去访问,如果没有令牌,请求会被限流或拒绝

public void doRateLimit(String key) {
        RRateLimiter rateLimiter = redissonClient.getRateLimiter(key);
        rateLimiter.trySetRateAsync(RateType.OVERALL,2,1, RateIntervalUnit.SECONDS);
        //每当一个用户调用接口时,请求一个令牌
        boolean b = rateLimiter.tryAcquire(1);
        if (!b) {
            throw new BusinessException(ErrorCode.TOO_MANY_REQUEST_ERROR);
        }
1
2
3
4
5
6
7
8

后端面试中关于 Redis 的高频核心考点

# 1. 为什么 Redis 是单线程还那么快?

  • 纯内存操作

  • IO多路复用,单线程可以同时监听成千上万个客户端连接,只有当某个连接有数据可读或可写时,才会触发事件去处理。这避免了大量的线程上下文切换和阻塞等待。

  • 单线程无锁竞争

  • 数据结构高效

# 2. 缓存和 DB 一致性问题怎么解决?

  1. 延时双删

    • 先删除缓存(防止读到旧数据)

    • 更新数据库

    • 再删缓存(防止其他线程在更新数据库期间,读到了旧数据写进缓存)

  2. 先更新数据库再删除缓存

# 3. Redis 做消息队列和 RabbitMQ 有什么区别?

  • RabbitMQ支持消息确认机制,消息可靠度高,redis不支持

  • RabbitMQ支持复杂的路由规则,延迟队列,广播,优先级队列等高级特性,redis只支持简单的先进先出,发布订阅

  • RabbitMQ有完善的磁盘持久化机制,支持海量消息堆积,而redis是内存操作,消息堆积影响性能

最近更新: 9/19/2026, 1:27:08 PM
06.面试八股-Redis篇

编程NOTE   |