做后端开发的应该都用过 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
| HSCAN user:1001:profile 0 COUNT 100 HDEL user:1001:profile field1 field2 ...
SSCAN user:1001:tags 0 COUNT 100 SREM user:1001:tags member1 member2 ...
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 EXPIRE cache:user:1002 3660 EXPIRE cache:user:1003 3480
|
对于不过期的数据,需要重点关注 idletime,定期清理冷数据:
1 2
| 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
| 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
| const Redis = require('ioredis'); const redis = new Redis();
await redis.mset('key1', 'value1', 'key2', 'value2'); const values = await redis.mget('key1', 'key2');
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
|
MULTI SET {user}:1001 "active" HSET {user}:1001:profile name "tom" EXEC
|
Lua 脚本使用注意事项
集群版本使用 Lua 脚本有特殊要求:
- 所有 Key 必须由
KEYS 数组传递
- 所有 Key 必须在同一个 Slot 上
1 2 3 4 5 6 7 8
| 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
|
EVAL "redis.call('SET', 'user:1', 'active')" 0
EVAL "redis.call('SET', KEYS[1], ARGV[1])" 1 user:1 active
|
Monitor 命令使用限制
MONITOR 命令会输出服务器接收到的所有命令,在高 QPS 场景下:
- 消耗大量 CPU 资源
- 可能导致 Redis 性能急剧下降
- 仅用于短时间调试,不要长时间运行
1 2 3 4 5
| 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
|
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
| 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 使用过程中的一些经验:
- Key 命名:用
业务:表名:id 格式,太长太短都不好
- 拒绝 BigKey:String 别超过 10KB,集合类型元素控制在 5000 以内,删除大 Key 要用渐进式
- 选对数据类型:对象存 Hash,排行榜用 ZSet,标签用 Set
- 设置过期时间:所有数据都要有 TTL,过期时间分散开
- 谨慎用命令:O(N) 命令注意数据量,KEYS/FLUSHALL 这些危险命令要禁用
- 集群注意事项:事务和 Lua 脚本的 Key 要在同一 Slot,用 Hash Tag 解决
- 监控告警:内存、连接数、QPS 都要监控,别等出问题了才发现
规范不是死的,但基本的底线要守住,不然坑的还是自己。