Redis 开发规范与踩坑记录

做后端开发的应该都用过 Redis,但真到了生产环境,各种坑就出来了。键值命名不规范、BigKey 拖垮服务、命令用错导致阻塞…这些问题我都踩过。这篇记录一下我们在实际项目中总结的 Redis 开发规范。

键值设计规范

Key 命名设计

良好的 Key 命名是 Redis 使用的第一步,直接影响可读性和可维护性。

可读性与可管理性

以业务名(或数据库名)为前缀,用冒号分隔,形成层次化的命名空间:

1
2
3
4
5
6
业务名:表名:id

示例:
ugc:video:1
user:profile:10086
order:detail:20240901001
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
┌─────────────────────────────────────────────────────────────────────┐
│ Redis Key 命名规范示例 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 用户系统 │
│ ├── user:profile:{uid} 用户基本信息 │
│ ├── user:session:{token} 用户登录会话 │
│ └── user:settings:{uid} 用户个性化设置 │
│ │
│ 订单系统 │
│ ├── order:detail:{orderId} 订单详情 │
│ ├── order:status:{orderId} 订单状态 │
│ └── order:user:{uid} 用户订单列表 │
│ │
│ 内容系统 │
│ ├── article:content:{id} 文章内容 │
│ ├── article:view:{id} 文章浏览量 │
│ └── article:like:{id} 文章点赞数 │
│ │
└─────────────────────────────────────────────────────────────────────┘

简洁性

在保证语义清晰的前提下,控制 Key 的长度。当 Key 数量达到千万级别时,过长的 Key 会显著增加内存开销。

1
2
3
4
5
反例(过长):
user:{uid}:friends:messages:{mid}

优化后:
u:{uid}:fr:m:{mid}
场景 推荐长度 说明
简单缓存 < 50 字节 u:1001:name
复杂结构 < 100 字节 order:2024:0915:001
特殊场景 < 200 字节 包含较多参数的 Key

禁止使用特殊字符

Key 中不要包含空格、换行、单双引号以及其他转义字符,这些字符可能导致客户端解析错误或命令注入风险。

1
2
3
4
反例:
"user name" ← 包含空格
'user\nprofile' ← 包含换行符
user"data ← 包含引号

Value 设计规范

拒绝 BigKey

BigKey 是 Redis 性能的头号杀手,会导致网卡流量激增、慢查询甚至阻塞主线程。

数据类型 大小限制 元素数量限制
String < 10 KB -
Hash < 10 KB < 5000 个元素
List < 10 KB < 5000 个元素
Set < 10 KB < 5000 个元素
ZSet < 10 KB < 5000 个元素
1
2
3
反例(BigKey):
一个包含 200 万个元素的 List
一个 50 MB 的 String(存储 JSON 数据)

BigKey 的渐进式删除:

非字符串类型的 BigKey,不要使用 DEL 直接删除,应使用扫描命令渐进式删除:

1
2
3
4
5
6
7
8
9
10
11
# Hash 类型渐进删除
HSCAN user:1001:profile 0 COUNT 100
HDEL user:1001:profile field1 field2 ...

# Set 类型渐进删除
SSCAN user:1001:tags 0 COUNT 100
SREM user:1001:tags member1 member2 ...

# ZSet 类型渐进删除
ZSCAN hot:articles 0 COUNT 100
ZREM hot:articles member1 member2 ...

注意:BigKey 设置过期时间时要特别小心。例如一个包含 200 万元素的 ZSet 设置 1 小时过期,到期时会触发 DEL 操作造成阻塞,且该操作不会出现在慢查询日志中(可通过 latency 命令监控)。

选择合适的数据类型

合理选择数据类型可以在节省内存的同时提升性能。

1
2
3
4
5
6
7
反例(使用多个 String):
SET user:1:name tom
SET user:1:age 19
SET user:1:favor football

正例(使用 Hash):
HMSET user:1 name tom age 19 favor football
场景 推荐类型 原因
对象缓存 Hash 节省内存,支持部分更新
计数器 String 支持原子增减
排行榜 ZSet 天然有序,支持范围查询
消息队列 List 支持阻塞读取
标签系统 Set 支持交集、并集运算
分布式锁 String 支持原子操作和过期

控制 Key 的生命周期

Redis 不是垃圾桶,所有数据都应该有过期策略。

1
2
3
4
5
6
7
# 设置过期时间
EXPIRE user:session:token 3600

# 打散过期时间,防止集中过期
EXPIRE cache:user:1001 3540 # 59 分钟
EXPIRE cache:user:1002 3660 # 61 分钟
EXPIRE cache:user:1003 3480 # 58 分钟

对于不过期的数据,需要重点关注 idletime,定期清理冷数据:

1
2
# 查看 Key 的空闲时间
OBJECT idletime user:profile:1001

命令使用规范

O(N) 命令关注 N 的数量

以下命令的时间复杂度为 O(N),使用时必须明确 N 的值:

命令 时间复杂度 建议替代方案
HGETALL O(N) HSCAN + HGET
LRANGE O(S+N) 控制范围大小
SMEMBERS O(N) SSCAN
ZRANGE O(log(N)+M) 控制返回数量
SINTER O(N*M) 分步计算
1
2
3
4
5
6
# 反例:直接获取所有字段
HGETALL user:1001:activities

# 正例:使用扫描分批获取
HSCAN user:1001:activities 0 COUNT 50
HSCAN user:1001:activities 17 COUNT 50

禁用危险命令

以下命令在线上环境应禁止使用或通过 rename-command 配置重命名:

命令 危险原因 替代方案
KEYS 全量扫描,阻塞主线程 SCAN
FLUSHALL 清空所有数据
FLUSHDB 清空当前库
1
2
3
4
# redis.conf 配置禁用危险命令
rename-command KEYS ""
rename-command FLUSHALL ""
rename-command FLUSHDB ""

合理使用 SELECT

Redis 的多数据库功能较弱:

  • 使用数字编号区分,语义不清晰
  • 很多客户端对多数据库支持不完善
  • 多业务使用多数据库实际仍是单线程处理,存在干扰

推荐做法:使用 Key 前缀区分业务,而非多数据库。

1
2
3
4
5
6
7
8
9
10
# 不推荐
SELECT 1
SET user:1001 "data"

SELECT 2
SET order:001 "data"

# 推荐
SET app1:user:1001 "data"
SET app2:order:001 "data"

批量操作提升效率

操作类型 命令 说明
原生批量 MGET, MSET 原子操作,同时支持
Pipeline 打包多个命令 非原子操作,可混合命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Node.js 中使用 Pipeline
const Redis = require('ioredis');
const redis = new Redis();

// 原生批量(原子操作)
await redis.mset('key1', 'value1', 'key2', 'value2');
const values = await redis.mget('key1', 'key2');

// Pipeline(非原子操作,性能更高)
const pipeline = redis.pipeline();
for (let i = 0; i < 100; i++) {
pipeline.set(`key:${i}`, `value:${i}`);
pipeline.expire(`key:${i}`, 3600);
}
await pipeline.exec();

注意:Pipeline 需要客户端和服务端同时支持。批量操作的元素个数建议控制在 500 以内。

谨慎使用事务

Redis 的事务功能较弱:

  • 不支持回滚
  • 集群版本要求一次事务操作的所有 Key 必须在同一个 Slot 上
  • 可以使用 Hash Tag 功能解决 Slot 问题
1
2
3
4
5
6
# 使用 Hash Tag 确保 Key 在同一 Slot
# {user}:1001 和 {user}:1001:profile 会路由到同一个 Slot
MULTI
SET {user}:1001 "active"
HSET {user}:1001:profile name "tom"
EXEC

Lua 脚本使用注意事项

集群版本使用 Lua 脚本有特殊要求:

  1. 所有 Key 必须由 KEYS 数组传递
  2. 所有 Key 必须在同一个 Slot 上
1
2
3
4
5
6
7
8
-- 正确写法:所有 Key 通过 KEYS 传递
local user_key = KEYS[1]
local profile_key = KEYS[2]
local value = ARGV[1]

redis.call('SET', user_key, value)
redis.call('HSET', profile_key, 'status', value)
return 'ok'
1
2
3
4
5
6
# 错误示例:直接在脚本中写死 Key
# 会返回错误:-ERR bad lua script for redis cluster
EVAL "redis.call('SET', 'user:1', 'active')" 0

# 正确示例:通过 KEYS 传递
EVAL "redis.call('SET', KEYS[1], ARGV[1])" 1 user:1 active

Monitor 命令使用限制

MONITOR 命令会输出服务器接收到的所有命令,在高 QPS 场景下:

  • 消耗大量 CPU 资源
  • 可能导致 Redis 性能急剧下降
  • 仅用于短时间调试,不要长时间运行
1
2
3
4
5
# 短暂使用 Monitor 排查问题
redis-cli MONITOR | grep "SLOW_KEY_PATTERN" &
MONITOR_PID=$!
sleep 5
kill $MONITOR_PID

性能优化策略

内存优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
┌─────────────────────────────────────────────────────────────────────┐
│ Redis 内存优化策略 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1. 数据编码优化 │
│ • Hash 使用 ziplist(默认元素 < 512,值 < 64 字节) │
│ • List 使用 quicklist │
│ • Set 使用 intset(元素为整数且数量 < 512) │
│ │
│ 2. 淘汰策略选择 │
│ • volatile-lru:淘汰过期的最近最少使用 │
│ • allkeys-lru:淘汰所有 Key 的最近最少使用 │
│ • volatile-ttl:淘汰即将过期的 │
│ │
│ 3. 内存上限设置 │
│ maxmemory 4gb │
│ maxmemory-policy allkeys-lru │
│ │
└─────────────────────────────────────────────────────────────────────┘

连接优化

1
2
3
4
5
6
7
8
9
10
# 使用连接池,避免频繁创建连接
# Jedis 连接池配置示例
maxTotal=100
maxIdle=50
minIdle=10
maxWaitMillis=3000

testOnBorrow=true
testOnReturn=true
testWhileIdle=true

集群环境注意事项

数据分片与 Slot

Redis Cluster 将数据分布在 16384 个 Slot 上,每个 Key 通过 CRC16 算法映射到特定 Slot:

1
Slot = CRC16(key) % 16384
概念 说明
Slot 数据分片的基本单位,共 16384 个
Master 负责处理 Slot 的读写请求
Slave 复制 Master 数据,提供读服务
迁移 Slot 可以在节点间迁移,实现扩容缩容

跨 Slot 操作限制

以下操作要求所有 Key 在同一个 Slot:

  • 事务(MULTI/EXEC)
  • Lua 脚本
  • 批量操作(MGET/MSET)

解决方案:使用 Hash Tag

1
2
# {user}:1001 和 {user}:1001:profile 会路由到同一个 Slot
MGET {user}:1001 {user}:1001:profile

生产环境检查清单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
┌─────────────────────────────────────────────────────────────────────┐
│ Redis 生产环境检查清单 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 配置检查 │
│ □ 设置 maxmemory 并配置淘汰策略 │
│ □ 禁用危险命令(KEYS、FLUSHALL、FLUSHDB) │
│ □ 启用持久化(RDB + AOF) │
│ □ 配置慢查询日志(slowlog-log-slower-than) │
│ │
│ 设计检查 │
│ □ Key 命名符合业务前缀规范 │
│ □ 所有 Key 设置了合理的过期时间 │
│ □ 不存在 BigKey(String < 10KB,集合 < 5000 元素) │
│ □ 选择合适的数据类型 │
│ │
│ 运维检查 │
│ □ 配置了监控告警(内存、连接数、QPS) │
│ □ 定期备份 RDB 文件 │
│ □ 配置了主从复制或 Cluster 高可用 │
│ □ 开启了连接池复用 │
│ │
└─────────────────────────────────────────────────────────────────────┘

总结

Redis 使用过程中的一些经验:

  1. Key 命名:用 业务:表名:id 格式,太长太短都不好
  2. 拒绝 BigKey:String 别超过 10KB,集合类型元素控制在 5000 以内,删除大 Key 要用渐进式
  3. 选对数据类型:对象存 Hash,排行榜用 ZSet,标签用 Set
  4. 设置过期时间:所有数据都要有 TTL,过期时间分散开
  5. 谨慎用命令:O(N) 命令注意数据量,KEYS/FLUSHALL 这些危险命令要禁用
  6. 集群注意事项:事务和 Lua 脚本的 Key 要在同一 Slot,用 Hash Tag 解决
  7. 监控告警:内存、连接数、QPS 都要监控,别等出问题了才发现

规范不是死的,但基本的底线要守住,不然坑的还是自己。