Skip to content

❤️‍🔥高性能Redis

I. 核心架构与原理

1. Redis 为什么这么快?

简答:
主要有三个原因:

  1. 纯内存操作: 数据都在内存中,读写速度非常快(纳秒级)。

  2. 单线程模型(核心): 避免了多线程的上下文切换(Context Switch)和锁竞争(Locking)的开销。

  3. 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 语言原生字符串有以下优势:

  1. O(1) 获取长度: SDS 头部记录了 len,无需遍历。

  2. 杜绝缓冲区溢出: 修改字符串时会自动扩容。

  3. 减少内存重分配: 采用“空间预分配”和“惰性空间释放”策略。

  4. 二进制安全: 可以存储包含空字符 \0 的数据(如图片、视频流)。

5. 跳表(SkipList)是什么?为什么 ZSet 用它而不用红黑树?

简答:
跳表是一种基于链表的随机化数据结构,通过多层索引实现快速查找。

  • 复杂度: 查找、插入、删除平均均为

    O(logN)

  • 对比红黑树:

    1. 实现简单: 代码容易实现和调试。

    2. 区间查找更优: ZSet 经常需要范围查找(如排行榜),跳表只需找到起点然后遍历链表即可,红黑树则较复杂。

    3. 并发调整代价小: 虽然 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 分析工具。

  • 原因:

    1. 业务设计本身不合理,需要考虑重新设计缓存策略。

    2. 没有定期的删除机制、合理的过期机制或者最大容量限制。

  • 解决:

    1. 拆分: 把大 Hash 拆成多个小 Hash。

    2. 异步删除: 使用 UNLINK 命令代替 DEL,在后台线程释放内存,避免阻塞主线程。

12. Redis 变慢了,如何排查?

简答:
按以下顺序排查:

  1. 命令复杂度: 是否使用了

    O(N)

    的命令(如 KEYS *, HGETALL)。

  2. Big Key: 是否有大 Key 的读写或删除。

  3. 持久化: 检查是否频繁 fork 导致阻塞,或 AOF 刷盘阻塞。

  4. 内存使用: 是否触发了 Swap(交换分区),这会极度降低性能。

  5. 网络带宽: 是否流量打满。


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 脚本是原子的,中间不会被插入其他命令,保证了逻辑的严密性。


✨其它

  1. Pipeline(管道): 批量执行命令,减少 RTT(网络往返时间)。

  2. Lua 脚本: 保证多条命令执行的原子性,减少网络开销。

  3. Copy On Write (COW): 解释 RDB fork 子进程时,操作系统利用 COW 机制共享内存,只有写操作时才复制页,从而提高效率。