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),进一步减少网络请求

参考链接

相关推荐
Walter先生2 小时前
实时行情系统设计:从协议选择到高可用架构,再到数据源选型
后端·架构·实时行情数据源
无籽西瓜a2 小时前
【西瓜带你学设计模式 | 第四期 - 抽象工厂模式】抽象工厂模式 —— 定义、核心结构、实战示例、优缺点与适用场景及模式区别
java·后端·设计模式·软件工程·抽象工厂模式
_院长大人_2 小时前
Spring Boot 3.3 + Atomikos 分布式事务日志路径配置踩坑记录
spring boot·分布式·后端
java1234_小锋2 小时前
Java高频面试题:怎么实现Redis的高可用?
java·开发语言·redis
snakeshe10103 小时前
MyBatis 从入门到实践:ORM 核心机制与动态 SQL 全解析
后端
野犬寒鸦3 小时前
高并发利器:SingleFlight优化指南(Java版实现与项目实战)
服务器·开发语言·redis·后端·面试
gelald3 小时前
JVM - 类加载机制
java·jvm·后端
weixin_449190413 小时前
golang中int8溢出
开发语言·后端·golang
深蓝轨迹3 小时前
解决Redis排序后MySQL查询乱序问题:从原因到落地(通用版)
数据库·redis·笔记·mysql·bug