Redis 为什么快?

导读:Redis 官方给出的基准数据是单节点 10 万+ QPS,实际生产环境中跑到 8~15 万 QPS 也是家常便饭。一个没有什么"黑科技"的 C 语言程序凭什么能这么快?这篇文章从内存模型、数据结构、IO 模型、线程模型四个维度把 Redis 的性能根因拆开讲透。读完之后,你不仅能在面试中从容应答"Redis 为什么快",更能在实际项目中做出正确的 Redis 性能优化决策。


一、先给结论:五大核心原因

Redis 的高性能不是靠某一个银弹,而是五个层面协同作用的结果:

层面 核心机制 对性能的贡献
存储介质 纯内存操作 消除了磁盘 IO 瓶颈,读写延迟从 ms 级降到 μs 级
数据结构 精心设计的底层编码 同一个 API 后面会根据数据规模自动切换最优编码
IO 模型 epoll 多路复用 单线程处理数万并发连接,避免 select/poll 的线性扫描
线程模型 核心单线程 无锁设计,消除上下文切换和竞争开销
协议设计 RESP 协议 文本协议,解析简单快速,支持 Pipeline 批量操作

这五个原因不是并列关系,而是有层次递进的。内存是地基,数据结构是建筑材料,IO 模型是施工方案,线程模型是施工队组织形式,协议设计是运输通道。

图1 展示了 Redis 高性能的五层架构。从底层的内存存储到上层的 RESP 协议,每一层都在为极致性能做贡献。理解这个分层关系,是理解 Redis 性能优化的基础。

下面逐一深入。


二、纯内存操作:10 万倍的速度差

2.1 内存 vs 磁盘的本质差异

Redis 所有数据都在内存中,这是它快的第一性原理。来看一组硬件延迟数据(来自 Jeff Dean 的经典 "Numbers Every Programmer Should Know"):

操作 延迟 相对倍数
L1 缓存引用 0.5 ns
L2 缓存引用 7 ns 14×
主内存引用 100 ns 200×
SSD 随机读取 16,000 ns(16 μs) 32,000×
机械磁盘寻道 10,000,000 ns(10 ms) 20,000,000×

内存访问和磁盘访问之间有5 个数量级的差距。对于一个 GET 操作:

  • Redis(内存):查找哈希表 → 返回值,耗时约 1~10 μs
  • MySQL(磁盘 + B+ 树):索引查找 → 磁盘 IO → 数据页加载 → 返回值,耗时约 1~10 ms

这就是为什么 Redis 能比 MySQL 快 100~1000 倍的根本原因。

2.2 Redis 的内存布局

Redis 使用自定义的内存分配器(默认是 jemalloc)来管理内存。每个键值对在内存中的存储结构如下:

c 复制代码
// Redis 中每个键值对的核心数据结构
typedef struct redisObject {
    unsigned type:4;       // 数据类型(string/list/hash/set/zset)
    unsigned encoding:4;   // 内部编码方式(int/embstr/raw/ziplist/...)
    unsigned lru:24;       // LRU 时间戳(用于内存淘汰)
    int refcount;          // 引用计数
    void *ptr;             // 指向实际数据的指针
} robj;
// sizeof(robj) = 16 字节

注意 encoding 字段------Redis 的每种数据类型都有多种内部编码方式。这是 Redis 高性能的第二个秘密:自适应编码

2.3 内存操作不意味着"没有IO"

一个常见的误区是"Redis 在内存中所以没有 IO"。实际上 Redis 的持久化机制(RDB/AOF)会产生磁盘 IO,但关键在于:

  • RDB 快照:fork 子进程做持久化,利用操作系统的 COW(Copy-On-Write)机制,不阻塞主线程
  • AOF 追加:每次写操作追加到 AOF 缓冲区,由后台线程异步刷盘
  • 读操作:100% 在内存中完成,零磁盘 IO

也就是说,读操作完全不涉及磁盘,写操作的磁盘 IO 被异步化了。用户感知到的延迟只有内存操作的延迟。


三、数据结构:同一个命令,不同的引擎

Redis 最让人叹服的设计之一,是对外暴露简单的 5 种数据类型,内部却有十几种编码方式,并且会根据数据的实际情况自动选择最优编码。

3.1 类型与编码的映射关系

数据类型 内部编码 触发条件
String int 值为 64 位整数
embstr 字符串 ≤ 44 字节
raw 字符串 > 44 字节
List listpack(旧版 ziplist) 元素数 ≤ 128 且每个元素 ≤ 64 字节
quicklist 超出上述阈值
Hash listpack 键值对数 ≤ 128 且每个值 ≤ 64 字节
hashtable 超出上述阈值
Set intset 所有元素均为整数且元素数 ≤ 512
listpack 元素数 ≤ 128 且每个元素 ≤ 64 字节
hashtable 超出上述阈值
ZSet listpack 元素数 ≤ 128 且每个元素 ≤ 64 字节
skiplist + hashtable 超出上述阈值

以上阈值基于 Redis 7.x 默认配置,可通过 list-max-listpack-sizehash-max-listpack-entries 等参数调整。

可以用 OBJECT ENCODING <key> 命令查看任意 key 的实际编码:

bash 复制代码
# String 类型的三种编码
127.0.0.1:6379> SET counter 42
OK
127.0.0.1:6379> OBJECT ENCODING counter
"int"                                    # 整数直接用 int 编码

127.0.0.1:6379> SET name "hello"
OK
127.0.0.1:6379> OBJECT ENCODING name
"embstr"                                 # 短字符串用 embstr

127.0.0.1:6379> SET bio "a]very-long-string-that-exceeds-44-bytes-limit-for-embstr-encoding"
OK
127.0.0.1:6379> OBJECT ENCODING bio
"raw"                                    # 长字符串用 raw

# Hash 类型的编码升级
127.0.0.1:6379> HSET user:1 name "Alice"
(integer) 1
127.0.0.1:6379> OBJECT ENCODING user:1
"listpack"                               # 少量字段用 listpack(紧凑)

# 当字段数超过阈值后自动升级为 hashtable
127.0.0.1:6379> # (插入 129 个字段后)
127.0.0.1:6379> OBJECT ENCODING user:1
"hashtable"                              # 大量字段用 hashtable(快速查找)

3.2 为什么小数据要用紧凑编码?

以 Hash 类型为例,当字段少的时候用 listpack,字段多了才用 hashtable。为什么不一直用 hashtable?

hashtable 的开销

ini 复制代码
每个 dictEntry = 24 字节(key指针 + value指针 + next指针)
每个 redisObject = 16 字节
每个 SDS 字符串 = header(3~17字节) + 内容 + '\0'

一个只存 3 个字段的 Hash,用 hashtable 编码:
3 × (24 + 16×2 + SDS×2) ≈ 300+ 字节的元数据开销

listpack 的做法

把所有字段名和值连续存储在一块内存中,没有指针、没有额外的对象头:

css 复制代码
[总字节数][字段1名][字段1值][字段2名][字段2值]...[结束标记]

3 个字段只需要 50~100 字节。内存省了 3~5 倍,而且由于数据连续存储,对 CPU 缓存非常友好------L1/L2 缓存命中率高,遍历速度反而比 hashtable 更快(在数据量小的时候)。

这就是 Redis 数据结构设计的核心哲学:小数据用紧凑编码省内存、提升缓存命中率;大数据切换到复杂结构保证查询效率。

图2 展示了 Redis 数据结构的双层设计。上层是用户看到的 5 种数据类型,下层是实际使用的底层编码。随着数据量增长,编码会自动升级(但不会降级)。小数据用紧凑编码优化内存和缓存命中,大数据用高级结构优化查询复杂度。

3.3 跳表(Skip List):ZSet 的高性能排序引擎

ZSet(有序集合)是 Redis 最复杂的数据结构,底层使用跳表 + 哈希表的组合:

  • 跳表 负责按分数排序,支持范围查询(ZRANGEBYSCORE
  • 哈希表 负责 O(1) 查找单个元素的分数(ZSCORE

跳表的核心思想是"空间换时间"------通过建立多层索引,将链表的 O(n) 查找优化到 O(log n):

yaml 复制代码
Level 3:  1 ────────────────────────────> 9 ──────> NULL
Level 2:  1 ────────> 4 ────────────────> 9 ──────> NULL
Level 1:  1 ───> 3 ─> 4 ───> 6 ────────> 9 ──────> NULL
Level 0:  1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 9 -> NULL

查找元素 7 的路径:
Level 3: 1 → 9(9 > 7,下降)
Level 2: 1 → 4 → 9(9 > 7,下降)
Level 1: 4 → 6 → 9(9 > 7,下降)
Level 0: 6 → 7(找到!)

只访问了 7 个节点,而普通链表需要访问全部 9 个节点

Redis 的跳表实现有几个精心的设计:

c 复制代码
typedef struct zskiplistNode {
    sds ele;                        // 元素值
    double score;                   // 排序分数
    struct zskiplistNode *backward; // 后退指针(方便逆序遍历)
    struct zskiplistLevel {
        struct zskiplistNode *forward;  // 前进指针
        unsigned long span;             // 跨度(用于快速计算排名)
    } level[];                      // 柔性数组,每个节点的层数随机决定
} zskiplistNode;

为什么选跳表不选红黑树? Redis 的作者 antirez 给出的理由:

  1. 实现简单:跳表代码量约 300 行,红黑树需要 500+ 行,且旋转操作容易出 bug
  2. 范围查询高效:跳表找到起点后沿链表遍历即可,红黑树需要中序遍历
  3. 内存可控:跳表平均每个节点 1.33 个指针(概率 p=0.25),红黑树固定 3 个指针
  4. 并发友好:跳表的局部修改比红黑树的全局旋转更适合加锁(虽然 Redis 单线程不需要,但 ConcurrentSkipListMap 就是这个思路)

3.4 SDS(Simple Dynamic String):比 C 字符串快在哪?

Redis 没有直接使用 C 语言的 char* 字符串,而是自己实现了 SDS:

c 复制代码
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len;        // 已使用长度
    uint8_t alloc;      // 分配的总长度
    unsigned char flags; // 类型标记(sdshdr5/8/16/32/64)
    char buf[];          // 实际的字符数组
};

SDS 比 C 字符串快的三个原因:

操作 C 字符串 SDS
获取长度 O(n) --- 遍历到 \0 O(1) --- 直接读 len 字段
追加字符串 每次 realloc 预分配策略,减少 realloc 次数
二进制安全 遇到 \0 截断 len 判断结尾,可存任意二进制数据

SDS 的预分配策略尤其巧妙:

c 复制代码
// 当 SDS 需要扩容时
if (newlen < 1MB) {
    // 小于 1MB:分配 2 倍空间
    newlen *= 2;
} else {
    // 大于 1MB:每次多分配 1MB
    newlen += 1MB;
}

这样做的效果是:对一个 SDS 多次追加操作(比如 APPEND 命令),大概率只需要一次 realloc,后续追加直接写入预分配的空间。


四、IO 多路复用:一个线程撑起万级连接

4.1 问题的本质:网络 IO 的等待

Redis 是网络服务,每个客户端请求都是一次 TCP 通信。如果用最朴素的方式------一个连接一个线程------那 1 万个并发连接就需要 1 万个线程。线程数一多,上下文切换的开销就会吞噬掉大量 CPU 时间。

IO 多路复用的核心思想是:用一个线程同时监听多个文件描述符(socket),哪个 socket 有数据可读就处理哪个

4.2 三代 IO 多路复用的演进

Linux 上的 IO 多路复用经历了 select → poll → epoll 三代演进:

维度 select poll epoll
最大连接数 1024(FD_SETSIZE) 无限制 无限制
就绪检测 O(n) 线性扫描全部 fd O(n) 线性扫描 O(1) 回调通知
数据拷贝 每次调用都要复制整个 fd 集合到内核 同 select 只需注册一次
触发方式 水平触发 水平触发 支持边缘触发 + 水平触发
典型场景 连接数 < 100 连接数 < 1000 连接数 > 1000

Redis 在 Linux 上默认使用 epoll ,在 macOS 上使用 kqueue ,在 Solaris 上使用 evport

4.3 epoll 为什么比 select 快?

select 的工作方式是"轮询":每次调用 select() 时,内核要遍历所有注册的 fd,逐一检查是否有事件就绪。如果有 1 万个连接,每次检查就要遍历 1 万次。

epoll 的工作方式是"回调":当 socket 有数据到达时,内核通过回调函数将这个 fd 加入就绪队列。epoll_wait() 只需要检查就绪队列即可,不需要遍历所有 fd。

c 复制代码
// select 模型(伪代码)------ O(n) 每次调用
while (true) {
    fd_set readfds;
    FD_ZERO(&readfds);
    for (int fd : all_client_fds) {     // 每次都要重新设置
        FD_SET(fd, &readfds);
    }
    select(maxfd + 1, &readfds, NULL, NULL, NULL);  // 内核遍历所有 fd

    for (int fd : all_client_fds) {     // 用户态再遍历一次
        if (FD_ISSET(fd, &readfds)) {
            handle_request(fd);
        }
    }
}

// epoll 模型(伪代码)------ O(1) 获取就绪事件
int epfd = epoll_create(1);

// 只需注册一次
for (int fd : all_client_fds) {
    epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);  // 注册到内核
}

while (true) {
    // 只返回就绪的 fd,不需要遍历所有连接
    int n = epoll_wait(epfd, events, MAX_EVENTS, -1);

    for (int i = 0; i < n; i++) {       // 只遍历有事件的 fd
        handle_request(events[i].data.fd);
    }
}

假设 1 万个连接中只有 10 个有数据到达:

  • select:遍历 1 万个 fd → 发现 10 个就绪 → 处理 10 个请求
  • epoll:直接返回 10 个就绪 fd → 处理 10 个请求

连接数越多,epoll 的优势越明显。这就是 Redis 能用单线程处理数万并发连接的关键。

图3 展示了 select 与 epoll 的核心区别。select 每次调用都需要将整个 fd 集合拷贝到内核并线性扫描;epoll 通过注册机制和就绪队列,实现了 O(1) 的事件获取效率。

4.4 Redis 的事件循环(Event Loop)

Redis 在 IO 多路复用之上构建了一个事件驱动的主循环,这是它的运行核心:

c 复制代码
// Redis 事件循环(ae.c 简化版)
void aeMain(aeEventLoop *eventLoop) {
    eventLoop->stop = 0;
    while (!eventLoop->stop) {
        // 1. 执行 beforesleep 回调(处理一些周期性任务)
        if (eventLoop->beforesleep != NULL)
            eventLoop->beforesleep(eventLoop);

        // 2. 调用多路复用 API 等待事件(epoll_wait / kqueue / ...)
        //    同时处理定时事件的超时
        aeProcessEvents(eventLoop, AE_ALL_EVENTS | AE_CALL_BEFORE_SLEEP);
    }
}

// aeProcessEvents 内部逻辑
int aeProcessEvents(aeEventLoop *eventLoop, int flags) {
    // 计算最近的定时器到期时间作为 epoll_wait 的超时
    struct timeval *tvp = getShortestTimerTimeout();

    // 调用底层的多路复用 API
    numevents = aeApiPoll(eventLoop, tvp);  // epoll_wait()

    for (int i = 0; i < numevents; i++) {
        aeFileEvent *fe = &eventLoop->events[eventLoop->fired[i].fd];

        // 可读事件 → 读取客户端命令
        if (fe->mask & AE_READABLE) {
            fe->rfileProc(eventLoop, fd, fe->clientData, mask);
        }

        // 可写事件 → 发送响应给客户端
        if (fe->mask & AE_WRITABLE) {
            fe->wfileProc(eventLoop, fd, fe->clientData, mask);
        }
    }

    // 处理到期的定时事件(如过期键清理、持久化触发等)
    processTimeEvents(eventLoop);

    return numevents;
}

整个流程可以概括为:等待事件 → 处理网络请求 → 处理定时任务 → 回到等待。这就是经典的 Reactor 模式。


五、单线程模型:没有锁才是最快的锁

5.1 "单线程"到底是什么意思?

一个常见的误解是"Redis 整个进程只有一个线程"。实际上 Redis 有多个线程,但处理客户端命令的核心路径是单线程的。

vbnet 复制代码
Redis 进程中的线程分工:

主线程(单个):
  ├── 接收客户端连接
  ├── 读取客户端命令
  ├── 执行命令(操作内存中的数据结构)
  └── 将响应写回客户端

后台线程(多个):
  ├── bio_close_file:异步关闭大文件
  ├── bio_aof_fsync:AOF 文件异步刷盘
  └── bio_lazy_free:异步释放大对象内存(UNLINK、FLUSHDB ASYNC)

IO 线程(Redis 6.0+,可选):
  ├── io_thread_1:辅助读取客户端请求
  ├── io_thread_2:辅助写入客户端响应
  └── ...(可配置多个)

关键点:命令执行永远是单线程的。即使开启了 IO 多线程,命令的解析和执行仍然在主线程中串行完成。

5.2 单线程为什么反而快?

这是 Redis 面试中最高频的问题。答案是:对于 Redis 的场景,单线程避免的开销 > 多线程带来的收益

多线程带来的开销

  1. 锁竞争 :多线程操作共享数据结构需要加锁。一个 INCR 操作在多线程下变成:加锁 → 读值 → +1 → 写值 → 解锁。锁本身就是一次原子操作(CAS 或 mutex),在高并发下会导致大量线程阻塞和唤醒。

  2. 上下文切换:线程切换一次的成本约 1~10 μs,包括保存/恢复寄存器、刷新 TLB、切换内核栈。如果有 100 个线程竞争同一把锁,上下文切换的开销会远超实际业务操作。

  3. 缓存失效:线程切换后,新线程的工作集大概率不在 L1/L2 缓存中,导致 cache miss。Redis 的数据结构遍历(如 ziplist 扫描)非常依赖缓存命中率。

单线程的优势

scss 复制代码
单线程执行一个 GET 命令的完整路径:

1. epoll_wait 返回可读事件          ~0 ns(事件已就绪)
2. read() 系统调用读取请求           ~1 μs
3. 解析 RESP 协议                   ~0.1 μs
4. 在哈希表中查找 key               ~0.1 μs
5. write() 系统调用发送响应          ~1 μs

总计:约 2~3 μs,全程无锁、无切换、缓存热数据常驻 L1

Redis 的命令执行时间极短(通常在 1 μs 以下),瓶颈根本不在 CPU 计算上,而在网络 IO 上。用多线程来加速一个 0.1 μs 的哈希查找没有意义,反而引入了锁的开销。

5.3 Redis 6.0 的 IO 多线程

既然瓶颈在网络 IO,Redis 6.0 引入了 IO 多线程------用多个线程并行处理网络读写,但命令执行仍然是单线程。

图4 对比了 Redis 6.0 前后的线程模型。6.0 之前完全单线程处理所有网络 IO 和命令执行;6.0 之后可以开启 IO 多线程,将网络读写分散到多个线程,但命令执行仍然是主线程串行处理,保持了无锁的简洁性。

开启方式:

conf 复制代码
# redis.conf
io-threads 4           # IO 线程数(建议设为 CPU 核数的一半)
io-threads-do-reads yes # 同时开启读操作的多线程(默认只有写是多线程的)

IO 多线程的工作流程:

markdown 复制代码
1. 主线程通过 epoll 收集所有就绪的客户端连接
2. 主线程将这些连接分配给 N 个 IO 线程
3. IO 线程并行读取请求数据并解析协议(无锁,因为各线程处理不同连接)
4. 主线程等待所有 IO 线程读取完成
5. 主线程串行执行所有命令(核心单线程,无锁)
6. 主线程将响应分配给 IO 线程
7. IO 线程并行将响应写回客户端
8. 主线程等待所有 IO 线程写入完成
9. 回到步骤 1

根据 Redis 官方的基准测试,开启 IO 多线程后,吞吐量可以提升一倍(从 10 万 QPS 到 20 万 QPS),同时保持了命令执行的单线程简洁性。

5.4 单线程的代价:慢查询的致命影响

单线程是把双刃剑。它的代价是:一个慢命令会阻塞所有后续请求

以下操作在生产中需要格外小心:

危险命令 问题 替代方案
KEYS * O(n) 遍历所有键 SCAN 渐进式遍历
HGETALL(大 Hash) 一次性返回全部字段 HSCAN 分批获取
DEL(大 key) 同步释放大量内存 UNLINK 异步释放
FLUSHDB 同步清空整个库 FLUSHDB ASYNC
SORT 大数据集排序 业务层排序或限制排序范围
Lua 脚本(长时间运行) 脚本执行期间阻塞一切 控制脚本复杂度,使用 redis.replicate_commands()

一个血泪教训:某次线上故障,开发在 Redis 中存了一个 500 万元素的 Set,然后调了一次 SMEMBERS。这一个命令耗时 3 秒,直接导致 3 秒内所有请求超时,告警炸了一屏。

最佳实践 :使用 redis-cli --bigkeys 定期扫描大+ key,使用 slowlog get 监控慢查询。


六、RESP 协议:简单就是快

6.1 RESP 协议格式

Redis 客户端与服务端之间使用 RESP(Redis Serialization Protocol) 通信。它是一个基于文本的协议,人眼可读,解析极简:

bash 复制代码
客户端发送 SET name Alice:

*3\r\n         ← 数组,包含 3 个元素
$3\r\n         ← 第 1 个元素:长度为 3 的字符串
SET\r\n        ← "SET"
$4\r\n         ← 第 2 个元素:长度为 4 的字符串
name\r\n       ← "name"
$5\r\n         ← 第 3 个元素:长度为 5 的字符串
Alice\r\n      ← "Alice"

服务端响应:
+OK\r\n        ← 简单字符串响应

RESP 的解析只需要逐字节读取,遇到 \r\n 就知道当前元素结束了。没有复杂的转义、嵌套或编码问题,解析速度极快。

6.2 Pipeline:批量操作的大杀器

普通模式下,每个命令都是一次完整的 RTT(Round Trip Time):

arduino 复制代码
普通模式(100 个 SET 命令):
Client → SET k1 v1 → Server → +OK → Client
Client → SET k2 v2 → Server → +OK → Client
...(重复 100 次)
总耗时 ≈ 100 × RTT(如果 RTT = 1ms,则 100ms)

Pipeline 模式下,客户端一次性发送所有命令,服务端一次性返回所有响应:

vbscript 复制代码
Pipeline 模式(100 个 SET 命令):
Client → SET k1 v1
         SET k2 v2
         ...
         SET k100 v100
                        → Server(批量执行)
                        → +OK +OK ... +OK → Client
总耗时 ≈ 1 × RTT + 执行时间(约 1~2ms)

性能对比(基于局域网 RTT ≈ 0.1ms 的环境):

模式 100 条命令耗时 QPS
逐条执行 ~10 ms ~10,000
Pipeline ~0.5 ms ~200,000

Pipeline 的加速倍数取决于网络延迟:延迟越高(比如跨机房),加速效果越明显。

java 复制代码
// Jedis Pipeline 示例
try (Jedis jedis = jedisPool.getResource()) {
    Pipeline pipeline = jedis.pipelined();

    for (int i = 0; i < 10000; i++) {
        pipeline.set("key:" + i, "value:" + i);
    }

    // 一次性发送所有命令并接收所有响应
    List<Object> results = pipeline.syncAndReturnAll();
}

七、性能实测:用数据说话

理论分析之后,用 redis-benchmark 做一组实际测试。测试环境:macOS M1 Pro、Redis 7.2、默认配置。

bash 复制代码
# 测试 GET/SET 操作,100 个并发连接,10 万次请求
redis-benchmark -t get,set -c 100 -n 100000 -q

典型结果:

sql 复制代码
SET: 135,135.14 requests per second, p50=0.367 msec
GET: 142,857.14 requests per second, p50=0.351 msec

开启 Pipeline 后的对比:

bash 复制代码
# Pipeline 模式,每次批量发送 16 条命令
redis-benchmark -t get,set -c 100 -n 100000 -P 16 -q
sql 复制代码
SET: 1,052,631.58 requests per second, p50=0.543 msec
GET: 1,176,470.59 requests per second, p50=0.487 msec

Pipeline=16 时吞吐量提升了约 8 倍,突破百万 QPS。

不同数据大小的影响

bash 复制代码
# 不同 value 大小对 SET 性能的影响
redis-benchmark -t set -c 50 -n 100000 -d 10 -q     # 10 字节
redis-benchmark -t set -c 50 -n 100000 -d 1000 -q   # 1 KB
redis-benchmark -t set -c 50 -n 100000 -d 10000 -q  # 10 KB
redis-benchmark -t set -c 50 -n 100000 -d 100000 -q # 100 KB
Value 大小 QPS 说明
10 B ~130,000 基线性能
1 KB ~110,000 轻微下降,影响不大
10 KB ~60,000 明显下降,网络带宽开始成为瓶颈
100 KB ~12,000 大幅下降,内存拷贝和网络传输成为主要开销

结论:Redis 最适合小数据高并发场景。Value 超过 10 KB 后性能显著下滑,超过 100 KB 则不建议使用 Redis。


八、性能优化实践清单

基于以上分析,整理一份可直接落地的优化清单:

数据结构层面

bash 复制代码
# 1. 监控大 key,避免超大数据结构
redis-cli --bigkeys

# 2. 检查慢查询日志
redis-cli SLOWLOG GET 10

# 3. 确保小数据使用紧凑编码
# 如果你的 Hash 只有几十个字段,确认阈值设置合理
CONFIG GET hash-max-listpack-entries  # 默认 128
CONFIG GET hash-max-listpack-value    # 默认 64

网络层面

bash 复制代码
# 4. 使用 Pipeline 批量操作(减少 RTT)
# 5. 使用连接池,避免频繁创建/销毁连接
# 6. 开启 IO 多线程(Redis 6.0+,高并发场景)
io-threads 4
io-threads-do-reads yes

命令层面

场景 避免 使用
遍历所有 key KEYS * SCAN 0 MATCH * COUNT 100
获取大 Hash 全部字段 HGETALL HSCANHMGET 指定字段
删除大 key DEL UNLINK(异步删除)
清空数据库 FLUSHDB FLUSHDB ASYNC
集合运算 SUNION(大集合) 业务层分批处理

架构层面

bash 复制代码
# 7. 读写分离(主节点写,从节点读)
# 8. 集群分片(Redis Cluster),水平扩展吞吐量
# 9. 客户端缓存(Redis 6.0+ 的 Client-Side Caching),进一步减少网络请求

参考链接

相关推荐
红尘散仙5 小时前
我把终端小说阅读器接上了 AI Agent:TRNovel 现在能用 skill 生成书源了
人工智能·后端·rust
卷毛的技术笔记6 小时前
告别硬编码!Spring AI Alibaba 实现 AI Agent 智能工具调用(Tool Calling)
java·人工智能·后端·python·spring·ai编程
会编程的土豆6 小时前
Go 语言反射(Reflection)详解
开发语言·后端·golang
喵个咪7 小时前
GoWind Toolkit Go后端代码生成 完整全流程实战
后端·go·orm
basketball6167 小时前
Go 语言从入门到进阶:4. 数组和MAP使用方法总结
开发语言·后端·golang
qq_2518364577 小时前
SpringBoot+Vue 共享电池柜管理系统 完整实现 前后端分离项目实战 完整代码
vue.js·spring boot·后端
zhangxingchao7 小时前
AI 大模型核心六:量化、Workflow 与 Agent、多轮 RAG
前端·人工智能·后端
IT_陈寒9 小时前
Vite打包时遇到的坑,原来问题出在这里
前端·人工智能·后端
ayqy贾杰10 小时前
基层管理的三板斧,在AI时代行不通了
前端·后端·团队管理
Apifox10 小时前
Apifox 5 月更新|Postman 导入优化、Runner 支持非 root 运行、请求代码自动带鉴权
前端·后端·安全