跳过导航

Redis 为什么快?“单线程”解释远远不够

约 5 分钟...次浏览
专栏Redis 深度专题第 1 篇

Redis 快,不是因为“单线程天然比多线程快”。单线程只是让命令执行路径避免了大量共享数据竞争;真正的性能来自内存数据集、高效数据结构、事件驱动网络模型和对慢操作的主动规避。忽略这些条件,照样能把 Redis 用成系统瓶颈。

1. 先澄清:Redis 并非所有工作都单线程

通常所说的单线程,是指核心命令在主线程上按顺序执行。Redis 还会使用后台线程或子进程处理持久化、异步释放、集群任务;新版本也支持 I/O 线程分担网络读写。命令对数据的修改仍保持清晰的串行语义,客户端无需为单条命令中的数据竞争加锁。

好处包括:

  • 避免锁竞争、死锁和大量上下文切换;
  • 命令原子性容易理解;
  • 数据结构实现可针对串行访问优化;
  • 延迟更可预测。

代价也很明确:一个耗时命令就可能阻塞后续所有请求。

2. 数据主要在内存中

内存随机访问比磁盘 I/O 快得多。Redis 的读写通常操作内存数据结构,持久化由 RDB/AOF 在另一条路径完成。但“在内存”不等于永远微秒级:缺页、内存换页、fork 写时复制、NUMA、过大的对象和网络排队都会抬高延迟。

生产机器应避免 swap 压力,并为操作系统、fork 与复制留出内存余量。used_memory 不是进程全部 RSS,也不等于容器所需上限。

3. I/O 多路复用解决大量连接

Redis 使用事件循环监听多个 socket 的可读、可写事件。一个线程不必为每个连接阻塞等待,而是在连接就绪时执行对应处理。这让它能以较低线程调度成本服务大量连接。

socket 就绪 → 解析协议 → 执行命令 → 写入响应

                 核心命令串行

因此连接多不一定可怕,“同时到达的大量重命令”才危险。客户端还要使用连接池、合理超时和背压,不能无限建立连接或积压请求。

4. 为场景定制的数据结构

Redis 并非所有 String 都是普通字符数组,也不是所有 Hash 都用同一种结构。它会根据元素数量、长度和配置在紧凑编码与通用结构之间转换,以平衡内存和操作复杂度。

  • 字符串使用 SDS,保存长度并预留安全的二进制能力;
  • Hash、ZSet 等小对象可使用紧凑编码;
  • 跳表让有序集合范围查询保持良好复杂度;
  • 渐进式 rehash 避免一次性迁移整个哈希表;
  • 位图、HyperLogLog 等用紧凑表示解决特定问题。

复杂度仍然重要。GET 很快,不代表对百万成员集合执行全量操作也快。

5. 批处理减少网络往返

许多应用瓶颈不是服务器计算,而是 RTT。依次发送 1000 个 GET 需要大量往返;Pipeline 可以连续发送后批量读取响应。

try (Jedis jedis = pool.getResource()) {
    Pipeline p = jedis.pipelined();
    for (String key : keys) p.get(key);
    List<Object> values = p.syncAndReturnAll();
}

Pipeline 不是事务,也不会让多条命令原子执行。批次过大还会占用客户端与服务端缓冲区,造成队头阻塞。通常应分批并压测确定大小。

Lua 脚本可把多个步骤放到服务端原子执行,却也在主执行路径上运行。脚本必须短小、确定,禁止遍历无界数据。

6. 哪些操作会破坏“快”

  • 对大集合执行 SMEMBERSHGETALL
  • 在线上使用 KEYS *
  • 删除巨大 Key,释放内存耗时;
  • Lua 脚本做复杂循环;
  • 大 value 导致网络、复制和持久化放大;
  • AOF 重写或 RDB fork 期间内存压力过高;
  • 热 Key 把所有流量压到单个分片;
  • 客户端慢读导致输出缓冲增长。

优先用 SCAN 家族做增量遍历,用 UNLINK 异步释放大对象,并设置命令超时、慢日志阈值和输出缓冲限制。

7. 如何建立性能证据

关注:

redis-cli --latency
redis-cli --latency-history
redis-cli slowlog get 20
redis-cli info commandstats
redis-cli info memory

还应采集实例 CPU、网络吞吐、命中率、evicted_keys、客户端连接数和 p99 延迟。基准测试要使用接近生产的 value 大小、Pipeline 批次、持久化策略与集群拓扑。

8. 检查清单

  • 是否把“核心命令单线程”误写成 Redis 完全单线程?
  • 是否存在 O(N) 命令作用于无界集合?
  • 是否用 Pipeline 降低 RTT,并限制批次?
  • 是否监控慢日志、热 Key、大 Key 和客户端缓冲?
  • 是否为持久化、复制与 fork 留出内存和 I/O 余量?

Redis 的快是一组工程选择共同作用的结果。保持它快的关键,是让每条命令短小、数据有界、网络往返可控,并让慢工作离开主执行路径。

分享:
文章作者:狼码纪
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。文章可能参考了其他优秀文章,如有侵权请联系删除。