Redis 核心概念
Redis 定位、架构原理、通信协议、Key 规范与通用命令速查。
1. Redis 是什么
Redis(REmote 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\nGET 请求与响应
# 客户端发送
*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 # 热门文章缓存 top105.2 命名原则
| 原则 | 说明 | 好例子 | 坏例子 |
|---|---|---|---|
| 可读性 | 见名知意 | user:1001:email |
u:1001:e |
| 层级化 | : 分隔形成树形结构 |
app:module:sub:id |
app-module_sub_id |
| 防冲突 | 业务前缀避免键名重复 | order:id:1001 和 user: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:0025.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 seconds或SET 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 存在但无过期时间,返回 -1PERSIST — 移除过期时间
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 16.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_memoryFLUSHDB / FLUSHALL — 清空数据
FLUSHDB # 清空当前数据库的所有 key
FLUSHALL # 清空所有数据库(db 0-15)的所有 key
# 异步清空(4.0+,不阻塞)
FLUSHDB ASYNC
FLUSHALL ASYNC🚨 陷阱:
FLUSHDB和FLUSHALL是不可逆的危险操作。生产环境必须通过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 mode8. 命令时间复杂度速查
| 命令 | 时间复杂度 | 备注 |
|---|---|---|
| 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:
break10. 快速参考卡
键操作速查
| 命令 | 用途 | 注意 |
|---|---|---|
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 |
安全关闭 | 关闭前保存 |