Redis面试三连击:穿透、击穿、雪崩,你真的能答清楚吗?
开篇
美团二面现场。
面试官:"说说缓存穿透、击穿、雪崩的区别。"
候选人:"呃...穿透是查不存在的key,击穿是热点key过期,雪崩是大量key同时过期..."
面试官追问:"那解决穿透用布隆过滤器,布隆过滤器的原理是什么?误判率怎么控制?"
候选人沉默了。
面试官继续:"分布式锁解决击穿,如果锁过期但业务没执行完怎么办?"
候选人更沉默了。
这三个问题,是Redis面试的标配连环追问。
问题在于:大多数候选人能背出名词解释,却答不出底层原理和生产实践。
面试官考察的不只是"是什么",而是:
- 能不能从原理层面理解问题
- 有没有真实的生产经验
- 遇到边界情况怎么处理
本文将从面试官视角深度拆解Redis的核心考点,不只是告诉你怎么答,更告诉你为什么这么问。
本文结构
第一章 缓存三大问题(必问)
├── 1.1 穿透、击穿、雪崩的本质区别
├── 1.2 布隆过滤器原理与实现
├── 1.3 分布式锁的完整实现
└── 1.4 缓存一致性方案
第二章 数据结构底层实现(高频)
├── 2.1 String/SDS为什么比C字符串好
├── 2.2 跳表为什么能替代红黑树
├── 2.3 渐进式rehash机制
└── 2.4 各类型的编码转换条件
第三章 持久化机制(必问)
├── 3.1 RDB的fork与COW
├── 3.2 AOF的重写流程
├── 3.3 混合持久化
└── 3.4 生产环境最佳配置
第四章 高可用架构(加分项)
├── 4.1 主从复制流程
├── 4.2 哨兵故障转移
├── 4.3 Cluster分片原理
└── 4.4 脑裂问题与解决
第五章 性能优化实战
├── 5.1 大Key问题排查与处理
├── 5.2 热Key问题解决方案
├── 5.3 单线程模型与IO多路复用
└── 5.4 Redis 6.0多线程
第一章 缓存三大问题
1.1 穿透、击穿、雪崩的本质区别
这三个概念经常混淆,面试官问的第一个问题就是让你说清楚区别。
一张图搞清三者区别
请求来了
│
┌─────────────┼─────────────┐
▼ ▼ ▼
缓存穿透 缓存击穿 缓存雪崩
│ │ │
▼ ▼ ▼
key根本不存在 热点key过期 大量key同时过期
│ │ │
▼ ▼ ▼
每次都打DB 并发都打DB 全部都打DB
│ │ │
▼ ▼ ▼
布隆过滤器 互斥锁 随机过期时间
缓存空值 永不过期 多级缓存
详细对比
| 问题 | 原因 | 危害 | 解决方案 |
|---|---|---|---|
| 穿透 | 请求的key在DB中不存在 | 恶意攻击可打垮DB | 布隆过滤器、缓存空值 |
| 击穿 | 热点key突然过期 | 瞬间并发全打DB | 互斥锁、永不过期 |
| 雪崩 | 大量key同时过期 | DB压力骤增 | 过期时间打散、多级缓存 |
面试常见追问
追问1:"穿透和击穿有什么本质区别?"
穿透:key从来不存在(恶意攻击或业务bug)
└── 每次请求都100%打到DB
击穿:key曾经存在,但刚好过期(正常业务)
└── 只在过期那一刻有并发问题
追问2:"雪崩和击穿的区别?"
击穿:单个热点key过期
└── 影响单个热点数据的请求
雪崩:大量key同时过期
└── 影响大面积请求,可能导致整个系统崩溃
1.2 布隆过滤器原理与实现
这是穿透问题的核心解决方案,面试官一定会追问原理。
布隆过滤器原理
核心结构:
├── m位的bit数组(全为0)
├── k个独立的哈希函数
添加元素时:
├── 对元素计算k个哈希值
└── 将对应k个位置设为1
查询元素时:
├── 对元素计算k个哈希值
├── 检查k个位置是否都为1
├── 全为1 → 可能存在(可能误判)
└── 有一个为0 → 一定不存在(100%准确)
图解:
添加元素 "apple":
hash1("apple") = 2
hash2("apple") = 5
hash3("apple") = 8
bit数组变化:
位置: 0 1 2 3 4 5 6 7 8 9
之前: 0 0 0 0 0 0 0 0 0 0
之后: 0 0 1 0 0 1 0 0 1 0
查询 "orange":
hash1("orange") = 2 → 1 ✓
hash2("orange") = 5 → 1 ✓
hash3("orange") = 7 → 0 ✗
结论:一定不存在(因为7位是0)
查询 "banana":
hash1("banana") = 2 → 1 ✓
hash2("banana") = 5 → 1 ✓
hash3("banana") = 8 → 1 ✓
结论:可能存在(但其实我们没添加过,这是误判)
误判率公式
误判率 ≈ (1 - e^(-kn/m))^k
其中:
├── m:bit数组长度
├── n:已插入元素数量
├── k:哈希函数个数
最优k值:k = (m/n) × ln(2) ≈ 0.693 × (m/n)
示例计算:
├── 预计1000万元素
├── 期望误判率1%
├── 需要的bit数组大小:约9600万位 = 12MB
└── 最优哈希函数个数:7个
Redis中使用布隆过滤器
# Redis 4.0+ 使用RedisBloom模块
# 创建布隆过滤器
BF.RESERVE product_filter 0.01 10000000
# 过滤器名 误判率 预计元素数
# 添加元素
BF.ADD product_filter "product:10001"
# 批量添加
BF.MADD product_filter "product:10002" "product:10003"
# 检查元素
BF.EXISTS product_filter "product:10001" # 返回1(存在)
BF.EXISTS product_filter "product:99999" # 返回0(不存在)
完整代码实现
def get_product(product_id):
"""防止缓存穿透的完整实现"""
# 1. 布隆过滤器判断
if not bloom_filter.exists(f"product:{product_id}"):
return None # 一定不存在,直接返回
# 2. 查缓存
cache_key = f"product:{product_id}"
data = redis.get(cache_key)
if data is not None:
if data == "NULL": # 缓存的空值
return None
return json.loads(data)
# 3. 查数据库
product = db.query_product(product_id)
if product:
redis.setex(cache_key, 3600, json.dumps(product))
else:
# 缓存空值,短过期时间
redis.setex(cache_key, 60, "NULL")
return product
1.3 分布式锁的完整实现
这是解决缓存击穿的核心方案,也是面试必问的实战题。
基础实现
import uuid
import time
class RedisLock:
def __init__(self, redis_client):
self.redis = redis_client
def acquire(self, key, timeout=10):
"""
获取锁
返回锁标识(成功)或None(失败)
"""
# 生成唯一标识,防止误删其他客户端的锁
identifier = str(uuid.uuid4())
# SET key value NX EX timeout
# NX: 只有key不存在时才设置
# EX: 设置过期时间
result = self.redis.set(
key, identifier,
nx=True,
ex=timeout
)
return identifier if result else None
def release(self, key, identifier):
"""
释放锁
使用Lua脚本保证原子性
"""
script = """
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
"""
return self.redis.eval(script, 1, key, identifier)
面试必问:锁续期问题
问题:如果业务执行时间超过锁的过期时间怎么办?
场景:
├── 加锁,设置10秒过期
├── 业务执行了15秒
├── 第10秒时锁过期被删除
├── 其他客户端获得锁
├── 第15秒业务完成,删除锁
└── 但删除的是别人的锁!
解决方案:Watchdog自动续期
import threading
class RedisLockWithWatchdog:
def __init__(self, redis_client):
self.redis = redis_client
self.watchdog_running = False
self.watchdog_thread = None
def acquire(self, key, timeout=30):
identifier = str(uuid.uuid4())
if self.redis.set(key, identifier, nx=True, ex=timeout):
# 启动watchdog线程,定期续期
self._start_watchdog(key, identifier, timeout)
return identifier
return None
def _start_watchdog(self, key, identifier, timeout):
"""每1/3过期时间续期一次"""
def watchdog():
while self.watchdog_running:
time.sleep(timeout / 3)
if self.watchdog_running:
# 续期
script = """
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("EXPIRE", KEYS[1], ARGV[2])
end
"""
self.redis.eval(script, 1, key, identifier, timeout)
self.watchdog_running = True
self.watchdog_thread = threading.Thread(target=watchdog, daemon=True)
self.watchdog_thread.start()
def release(self, key, identifier):
# 停止watchdog
self.watchdog_running = False
# 释放锁
script = """
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
end
"""
return self.redis.eval(script, 1, key, identifier)
面试必问:Redlock算法
问题:单Redis实例的锁有什么问题?
问题:主从切换时锁可能丢失
场景:
├── 客户端A在主节点加锁成功
├── 主节点宕机,锁未同步到从节点
├── 从节点提升为主节点
├── 客户端B在新主节点加锁成功
└── 两个客户端同时持有锁!
Redlock解决方案:
1. 准备N个独立的Redis实例(N>=5,推荐5个)
2. 客户端依次向所有实例加锁
3. 在超过半数实例(N/2+1)成功加锁
4. 且总耗时小于锁过期时间
5. 才认为加锁成功
6. 解锁时向所有实例发送DEL命令
生产环境推荐:使用Redisson客户端,内置完整实现。
1.4 缓存一致性方案
这是另一个高频面试题:如何保证缓存和数据库的一致性?
Cache Aside模式(推荐)
# 读操作
def get_user(user_id):
# 1. 先读缓存
cache_key = f"user:{user_id}"
data = redis.get(cache_key)
if data:
return json.loads(data)
# 2. 缓存未命中,读数据库
user = db.query_user(user_id)
# 3. 写入缓存
if user:
redis.setex(cache_key, 3600, json.dumps(user))
return user
# 写操作(重点!)
def update_user(user_id, new_data):
# 1. 先更新数据库
db.update_user(user_id, new_data)
# 2. 再删除缓存(不是更新!)
cache_key = f"user:{user_id}"
redis.delete(cache_key)
为什么是"先更新DB,再删缓存"?
错误方案1:先删缓存,再更新DB
问题场景:
├── 线程A删除缓存
├── 线程B读取,缓存未命中
├── 线程B读DB(旧数据),写入缓存
├── 线程A更新DB
└── 结果:缓存是旧数据,DB是新数据!
错误方案2:先更新DB,再更新缓存
问题场景:
├── 线程A更新DB为100
├── 线程B更新DB为200
├── 线程B更新缓存为200
├── 线程A更新缓存为100
└── 结果:DB是200,缓存是100!
正确方案:先更新DB,再删缓存
即使有并发问题:
├── 线程A查询缓存未命中
├── 线程A读DB(旧数据)
├── 线程B更新DB
├── 线程B删除缓存
├── 线程A将旧数据写入缓存
└── 问题:缓存还是旧数据
但这种情况概率极低:
├── 需要"读DB"比"删缓存"慢
├── 实际上DB读写通常在ms级
├── 而且下次读会重新加载
延时双删(可选增强)
def update_user_with_double_delete(user_id, new_data):
cache_key = f"user:{user_id}"
# 1. 先删缓存
redis.delete(cache_key)
# 2. 更新数据库
db.update_user(user_id, new_data)
# 3. 延时后再删一次
time.sleep(0.5) # 延时500ms
redis.delete(cache_key)
延时时间怎么定?
延时时间 > 一次读DB+写缓存的时间
通常设置100ms~1s
第二章 数据结构底层实现
2.1 String/SDS为什么比C字符串好
面试官问"Redis String的底层实现",不是想听你说"就是字符串",而是想考察你对SDS的理解。
SDS结构
struct sdshdr {
int len; // 已使用长度
int free; // 剩余空间
char buf[]; // 字节数组
};
SDS vs C字符串
| 特性 | C字符串 | SDS |
|---|---|---|
| 获取长度 | O(n)遍历 | O(1)直接读len |
| 二进制安全 | 否(\0截断) | 是(用len判断结束) |
| 缓冲区溢出 | 可能 | 自动扩容 |
| 修改时内存分配 | 每次都分配 | 预分配+惰性释放 |
空间预分配策略
修改后的新长度 < 1MB:
新空间 = 2 × 新长度
修改后的新长度 >= 1MB:
新空间 = 新长度 + 1MB
示例:
├── 原长度10字节,追加5字节
├── 新长度15字节 < 1MB
├── 分配空间 = 15 × 2 = 30字节
└── free = 30 - 15 = 15字节
2.2 跳表为什么能替代红黑树
Zset使用跳表(skiplist)而不是红黑树,这是面试常问的设计抉择。
跳表结构
Level 3: 1 --------------------------> 30 --------------------> 60
Level 2: 1 --------> 10 -------------> 30 --------> 50 -------> 60
Level 1: 1 --> 5 --> 10 --> 20 --> 30 --> 40 --> 50 --> 55 --> 60
Level 0: 1 → 3 → 5 → 7 → 10 → 15 → 20 → 25 → 30 → 35 → 40 → 45 → 50 → 55 → 60
跳表 vs 红黑树
| 特性 | 跳表 | 红黑树 |
|---|---|---|
| 查找复杂度 | O(log n) | O(log n) |
| 插入复杂度 | O(log n) | O(log n) |
| 范围查询 | O(log n + m),链表遍历 | O(log n + m),中序遍历 |
| 实现难度 | 简单 | 复杂(旋转、变色) |
| 内存占用 | 略高 | 较低 |
| 并发友好 | 好(局部修改) | 差(旋转影响大) |
Redis选择跳表的原因
1. 实现简单:代码更少,bug更少
2. 范围查询快:ZRANGE直接遍历链表
3. 调参灵活:可以调整层数概率
4. 并发友好:插入删除只影响局部节点
2.3 渐进式rehash机制
Hash类型的底层是hashtable,扩容时使用渐进式rehash。
为什么需要渐进式rehash?
问题:一次性rehash大字典会阻塞主线程
例如:100万key的字典扩容
├── 一次性迁移:可能阻塞几秒
└── 渐进式迁移:每次操作迁移一个桶,无感知
渐进式rehash流程
1. 创建新哈希表ht[1](2倍大小)
2. 维护索引计数器rehashidx = 0
3. 每次操作时:
├── 迁移ht[0]中rehashidx桶的所有节点到ht[1]
├── rehashidx++
└── 如果ht[0]为空,释放ht[0],将ht[1]设为ht[0]
4. 迁移期间:
├── 写操作:只写ht[1]
├── 读操作:先查ht[0],再查ht[1]
└── 删除操作:两个表都要删
图解:
初始状态:
ht[0]: [A, B, C, D, E] rehashidx = 0
ht[1]: []
步骤1(一次操作后):
ht[0]: [ B, C, D, E] rehashidx = 1
ht[1]: [A]
步骤2:
ht[0]: [ C, D, E] rehashidx = 2
ht[1]: [A, B]
...
完成:
ht[0]: [] (释放)
ht[1]: [A, B, C, D, E] → 变成新的ht[0]
2.4 各类型的编码转换条件
这是面试常问的细节题。
String编码
| 编码 | 条件 | 说明 |
|---|---|---|
| int | 整数且可用long表示 | 直接存数值,最省空间 |
| embstr | 字符串且 ≤ 44字节 | SDS和对象头连续内存 |
| raw | 字符串且 > 44字节 | SDS和对象头分开分配 |
Hash编码
# 同时满足用ziplist:
hash-max-ziplist-entries 512 # 元素数 ≤ 512
hash-max-ziplist-value 64 # 单值 ≤ 64字节
# 否则用hashtable
List编码(Redis 3.2+)
全部使用quicklist
= 多个ziplist组成的双向链表
配置:
list-max-ziplist-size -2 # 每个ziplist最大8KB
list-compress-depth 0 # 压缩深度(0不压缩)
Zset编码
# 同时满足用ziplist:
zset-max-ziplist-entries 128 # 元素数 ≤ 128
zset-max-ziplist-value 64 # 单值 ≤ 64字节
# 否则用skiplist + hashtable
第三章 持久化机制
3.1 RDB的fork与COW
这是持久化面试的必问题。
RDB持久化流程
1. 父进程fork子进程
└── fork速度快,只复制页表
2. 子进程遍历数据,写入临时RDB文件
└── 父进程继续处理请求
3. 父进程修改数据时,触发COW
└── 复制修改的页,子进程读到的仍是旧数据
4. 子进程完成后,替换旧RDB文件
COW(Copy-On-Write)原理
初始状态(fork后):
父进程页表 ────┐
├──→ 物理页1 [数据A]
子进程页表 ────┘
父进程修改数据时:
1. 复制物理页1为物理页2
2. 父进程页表指向物理页2
3. 子进程页表仍指向物理页1
修改后:
父进程页表 ──→ 物理页2 [数据A']
子进程页表 ──→ 物理页1 [数据A] ← 保持原数据
fork性能问题
问题:大内存实例fork可能阻塞
1GB内存 → fork约20ms
10GB内存 → fork约200ms
优化:
1. 关闭透明大页(THP)
echo never > /sys/kernel/mm/transparent_hugepage/enabled
2. 减少RDB触发频率
save 900 1 # 只保留这一条
3. 使用AOF或混合持久化
3.2 AOF的重写流程
为什么需要AOF重写?
问题:AOF文件会越来越大
原始命令:
INCR count # count = 1
INCR count # count = 2
INCR count # count = 3
INCR count # count = 4
INCR count # count = 5
重写后:
SET count 5 # 一条命令搞定
AOF重写流程
1. fork子进程
└── 子进程遍历数据库,生成新AOF
2. 父进程继续处理请求
└── 新命令同时写入:
├── 旧AOF文件
└── AOF重写缓冲区
3. 子进程完成后,通知父进程
└── 父进程将重写缓冲区追加到新AOF
4. 原子替换旧AOF文件
图解:
┌──────────────┐
│ 父进程 │
└──────┬───────┘
fork │
┌────────────┴────────────┐
▼ ▼
┌──────────────┐ ┌──────────────────┐
│ 子进程 │ │ 继续处理请求 │
│ 生成新AOF │ │ │
└──────┬───────┘ │ 同时写入: │
│ │ ├── 旧AOF │
│ │ └── 重写缓冲区 │
▼ └────────┬─────────┘
新AOF完成 │
│ │
▼ ▼
追加重写缓冲区 ←─────────── 缓冲区内容
│
▼
原子替换旧AOF
3.3 混合持久化
Redis 4.0引入,结合RDB和AOF的优点。
# 开启混合持久化
aof-use-rdb-preamble yes
混合持久化文件格式
┌─────────────────────────────┐
│ RDB格式数据 │ ← 文件开头是RDB二进制
│ (fork时刻的全量数据) │
├─────────────────────────────┤
│ AOF格式增量命令 │ ← 后面是AOF增量命令
│ (fork后到重写完成的命令) │
└─────────────────────────────┘
优势
恢复速度:RDB快(二进制直接加载)
数据安全:AOF好(最多丢1秒)
文件大小:比纯AOF小
3.4 生产环境最佳配置
# === RDB配置 ===
save 900 1 # 900秒内有1次修改就触发
save "" # 或者完全关闭RDB
# === AOF配置 ===
appendonly yes # 开启AOF
appendfsync everysec # 每秒刷盘(平衡性能和安全)
no-appendfsync-on-rewrite yes # 重写期间不fsync
auto-aof-rewrite-percentage 100 # 文件增长100%触发重写
auto-aof-rewrite-min-size 64mb # 最小64MB才触发
# === 混合持久化 ===
aof-use-rdb-preamble yes # 开启混合持久化
第四章 高可用架构
4.1 主从复制流程
全量同步
1. 从节点发送:PSYNC ? -1
└── ?:不知道主节点runid
└── -1:没有offset
2. 主节点判断需要全量同步
└── 返回:+FULLRESYNC <runid> <offset>
3. 主节点执行BGSAVE
└── 同时缓存新命令到复制缓冲区
4. 发送RDB文件给从节点
└── 从节点清空数据,加载RDB
5. 发送缓冲区命令
└── 从节点执行,数据完全一致
增量同步
1. 从节点发送:PSYNC <runid> <offset>
└── 携带之前记录的主节点信息
2. 主节点判断:
├── runid匹配
└── offset在复制积压缓冲区范围内
3. 返回:+CONTINUE
└── 只发送offset之后的命令
复制积压缓冲区
作用:避免断线后重新全量同步
配置:
repl-backlog-size 10mb # 默认1MB,建议调大
计算:
缓冲区大小 >= 断线时间 × 写入速率
例如:断线5秒 × 2MB/秒 = 10MB
4.2 哨兵故障转移
完整流程
1. 哨兵定期PING主节点
2. 超时无响应 → 主观下线(SDOWN)
└── 单个哨兵认为主节点下线
3. 询问其他哨兵 → 超过半数确认 → 客观下线(ODOWN)
└── 真的下线了
4. 选举Leader哨兵(Raft算法)
└── 由Leader执行故障转移
5. Leader选择新主节点
├── 过滤不健康节点
├── 优先级最高的(replica-priority)
├── 复制偏移量最大的
└── runid最小的
6. 执行故障转移
├── 向新主发送:SLAVEOF NO ONE
├── 向其他从发送:SLAVEOF <新主IP> <新主Port>
└── 更新配置,通知客户端
4.3 Cluster分片原理
哈希槽
共16384个槽(0~16383)
key → 槽位计算:
slot = CRC16(key) % 16384
槽位分配示例(3主节点):
├── 节点A:0~5460
├── 节点B:5461~10922
└── 节点C:10923~16383
请求路由
客户端请求流程:
1. 计算key的槽位
2. 查本地槽位映射表,找到节点
3. 发送请求到该节点
4. 如果槽位已迁移,节点返回:
-MOVED <slot> <ip>:<port>
5. 客户端更新映射表,重试
槽位迁移中的请求:
-ASK <slot> <ip>:<port>
└── 临时重定向,不更新映射表
4.4 脑裂问题与解决
什么是脑裂?
场景:
├── 主节点与哨兵网络分区
├── 哨兵选举新主
├── 旧主仍在接受写入
├── 网络恢复后,旧主降为从
└── 旧主的写入数据丢失!
解决方案
# 主节点配置(Redis 5.0+)
# 至少1个从节点延迟小于10秒,才接受写入
min-replicas-to-write 1
min-replicas-max-lag 10
# 效果:
# 网络分区后,旧主无法写入(从节点不可达)
# 避免数据丢失
第五章 性能优化实战
5.1 大Key问题排查与处理
什么是大Key?
String:value > 10KB
Hash/Set/Zset/List:元素 > 5000个
排查方法
# 1. 扫描大Key(生产环境慎用)
redis-cli --bigkeys
# 2. 分析RDB文件(推荐)
rdb -c memory dump.rdb --bytes 10240 -f bigkeys.csv
# 3. 内存分析
MEMORY USAGE key
处理方案
# 方案1:拆分
# 原来:一个大Hash
user:1000 → {field1: val1, ..., field10000: val10000}
# 拆分后:分片
user:1000:0 → {field1: val1, ..., field100: val100}
user:1000:1 → {field101: val101, ..., field200: val200}
...
# 方案2:异步删除
# Redis 4.0+
UNLINK key # 后台线程删除
# vs
DEL key # 同步删除,阻塞主线程
5.2 热Key问题解决方案
什么是热Key?
单个Key的QPS远超平均值
例如:秒杀商品、热门新闻
解决方案
# 方案1:本地缓存
from cachetools import TTLCache
local_cache = TTLCache(maxsize=1000, ttl=1) # 1秒过期
def get_hot_data(key):
# 先查本地缓存
if key in local_cache:
return local_cache[key]
# 查Redis
data = redis.get(key)
local_cache[key] = data
return data
# 方案2:读写分离
# 热Key只读从节点
# 方案3:Key分片
# 原来:product:1000
# 分片:product:1000:0, product:1000:1, ...
# 随机选择一个读取
5.3 单线程模型与IO多路复用
为什么单线程还能高并发?
1. 纯内存操作
└── 内存操作是ns级,不是瓶颈
2. 避免锁竞争
└── 无锁,无上下文切换
3. IO多路复用(epoll)
├── 单线程监听多个socket
├── 有数据时才处理
└── 避免阻塞等待
4. 高效数据结构
└── SDS、跳表、压缩列表等
IO多路复用原理
传统模型(每个连接一个线程):
Thread1 ──→ Socket1 ──→ 阻塞等待...
Thread2 ──→ Socket2 ──→ 阻塞等待...
Thread3 ──→ Socket3 ──→ 阻塞等待...
问题:线程多,切换开销大
epoll模型(单线程监听所有连接):
┌→ Socket1
EventLoop ────────→├→ Socket2
└→ Socket3
1. 注册socket到epoll
2. epoll_wait阻塞等待事件
3. 有事件时返回,处理对应socket
4. 继续epoll_wait
5.4 Redis 6.0多线程
多线程做了什么?
Redis 6.0的多线程:
├── 网络IO(读写socket):多线程
└── 命令执行:仍是单线程
为什么只用于IO?
├── IO是性能瓶颈(尤其是网络带宽)
├── 命令执行本身够快
└── 保持原子性,无需加锁
开启多线程
# redis.conf
# 开启IO多线程
io-threads 4
# 开启读的多线程(写默认开启)
io-threads-do-reads yes
本章小结
Redis面试的核心考点:
| 考点 | 必问题 | 面试官关注 |
|---|---|---|
| 缓存问题 | 穿透/击穿/雪崩区别 | 能否说清原理和生产方案 |
| 数据结构 | 跳表/SDS/渐进式rehash | 是否理解设计思想 |
| 持久化 | RDB的fork、AOF重写 | 知道COW和混合持久化 |
| 高可用 | 哨兵故障转移流程 | 知道脑裂问题 |
| 分布式锁 | 完整实现 | 续期、Redlock |
面试回答技巧:
- 先答是什么:30秒给出定义
- 再答为什么:解释设计原因
- 举个例子:用场景说明
- 说出坑点:展示生产经验
下期预告
下一篇:《Java并发编程:synchronized锁升级全过程,从偏向锁到重量级锁的JVM底层揭秘》
将揭秘:
- 锁升级的完整流程
- 偏向锁/轻量级锁/重量级锁的区别
- CAS和AQS的原理
- 线程池参数配置的最佳实践
💬 互动话题
评论区聊聊:你们生产环境遇到过哪些Redis相关的坑?