Skip to content
Redis 核心概念

Redis 核心概念

Redis 定位、架构原理、通信协议、Key 规范与通用命令速查。


1. Redis 是什么

RedisREmote Dictionary Server)是一个用 C 语言编写的高性能内存键值(KV)存储系统。由 Salvatore Sanfilippo(antirez)于 2009 年创建,现已成为最流行的 NoSQL 数据库之一。

定位:不止是缓存

角色 说明
缓存(Cache) 最核心的使用场景,减轻数据库压力,加速读写
数据库(Database) 支持持久化,可作主存储(如会话、计数器、排行榜)
消息代理(Message Broker) Pub/Sub + Stream 提供消息队列能力

核心特性一览

特性 说明
内存存储 所有数据在内存中,读写速度极快(微秒级)
丰富数据结构 String、Hash、List、Set、Sorted Set、Bitmap、HyperLogLog、GEO、Stream
持久化 RDB 快照 + AOF 日志,保证数据不丢失
主从复制 读写分离、数据冗余、高可用基础
哨兵 / 集群 Sentinel 实现自动故障转移;Cluster 实现水平分片
Lua 脚本 原子性执行复杂逻辑,减少网络往返
事务 MULTI/EXEC 保证命令批量执行的原子性
Pub/Sub 发布订阅模式,支持实时消息推送

2. Redis 为什么快(🔬 深入原理)

Redis 单实例 QPS 可达 10 万+,这背后是多层次优化的结果。

2.1 纯内存操作

数据完全驻留在内存中,读写操作是纳秒级。与基于磁盘的数据库(MySQL、PostgreSQL)相比,省去了磁盘寻道、页缓存等大量开销。

传统数据库:客户端 → SQL解析 → 查询优化 → 磁盘读取 → 返回
Redis:     客户端 → 命令解析 → 内存读写 → 返回

2.2 单线程模型

Redis 使用单线程处理命令,这是它高性能的关键原因之一。

多线程方案 问题
上下文切换 CPU 频繁在线程间切换,浪费时钟周期
锁竞争 共享数据需要加锁,阻塞其他线程
死锁风险 复杂的锁依赖关系可能导致死锁

Redis 选择单线程:

  • 避免上下文切换:没有线程切换的 CPU 开销
  • 无锁化:无需互斥锁、读写锁、自旋锁
  • 代码简洁:逻辑简单,容易维护和调试

Redis 6.0+ 的多线程仅用于网络 I/O(读取客户端请求、写回响应),命令执行仍然是单线程顺序处理。详见第 3 节。

2.3 IO 多路复用(🔬 深入原理)

Redis 采用事件驱动 + IO 多路复用架构,单线程同时监控数千个客户端连接。

多路复用机制

系统 机制 Redis 使用
Linux epoll ✅ 默认,性能最佳
macOS / BSD kqueue
跨平台 select / poll 降级方案

事件驱动架构

Redis 内部有两种事件:

Redis 事件循环
├── 文件事件(File Event)
│   ├── 客户端连接请求(AE_READABLE)
│   └── 客户端命令响应(AE_WRITABLE)
└── 时间事件(Time Event)
    ├── serverCron(定期任务,100ms 一次)
    └── 过期键清理(见 6.3 节)

Reactor 模式简化流程

                        ┌─────────────┐
                        │   Redis     │
    Client 1 ──┐        │   主线程     │
    Client 2 ──┼─── epoll_wait ──►  命令处理
    Client 3 ──┤        │   (单线程)   │
    Client N ──┘        └─────────────┘

1. epoll_wait() 阻塞等待事件
2. 有事件到达 → 读取请求 → 解析命令 → 执行 → 写回响应
3. 回到步骤 1

核心要点:

  • 非阻塞 I/O:socket 设置为 non-blocking,读不到数据立即返回
  • 事件通知:epoll 在有数据可读时才通知,无需轮询
  • 单线程循环:一个 while 循环处理所有事件

2.4 高效数据结构

Redis 没有使用通用库的数据结构,而是专门优化过的:

数据结构 Redis 实现 优化点
字符串 SDS(Simple Dynamic String) O(1) 获取长度、预分配、惰性释放、二进制安全
列表 quicklist(3.2+) linkedlist + ziplist 混合,节省内存
哈希(小) ziplist / listpack 连续内存,省指针开销
哈希(大) dict(哈希表) 渐进式 rehash,避免阻塞
有序集合 skiplist + dict 跳表支持范围查询,字典支持点查询

2.5 RESP 协议简单

Redis 自定义的序列化协议(RESP)极其精简,解析开销极小。详见第 4 节。


3. 单线程模型详解(🔬 深入原理)

3.1 为什么用单线程

核心原因:Redis 的性能瓶颈是内存大小网络带宽,不是 CPU。

维度 分析
命令执行速度 普通 GET/SET 仅需 微秒级,单核 CPU 完全够用
瓶颈在哪 内存带宽(~50 GB/s)+ 网络带宽(10 Gbps),CPU 有余力
并发开销 多线程的锁、同步、上下文切换反而拖慢简单操作
代码质量 单线程逻辑清晰,bug 少,容易做持久化、复制等功能

3.2 单线程的局限性

单线程意味着同一时刻只能执行一条命令,慢操作会阻塞整个实例。

场景 阻塞影响 示例
KEYS * 遍历所有 key,百万级 key 阻塞数秒 KEYS user:*
大 key 删除 DEL 一个含百万元素的 list 阻塞 DEL big_list
FLUSHALL 清空所有数据时阻塞 FLUSHALL
CPU 密集计算 复杂 Lua 脚本、SORT 大集合 SORT big_set
多核未利用 一个 Redis 实例只用一个 CPU 核心 64 核服务器跑 1 实例浪费 63 核

解决方案

问题 解决方案
KEYS 阻塞 使用 SCAN 游标遍历
DEL 阻塞 使用 UNLINK 异步删除(4.0+)
FLUSHALL 阻塞 使用 FLUSHALL ASYNC(4.0+)
多核利用 一台机器启动多个 Redis 实例,或使用 Cluster 分片集群

3.3 Redis 6.0+ 多线程 IO

Redis 6.0 引入了多线程 I/O,但核心仍是单线程。

        Redis 6.0+ 架构

   Client ──► IO Thread 1 ──┐
   Client ──► IO Thread 2 ──┤  读请求
   Client ──► IO Thread 3 ──┘
                              │
                     ┌────────▼────────┐
                     │   主线程(单线程)   │ ← 命令执行仍在这里
                     │   解析+执行+响应   │
                     └────────┬────────┘
                              │
   Client ◄── IO Thread 1 ──┤
   Client ◄── IO Thread 2 ──┤  写响应
   Client ◄── IO Thread 3 ──┘

🚨 关键理解:多线程仅负责 socket 的读写(解析请求包、写回响应包),命令的实际执行始终在主线程,严格顺序处理。这意味着命令之间仍然没有并发问题,Lua 脚本、事务、watch 等机制完全不受影响。

配置项

# redis.conf
io-threads 4                  # IO 线程数,建议不超过 CPU 核数
io-threads-do-reads yes       # 读也使用多线程(默认只写多线程)

💡 最佳实践:IO 多线程在高并发大吞吐场景(单实例 QPS > 10w)才需要开启。一般场景下默认单线程足够,开启反而增加线程调度开销。


4. 通信协议 RESP

RESP(REdis Serialization Protocol)是 Redis 客户端与服务端之间的通信协议。设计理念:实现简单、解析快速、人类可读

4.1 五种数据类型

RESP 通过首字节区分数据类型:

首字节 类型 说明 示例
+ Simple String 简单字符串 +OK\r\n
- Error 错误 -ERR unknown command\r\n
: Integer 整数 :1000\r\n
$ Bulk String 批量字符串(二进制安全) $5\r\nhello\r\n
* Array 数组 *2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n

4.2 请求与响应示例

SET 请求与响应

# 客户端发送
*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n

# 解析:
# *3      → 数组,3个元素
# $3 SET  → 批量字符串,长度3,"SET"
# $3 key  → 批量字符串,长度3,"key"
# $5 value→ 批量字符串,长度5,"value"

# 服务端返回
+OK\r\n

GET 请求与响应

# 客户端发送
*2\r\n$3\r\nGET\r\n$3\r\nkey\r\n

# 服务端返回(存在时)
$5\r\nvalue\r\n

# 服务端返回(不存在时)
$-1\r\n                    # Null Bulk String

常见响应速查

响应 含义
+OK 成功
+PONG PING 响应
:1 整数 1(如成功影响行数)
$-1 Null(key 不存在)
*-1 Null Array(如 BLPOP 超时)
-ERR ... 错误

4.3 Pipeline(管道)

RESP 的设计天然支持 Pipeline:客户端一次性发送多条命令,无需等待逐条响应。

# Pipeline 发送(一次网络往返)
*3\r\n$3\r\nSET\r\n$3\r\nk1\r\n$2\r\nv1\r\n
*3\r\n$3\r\nSET\r\n$3\r\nk2\r\n$2\r\nv2\r\n
*2\r\n$3\r\nGET\r\n$3\r\nk1\r\n

# 服务端按序返回三条响应
+OK\r\n
+OK\r\n
$2\r\nv1\r\n

性能提示:Pipeline 将 N 次网络往返降为 1 次,在 RTT 高的场景下提升显著。但注意:Pipeline 不是原子的,中间可能穿插其他客户端的命令。


5. Key 设计规范(💡 最佳实践)

5.1 命名格式

推荐使用层级冒号分隔格式:

业务名:对象名:id[:字段名]

# 示例
user:1001:profile           # 用户 1001 的个人信息
user:1001:session           # 用户 1001 的会话
order:20240101:total        # 2024-01-01 订单总额
cache:article:hot:10        # 热门文章缓存 top10

5.2 命名原则

原则 说明 好例子 坏例子
可读性 见名知意 user:1001:email u:1001:e
层级化 : 分隔形成树形结构 app:module:sub:id app-module_sub_id
防冲突 业务前缀避免键名重复 order:id:1001user:id:1001 1001(谁都能用)
长度控制 不超过 44 字节(embstr 阈值) u:1001:pr user_profile_cache_for_user_id_1001
避免特殊字符 不用空格、换行、不可打印字符 user:name user name, user\nname

5.3 为什么用 : 分隔

Redis 客户端(如 RedisInsight、RDM)会将 : 解释为层级分隔符,在 UI 中呈现为文件夹结构:

user:                   ← 文件夹
├── user:1001:profile   ← 键
├── user:1001:session
├── user:1002:profile
└── user:1002:session
order:                  ← 文件夹
├── order:20240101:001
└── order:20240101:002

5.4 embstr 编码与长度限制

# embstr 编码:≤44 字节,一次内存分配,CPU 缓存友好
# raw 编码:  >44 字节,两次内存分配

SET key "this_is_a_short_value_under_44_bytes"    # embstr
SET key "this_is_a_very_long_value_exceeding_44_bytes..."  # raw

性能提示:Key 长度控制在 44 字节内,利于 embstr 编码,减少内存分配次数和内存碎片。


6. 通用命令详解

6.1 键操作

KEYS pattern — 查找键

KEYS *              # 匹配所有 key
KEYS user:*         # 匹配以 user: 开头的 key
KEYS *:email        # 匹配以 :email 结尾的 key
KEYS user:?         # ? 匹配单个字符
KEYS user:[ab]      # 匹配 user:a 或 user:b
KEYS user:[^ab]     # 不包含 a 或 b
KEYS user:[1-9]     # 匹配 user:1 到 user:9

🚨 陷阱生产环境严禁使用 KEYS 命令!KEYS 会遍历整个键空间,时间复杂度 O(n)。在百万级 key 的实例上执行 KEYS * 可能阻塞 Redis 数秒甚至数十秒,期间无法处理任何其他请求。请使用 SCAN 替代。

SCAN — 游标迭代(生产推荐)

SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]
# 基本用法:默认 COUNT 为 10
SCAN 0

# 匹配模式 + 指定每次扫描量(建议 COUNT 100-1000)
SCAN 0 MATCH user:* COUNT 100

# 按类型过滤(6.0+)
SCAN 0 TYPE string
SCAN 0 MATCH *:cache TYPE hash

# 迭代直到游标归零
SCAN 0 MATCH session:* COUNT 500    # 返回 (new_cursor, [keys...])
SCAN 15 MATCH session:* COUNT 500   # 使用上次返回的新游标继续
SCAN 0 MATCH session:* COUNT 500    # 游标回到 0,遍历结束

SCAN 特性

特性 说明
增量迭代 每次只返回少量数据,不阻塞
游标 使用返回的新游标继续,直到游标返回 0
可能重复 增量过程中 key 增减,可能导致同一 key 返回多次
不保证完整 增量过程中新增的 key 可能遍历不到
COUNT 建议 只是提示值,非精确返回量,建议 100-1000

💡 最佳实践COUNT 不是精确控制,Redis 可能返回更多或更少的元素。COUNT 越大,每次迭代的数据越多,总迭代次数越少,但单次时间越长。需要根据业务权衡。

DEL / UNLINK — 删除键

DEL key [key ...]       # 同步删除,阻塞
UNLINK key [key ...]    # 异步删除,非阻塞(4.0+)
DEL user:1001           # 删除一个
DEL k1 k2 k3            # 批量删除,返回删除数量
UNLINK big_hash         # 大 key 推荐使用,后台异步回收内存

🚨 陷阱DEL 删除大 key(如百万元素的 List、大 Hash)时是同步阻塞的,时间复杂度 O(n)。大 key 删除期间整个 Redis 无法响应。生产环境请用 UNLINK 替代 DEL,它会将内存回收任务交给后台线程,主线程不会被阻塞。

EXISTS — 判断键是否存在

EXISTS key [key ...]    # 返回存在的 key 数量(3.0.3+ 支持多个)

EXISTS user:1001               # 返回 1(存在)或 0(不存在)
EXISTS user:1001 user:1002     # 返回 2(两个都存在),或 1(只有一个存在)

EXPIRE / PEXPIRE / EXPIREAT — 设置过期时间

EXPIRE key seconds              # 秒级过期
PEXPIRE key milliseconds        # 毫秒级过期
EXPIREAT key unix_timestamp     # Unix 时间戳(秒)过期
PEXPIREAT key unix_timestamp_ms # Unix 时间戳(毫秒)过期(精确)
EXPIRE session:abc 3600           # 1 小时后过期
PEXPIRE session:abc 1800000       # 30 分钟后过期(毫秒)
EXPIREAT session:abc 1704067200   # 2024-01-01 00:00:00 UTC 过期

# 对已存在的 key 设置过期——会覆盖原有过期时间
EXPIRE user:1001 600

# 注意:SET 默认会移除过期时间!需要加 EX/PX 参数保留
SET user:1001 "data"              # 覆盖并清除过期时间!
SET user:1001 "data" EX 600       # 覆盖并设置新的过期时间

🚨 陷阱SET 命令会清除 key 上已有的过期时间。如果你的 key 已有 EXPIRE,再执行 SET 时该 key 会变为永久有效。需要保留过期时间,请使用 SET key value EX secondsSET key value KEEPTTL(6.0+)。

TTL / PTTL — 查看剩余生存时间

TTL key      # 返回剩余秒数
PTTL key     # 返回剩余毫秒数
返回值 含义
正整数 剩余生存时间(秒 / 毫秒)
-1 key 存在但没有设置过期时间(永久有效)
-2 key 不存在
SET temp "data"
EXPIRE temp 60
TTL temp        # 返回 ~60
PTTL temp       # 返回 ~60000

TTL not_exist   # 返回 -2
TTL permanent    # key 存在但无过期时间,返回 -1

PERSIST — 移除过期时间

PERSIST key     # 移除过期时间,变为永久有效

SET temp "data" EX 60
TTL temp        # 返回 ~60
PERSIST temp    # 返回 1(成功)
TTL temp        # 返回 -1(永久有效)

TYPE — 查看键的类型

TYPE key

TYPE user:1001     # 返回 "string"
TYPE article:list  # 返回 "list"
TYPE user:tags     # 返回 "set"
TYPE rankings      # 返回 "zset"
TYPE user:profile  # 返回 "hash"
TYPE none_key      # 返回 "none"(key 不存在)

RENAME / RENAMENX — 重命名

RENAME key newkey       # 重命名,如果 newkey 已存在会覆盖
RENAMENX key newkey     # 仅当 newkey 不存在时才重命名

RENAME user:1001 user:backup_1001      # 覆盖式重命名
RENAMENX temp:001 temp:002             # 如果 temp:002 存在则不执行,返回 0

🚨 陷阱RENAME无条件覆盖 newkey,相当于先 DEL 再 RENAME。如果 newkey 是一个大 key 或重要数据,请先用 EXISTS 检查,或使用 RENAMENX

MOVE — 移动 key 到另一个数据库

MOVE key db

SELECT 0
SET temp "data"
MOVE temp 2            # 将 temp 从 db 0 移动到 db 2
EXISTS temp            # db 0 中返回 0(已移走)
SELECT 2
GET temp               # db 2 中返回 "data"

其他键操作命令

RANDOMKEY              # 随机返回当前数据库的一个 key

DBSIZE                 # 返回当前数据库的 key 数量

SELECT index           # 切换数据库,index: 0-15(默认 16 个库)

# 示例
SELECT 0               # 切换到 db 0
SELECT 1               # 切换到 db 1

6.2 服务器命令

INFO — 服务器信息

INFO                    # 所有信息
INFO server             # 服务器基本信息
INFO clients            # 客户端连接信息
INFO memory             # 内存使用情况
INFO stats              # 统计信息(命令次数、命中率等)
INFO replication        # 主从复制信息
INFO cpu                # CPU 占用
INFO keyspace           # 各数据库 key 数量

# 示例输出片段
INFO memory
# used_memory:1048576          # 已使用内存(字节)
# used_memory_human:1.00M      # 可读格式
# used_memory_rss:2097152       # OS 分配内存(含碎片)
# mem_fragmentation_ratio:2.00  # 内存碎片率 = rss / used_memory

FLUSHDB / FLUSHALL — 清空数据

FLUSHDB              # 清空当前数据库的所有 key
FLUSHALL             # 清空所有数据库(db 0-15)的所有 key

# 异步清空(4.0+,不阻塞)
FLUSHDB ASYNC
FLUSHALL ASYNC

🚨 陷阱FLUSHDBFLUSHALL不可逆的危险操作。生产环境必须通过 rename-command 禁用:

rename-command FLUSHDB ""
rename-command FLUSHALL ""

SAVE / BGSAVE — 持久化保存

SAVE                  # 同步保存,阻塞 Redis 直到完成
BGSAVE                # 异步保存,fork 子进程后台执行(推荐)
LASTSAVE              # 查看上次成功保存的 Unix 时间戳

# 示例
BGSAVE
# Background saving started
INFO persistence      # 查看持久化状态

性能提示永远不要在生产环境使用 SAVE——它会阻塞 Redis 直到 RDB 文件写完。始终使用 BGSAVE

CLIENT — 客户端管理

CLIENT LIST                     # 列出所有客户端连接
CLIENT KILL ip:port             # 断开指定客户端
CLIENT KILL ADDR 192.168.1.100:54321
CLIENT SETNAME my-app           # 设置当前连接名称
CLIENT GETNAME                  # 获取当前连接名称
CLIENT PAUSE timeout_ms         # 暂停所有客户端请求(维护用)

# CLIENT LIST 输出示例
# id=3 addr=127.0.0.1:54320 fd=8 name= age=10 idle=0 flags=N db=0 sub=0 ...

MONITOR — 实时命令监控

MONITOR

# 实时输出所有命令:
# 1698765432.123456 [0 127.0.0.1:54321] "SET" "key" "value"
# 1698765432.234567 [0 127.0.0.1:54322] "GET" "key"
# 1698765432.345678 [0 127.0.0.1:54321] "DEL" "key"

🚨 陷阱MONITOR显著降低 Redis 性能(约 50%),会将所有命令输出到客户端,流量越大影响越大。仅用于短时调试,生产环境严禁长时间运行。部分云服务商直接禁用了它。

其他服务器命令

SHUTDOWN [NOSAVE|SAVE]    # 关闭服务器;默认先执行一次 SAVE
CONFIG GET parameter       # 读取配置项
CONFIG SET parameter value # 动态修改配置(不持久化到文件)
CONFIG REWRITE            # 将当前配置写回配置文件
PING                       # 测试连通性,返回 PONG
ECHO message               # 回显消息
TIME                       # 返回 Unix 时间戳 + 微秒
COMMAND                    # 获取命令详情列表
COMMAND COUNT              # 返回命令总数

6.3 过期删除策略(🔬 深入原理)

Redis 对过期键的删除采用了惰性删除 + 定期删除的组合策略,在 CPU 和内存之间取得平衡。

策略对比

策略 原理 优点 缺点
定时删除 每个 key 设置定时器,到期立即删除 对内存最友好 CPU 消耗巨大,大量 key 时创建大量定时器,不可行
惰性删除 访问 key 时才检查是否过期 CPU 开销最小 内存不友好,过期 key 长期不访问会一直占用内存
定期删除 定时扫描一批 key,删除其中过期的 CPU 和内存折中 需要精确控制扫描频率和数量

Redis 没有选择定时删除——在几十万个 key 上创建定时器是不可接受的 CPU 开销。

惰性删除

每次执行命令前,Redis 会检查目标 key 是否过期:

// 伪代码
int expireIfNeeded(redisDb *db, robj *key) {
    if (!keyIsExpired(db, key)) return 0;
    // key 已过期,删除
    deleteKey(db, key);
    return 1;
}

优点:对 CPU 最友好,只在访问时检查。

缺点:如果大量 key 过期后从未被访问,它们会一直占据内存(“内存泄漏”)。

定期删除(Active Expiration)

Redis 的后台任务 serverCron 每秒执行 10 次定期删除(每 100ms 一次):

定期删除算法:
1. 从每个数据库随机抽取 20 个 key(ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP)
2. 删除其中已过期的 key
3. 如果过期比例 > 25%,重复步骤 1(说明过期 key 密集)
4. 限制每次执行不超过 25ms(ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC),防止阻塞

速度控制:
- 慢速模式(slow):serverCron 中执行,限时 25ms
- 快速模式(fast):事件循环空闲时执行,限时 1ms
                    定期删除流程

    ┌─────────────────────────────────────────┐
    │         随机抽取 20 个设置了过期的 key      │
    └─────────────────┬───────────────────────┘
                      │
                      ▼
    ┌─────────────────────────────────────────┐
    │           检查并删除其中过期的 key          │
    └─────────────────┬───────────────────────┘
                      │
                      ▼
              ┌───────────────┐
              │  过期比例>25%?  │
              └───┬───────┬───┘
                 是│       │否
                  │       └────────► 退出本轮
                  ▼
    ┌─────────────────────────────────────────┐
    │  重复:再抽 20 个,直到过期比例 ≤25% 或超时  │
    └─────────────────────────────────────────┘

🔬 深入原理:为什么使用随机抽样而不是顺序遍历?因为如果顺序遍历,当大量 key 集中过期时,会导致某次 serverCron 执行严重超时,阻塞命令处理。随机抽样确保每次扫描均匀分布,避免"过期风暴"。

结合效果

惰性删除:兜底 —— 访问时发现过期就删
  +
定期删除:主动扫描 —— 周期性地清理过期 key
  =
CPU 开销可控 + 内存及时回收

🚨 陷阱:如果一个 key 过期后既没有被访问,定期删除又没抽到它(概率低但不是零),它会一直占用内存。这在大量 key 同时过期(如缓存预热同一时间设置相同 TTL)时尤为明显——可能出现内存"不降"的现象。解决方案:给过期时间加随机偏移量,避免集中过期:

# 不要这样
EXPIRE key 3600
# 而是这样(业务层实现)
EXPIRE key $((3600 + RANDOM % 300))  # 3600~3900 秒随机分布

7. 数据库与命名空间

7.1 数据库隔离

Redis 默认提供 16 个逻辑数据库,编号 0 ~ 15

# redis.conf
databases 16           # 可调整为任意数量(建议保持 16 以内)
# 默认操作 db 0
SET user:1001 "Alice"

# 切换到 db 1
SELECT 1
SET user:1001 "Bob"    # 与 db 0 中的 user:1001 相互独立

# 切回 db 0
SELECT 0
GET user:1001          # 返回 "Alice"

7.2 数据库隔离级别

特性 说明
键空间隔离 不同数据库的 key 完全独立,命名不会冲突
独立过期管理 每个数据库有独立的过期字典
共享持久化 RDB/AOF 对所有数据库统一保存和恢复
FLUSHDB 只清空当前数据库
FLUSHALL 清空所有 16 个数据库
内存共享 所有数据库共用一个内存池,无法按库限制内存

7.3 Cluster 模式限制

🚨 陷阱Redis Cluster 模式不支持 SELECT 命令,只能使用 db 0。如果你的应用依赖多数据库来隔离不同业务,迁移到 Cluster 时需要重构,改用 key 前缀(如 app1:..., app2:...)来实现逻辑隔离。这也是为什么生产环境普遍建议只用 db 0并完全依靠 key 前缀做命名空间。

# 单机 / Sentinel 模式:可用多数据库
SELECT 1
SET user:login:1001 "token"

# Cluster 模式:不支持!
127.0.0.1:7000> SELECT 1
(error) ERR SELECT is not allowed in cluster mode

8. 命令时间复杂度速查

命令 时间复杂度 备注
GET, SET, DEL, EXISTS, TYPE O(1) 安全,任意场景可用
HGET, HSET, LPUSH, RPUSH, SADD, ZADD O(1) 安全
KEYS O(n) n 为键空间大小,生产禁用
SCAN O(1) 每次迭代 全量遍历总代价 O(n),但分批执行
DEL(大 key) O(m) m 为元素个数,大 key 用 UNLINK
FLUSHDB / FLUSHALL O(n) n 为数据量,同步执行会阻塞
MONITOR 高开销 生产禁用或仅短时调试

9. 🚨 常见陷阱

9.1 KEYS 阻塞生产环境

# 错误:生产环境执行 KEYS
KEYS *                    # 百万 key 阻塞数秒

# 正确:使用 SCAN 分批迭代
SCAN 0 MATCH user:* COUNT 100
SCAN 15 MATCH user:* COUNT 100   # 继续迭代

9.2 FLUSHDB/FLUSHALL 误操作

# 误操作:本该清空测试库,结果清了生产库
FLUSHALL                  # 不可逆!

# 防御措施 1:rename 为随机字符串
rename-command FLUSHDB as8d7f9a0sd87f
rename-command FLUSHALL a9sd8f70asd8f7

# 防御措施 2:直接禁用
rename-command FLUSHDB ""
rename-command FLUSHALL ""

# 如果必须清空,使用 ASYNC
FLUSHALL ASYNC            # 4.0+ 异步清空

9.3 大 key 的 DEL 阻塞

# 错误:直接 DEL 大 key
DEL huge_list    # 列表有 500 万个元素,阻塞数秒

# 正确:使用 UNLINK 异步删除
UNLINK huge_list           # 主线程不阻塞,后台线程慢慢回收

# 或者分段删除(如果 Redis < 4.0)
# 使用 Lua 脚本分批删除,每次删一小部分

9.4 SET 覆盖过期时间

# 陷阱
EXPIRE user:1001 3600           # 设置 1 小时过期
SET user:1001 "new_data"        # 过期时间被清除!变为永久有效
TTL user:1001                   # 返回 -1(永久)

# 正确做法:SET 时显式设置过期
SET user:1001 "new_data" EX 3600
# 或者 6.0+:
SET user:1001 "new_data" KEEPTTL  # 保留原有过期时间

9.5 MONITOR 性能开销

# MONITOR 会显著拖慢 Redis(约 50% 性能损耗)
# 只用于短时调试,确认问题后立即 Ctrl+C 退出

# 替代方案:查看慢日志
SLOWLOG GET 10            # 最近 10 条慢命令
SLOWLOG LEN               # 慢日志数量
SLOWLOG RESET             # 清空慢日志

9.6 SELECT 与 Cluster 不兼容

如第 7.3 节所述,Cluster 模式只支持 db 0。生产环境从一开始就应该:

  • 只用 db 0
  • 使用 key 前缀隔离不同业务:business1:..., business2:...
  • 避免依赖 SELECT 做逻辑隔离

9.7 过期键不会立即回收

# 设置大量 key 同一时间过期
for i in {1..100000}; do
    SET "cache:$i" "data_$i" EX 600
done

# 600 秒后,不会瞬间回收全部内存
# 惰性删除:等访问时才删
# 定期删除:每 100ms 随机抽 20 个,需要多轮才覆盖完
# 如果这些 key 不再被访问,定期删除也可能扫不全
# 表现:到期后一段时间内内存使用量没有明显下降

# 解决:给 TTL 加随机偏移
EXPIRE key $((600 + RANDOM % 300))  # 10~15 分钟随机分布

9.8 DBSIZE 在数据库切换后混淆

SELECT 0
DBSIZE      # 返回 100(db 0 有 100 个 key)

SELECT 1
DBSIZE      # 返回 50(db 1 有 50 个 key)

# 容易误以为 DBSIZE 返回的是所有数据库的总量
# 实际它只返回当前 SELECT 的数据库的 key 数量

9.9 SCAN 结果可能重复

# SCAN 是增量遍历,遍历过程中 key 可能增减
# 同一 key 可能被返回多次!
# 客户端需要去重处理

# 示例:去重逻辑
seen = set()
cursor = 0
while True:
    cursor, keys = redis.scan(cursor, match="user:*", count=100)
    for key in keys:
        if key not in seen:
            seen.add(key)
            process(key)
    if cursor == 0:
        break

10. 快速参考卡

键操作速查

命令 用途 注意
SCAN 0 MATCH ... COUNT 100 迭代查询 key 替代 KEYS
UNLINK key 异步删除 替代 DEL(大key场景)
EXPIRE key 3600 设置过期(秒) SET 会清除过期时间
TTL key 查看剩余时间 -1=永久,-2=不存在
EXISTS key1 key2 批量判断存在 返回存在的数量
TYPE key 查看 key 类型 返回 “none” 表示不存在
RENAMENX old new 安全重命名 不覆盖已有 key
PERSIST key 移除过期 变永久
DBSIZE 当前库 key 数 仅当前 SELECT 的库

服务器命令速查

命令 用途 注意
INFO memory 内存状态 关注 mem_fragmentation_ratio
INFO keyspace 各库 key 数
FLUSHALL ASYNC 异步清空所有库 4.0+
BGSAVE 后台保存 RDB 永远不用 SAVE
CLIENT LIST 查看连接
SLOWLOG GET 10 查看慢日志 优于 MONITOR
CONFIG GET maxmemory 查看配置
CONFIG SET maxmemory 1gb 动态修改 重启失效,需 CONFIG REWRITE
SHUTDOWN SAVE 安全关闭 关闭前保存