❤️🔥高性能Redis
I. 核心架构与原理
1. Redis 为什么这么快?
简答:
主要有三个原因:
纯内存操作: 数据都在内存中,读写速度非常快(纳秒级)。
单线程模型(核心): 避免了多线程的上下文切换(Context Switch)和锁竞争(Locking)的开销。
I/O 多路复用: 采用了 Reactor 模式(如 Linux 下的 epoll),能在一个线程中非阻塞地处理大量并发网络连接。
2. Redis 是完全单线程的吗?6.0 引入多线程是为了什么?
简答:
不是完全单线程。 在 Redis 4.0 之后就引入了后台线程处理耗时任务(如 UNLINK 删除大 Key)。
Redis 6.0 引入多线程: 主要是为了解决网络 I/O 的瓶颈。
核心命令执行(计算)依然是单线程的(保证了线程安全,无需加锁)。
多线程只用于: 网络数据的读写(Socket Read/Write)和协议解析。
3. 什么是 I/O 多路复用?
简答:
Redis 利用 epoll 机制,让一个线程同时监听多个 Socket(客户端连接)。当某个 Socket 有数据到达(可读/可写)时,内核会通知 Redis,Redis 再去处理。这避免了为了等待 IO 而阻塞线程,极大地提升了并发处理能力。
II. 数据结构与底层实现(怎么省?)
4. Redis 的字符串(String)底层为什么用 SDS 而不是 C 字符串?
简答:
SDS(Simple Dynamic String)相比 C 语言原生字符串有以下优势:
O(1) 获取长度: SDS 头部记录了 len,无需遍历。
杜绝缓冲区溢出: 修改字符串时会自动扩容。
减少内存重分配: 采用“空间预分配”和“惰性空间释放”策略。
二进制安全: 可以存储包含空字符 \0 的数据(如图片、视频流)。
5. 跳表(SkipList)是什么?为什么 ZSet 用它而不用红黑树?
简答:
跳表是一种基于链表的随机化数据结构,通过多层索引实现快速查找。
复杂度: 查找、插入、删除平均均为
O(logN)。
对比红黑树:
实现简单: 代码容易实现和调试。
区间查找更优: ZSet 经常需要范围查找(如排行榜),跳表只需找到起点然后遍历链表即可,红黑树则较复杂。
并发调整代价小: 虽然 Redis 是单线程,但在理论上跳表插入节点只需修改局部指针。
6. 压缩列表(Ziplist/Listpack)是为了解决什么问题?
简答:
为了节省内存。当 Hash、List、ZSet 元素较少且较小时,Redis 会使用一块连续的内存空间(压缩列表)来紧凑存储数据,避免了指针带来的内存碎片和额外开销。
(注:Redis 7.0 后主要使用 Listpack 替代 Ziplist)
III. 内存管理与持久化(怎么稳?)
7. Redis 的过期策略是什么?
简答:
采用 惰性删除 + 定期删除 相结合的策略。
惰性删除: 访问 Key 时,检查是否过期,过期则删除。优点是省 CPU,缺点是可能残留垃圾数据。
定期删除: 每隔一段时间随机抽取一部分 Key 进行检查和删除。
8. 内存淘汰策略(Eviction Policy)有哪些?LRU 和 LFU 的区别?
简答:
当内存满了(达到 maxmemory)时触发。常见的有:
noeviction:报错(默认)。
allkeys-lru / volatile-lru:移除最近最少使用的 Key。
LRU (Least Recently Used): 基于时间,淘汰很久没用的。
LFU (Least Frequently Used): 基于频率,淘汰用得最少的(更能反映热点)。
9. AOF 和 RDB 持久化对性能的影响?
简答:
RDB(快照): 生成快照时会 fork 子进程,fork 操作会阻塞主线程。如果内存数据非常大(如几十 GB),fork 可能会导致毫秒甚至秒级停顿。
AOF(日志):
appendfsync always:每次写都刷盘,性能极差。
appendfsync everysec:每秒刷盘,折中方案(推荐)。
AOF rewrite 同样需要 fork 子进程,也有阻塞风险。
IV. 经典性能问题与场景(怎么避坑?)
10. 什么是缓存穿透、击穿、雪崩?怎么解决?
简答:
缓存穿透(查不到): 查询不存在的数据,请求直打数据库。
- 解法: 布隆过滤器(Bloom Filter)、缓存空对象。
缓存击穿(热点失效): 单个热点 Key 过期,并发请求瞬间击垮数据库。
- 解法: 互斥锁(Mutex)、逻辑过期(不设置真实 TTL,后台异步更新)。
缓存雪崩(集体失效): 大量 Key 同时过期或 Redis 宕机。
- 解法: 设置随机过期时间、Redis 高可用(Cluster/Sentinel)、限流降级。
11. 如何处理 Big Key(大 Key)?
简答:
Big Key 会导致读写阻塞、网络阻塞、删除阻塞,因为Redis本质单线程。
发现: 使用 --bigkeys 参数或 RDB 分析工具。
原因:
业务设计本身不合理,需要考虑重新设计缓存策略。
没有定期的删除机制、合理的过期机制或者最大容量限制。
解决:
拆分: 把大 Hash 拆成多个小 Hash。
异步删除: 使用 UNLINK 命令代替 DEL,在后台线程释放内存,避免阻塞主线程。
12. Redis 变慢了,如何排查?
简答:
按以下顺序排查:
命令复杂度: 是否使用了
O(N)的命令(如 KEYS *, HGETALL)。
Big Key: 是否有大 Key 的读写或删除。
持久化: 检查是否频繁 fork 导致阻塞,或 AOF 刷盘阻塞。
内存使用: 是否触发了 Swap(交换分区),这会极度降低性能。
网络带宽: 是否流量打满。
V. 分布式锁相关
Redisson 是一个在 Redis 基础上实现的 Java 驻内存数据网格(In-Memory Data Grid)。它实现分布式锁的核心思路是:Lua 脚本 + Hash 数据结构 + 看门狗机制(Watchdog)。
1. 解决“锁过期但业务没跑完” —— 看门狗机制(Watchdog)
这是 Redisson 最亮眼的功能。
原理:
当你加锁时,如果没有指定过期时间,Redisson 会默认设置一个 30 秒的过期时间(LockWatchdogTimeout)。
Redisson 会启动一个后台线程(定时任务),每隔 LockWatchdogTimeout / 3 (默认 10 秒)就会去检查一下:“持有锁的线程还在吗?”
如果还在,就将锁的过期时间重新重置为 30 秒(这个过程叫“续期”)。
只要业务不跑完,锁的时间就会一直续期,永远不会过期。
如果客户端宕机了(Watchdog 也就挂了),无法续期,Redis 里的锁会在 30 秒后自动过期,防止死锁。
2. 解决“误删别人的锁” —— UUID 校验 + Lua 脚本
Redisson 在加锁时,Value 存的不是乱七八糟的值,而是 UUID + 线程ID。
在释放锁时,Redisson 使用 Lua 脚本(保证原子性)去判断:
当前锁是否存在?
当前锁 Value 里的 ID 是不是我自己的 ID?
如果是,才执行 DEL;如果不是,说明锁已经过期被别人抢了,我就不删了。
3. 解决“不可重入” —— 使用 Hash 结构
Redisson 底层不使用 String (SETNX),而是使用 Hash 结构。
Key: 锁的名称。
Field: UUID + 线程 ID。
Value: 计数器(Count)。
原理:
第一次加锁:在 Hash 中写入字段,将 Value 设为 1。
再次加锁(重入):发现 Field 是当前线程,将 Value + 1(变为 2)。
释放锁:将 Value - 1。当 Value 变为 0 时,才真正从 Redis 中删除 Key。
这完全模拟了 Java 中 ReentrantLock 的机制。
4. 解决“原子性” —— 全程 Lua 脚本
Redisson 的加锁、释放锁、续期操作,全部都是通过 Lua 脚本 发送给 Redis 执行的。
Redis 执行 Lua 脚本是原子的,中间不会被插入其他命令,保证了逻辑的严密性。
✨其它
Pipeline(管道): 批量执行命令,减少 RTT(网络往返时间)。
Lua 脚本: 保证多条命令执行的原子性,减少网络开销。
Copy On Write (COW): 解释 RDB fork 子进程时,操作系统利用 COW 机制共享内存,只有写操作时才复制页,从而提高效率。