新手必踩 Redis 10 个低级坑:过期时间、KEYS 命令、持久化误区
Redis 用起来简单,踩起坑来要命。很多人以为自己会用 Redis,其实只是在"会用"和"会坑自己"之间反复横跳。以下这 10 个坑,每一个都是生产环境用真金白银换来的教训。
一、过期时间:看似简单,处处是陷阱
坑1:SET/DEL 会清除过期时间,INCR 不会
这是最容易混淆的点。
bash
bash
set mykey hello ex 300 # 过期时间 300s
ttl mykey # 294
set mykey world # 覆盖了!过期时间被清除
ttl mykey # -1(永不过期)
但用 INCR、LPUSH、HSET 修改值时,过期时间不会被清除:
bash
bash
set counter 1 ex 300
incr counter # 值变了,过期时间还在
ttl counter # 依然在倒计时
记住这个规则:覆盖整个 value 的命令(SET/DEL/GETSET/PERSIST)会清过期时间;局部修改的命令(INCR/LPUSH/HSET)不会。
坑2:EXPIRE 设置负数 = 直接删除
bash
bash
expire mykey -1 # key 立即被删除
expireat mykey 10000 # 时间戳是过去的,key 也被删除
这不是 bug,是特性。但很多人在代码里算错了时间戳,结果 key 莫名消失,排查半天找不到原因。
坑3:RENAME 会继承过期时间
bash
bash
set key_a "a" ex 300
set key_b "b" ex 600
rename key_a key_b # key_b 现在继承 key_a 的 300s 过期时间
老 key 的过期时间会"搬家"到新 key 上,不管新 key 原本有没有过期时间。这个特性很少有人注意,但在数据迁移场景下可能引发意外。
坑4:所有 key 同时过期 → 缓存雪崩
系统初始化时批量写入缓存,所有 key 的 TTL 都设成 3600 秒。到点集体失效,所有请求瞬间穿透到数据库,连接池直接打满。
解法:加随机偏移。
ini
python
import random
ttl = 3600 + random.randint(-300, 300) # ±5分钟随机
r.set(key, value, ex=ttl)
别小看这几分钟的随机值,它能把"集中爆破"变成"均匀释放"。
坑5:Key 忘设 TTL → 内存黑洞
代码里 set user:1001 json 写得飞起,就是不带 EX。半年后 Redis 内存从 2G 涨到 16G,INFO keyspace 显示 key 数量不断增加,但业务说数据量没变------全是没过期的僵尸 key。
写缓存时养成习惯:SET 必须带 EX。
sql
bash
SET user:1001 '{"name":"Alice"}' EX 3600
批量排查无 TTL 的 key:
bash
bash
redis-cli --scan --pattern "*" | while read key; do
ttl=$(redis-cli TTL "$key")
[ "$ttl" = "-1" ] && echo "永不过期: $key"
done
二、KEYS 命令:生产环境的定时炸弹
坑6:KEYS * 能把生产打挂
Redis 是单线程的。KEYS * 是 O(N) 阻塞命令,会从头到尾扫描所有 key,期间所有其他请求全部排队。
数据量小时感觉不到,百万级 key 时,一次 KEYS * 可能阻塞 Redis 数秒。这数秒内,支付超时、订单失败、监控报警,一连串雪崩。
有真实案例:运营同学执行 KEYS coupon:*,800 万 key,阻塞 680ms,直接导致支付接口全线超时,活动被迫暂停。
替代方案:SCAN。
sql
bash
SCAN 0 MATCH user:* COUNT 100
# 返回游标 + 一批 key,下次用游标继续,非阻塞
Python 完整遍历:
ini
python
import redis
r = redis.Redis(host='127.0.0.1', password='yourpassword', decode_responses=True)
cursor = 0
while True:
cursor, keys = r.scan(cursor=cursor, match='user:*', count=100)
for key in keys:
print(key)
if cursor == 0:
break
| 危险命令 | 危险原因 | 安全替代 |
|---|---|---|
| KEYS * | O(N) 阻塞全量遍历 | SCAN |
| HGETALL | 大 Hash 全量读取 | HSCAN |
| SMEMBERS | 大 Set 全量读取 | SSCAN |
| LRANGE key 0 -1 | 大 List 全量读取 | 分页 LRANGE |
| FLUSHALL | 阻塞删除所有 key | FLUSHALL ASYNC(Redis 4.0+) |
生产建议:在 redis.conf 里直接禁用危险命令。
lua
rename-command KEYS ""
rename-command FLUSHALL ""
rename-command FLUSHDB ""
rename-command CONFIG ""
三、持久化:RDB 和 AOF 的经典误区
坑7:只用 RDB,重启丢几分钟数据
RDB 是定时快照,默认触发规则是:
yaml
save 3600 1 # 1小时内1次写操作
save 300 100 # 5分钟内100次写操作
save 60 10000 # 1分钟内10000次写操作
崩溃发生在两次快照之间,这段时间的数据直接丢失。而且 RDB 文件可能因磁盘故障损坏,没有备份就彻底完蛋。
解法:必须开 AOF,或者用混合持久化(Redis 4.0+ 推荐)。
bash
bash
appendonly yes
appendfsync everysec # 推荐:每秒同步,最多丢1秒
aof-use-rdb-preamble yes # 混合持久化,重启更快
坑8:AOF 文件无限膨胀,磁盘写满
AOF 记录每一条写命令,同一个 key 改 1000 次就记 1000 条。时间一长文件可能几十 GB,但实际有效数据只有几百 MB。
解法:配置自动 rewrite。
arduino
bash
auto-aof-rewrite-percentage 100 # 比上次大100%时触发
auto-aof-rewrite-min-size 64mb # 至少64MB才触发
紧急手动触发:redis-cli BGREWRITEAOF
坑9:BGSAVE 失败,RDB 静默停止
INFO persistence 显示:
vbnet
rdb_last_bgsave_status:err
rdb_last_bgsave_error:Can't save in background: fork: Cannot allocate memory
Redis 执行 BGSAVE 时需要 fork 子进程,而 Linux 默认 overcommit_memory=0,内存高时拒绝 fork,导致快照失败、持久化静默停止------你以为数据安全,其实早就不存了。
解法:Redis 官方推荐配置。
bash
bash
echo 1 > /proc/sys/vm/overcommit_memory # 临时生效
echo "vm.overcommit_memory = 1" >> /etc/sysctl.conf # 永久生效
sysctl -p
坑10:不设置密码 + 暴露公网 = 等着被清库
没有密码的 Redis 一旦被扫描到,轻则被清空数据库,重则被植入挖矿程序。这不是假设,是每天都在发生的事。
最低配置:
bash
bash
requirepass yourStrongPassword
bind 127.0.0.1 ::1 # 只允许本地访问
生产环境还要考虑 SSL/TLS 加密、防火墙白名单、定期更新补丁。
总结:一张表记住所有坑
| 编号 | 坑 | 一句话解法 |
|---|---|---|
| 1 | SET 清过期时间,INCR 不清 | 区分"覆盖"和"局部修改" |
| 2 | EXPIRE 负数 = 删 key | 算准时间戳 |
| 3 | RENAME 继承过期时间 | 迁移时注意 |
| 4 | 统一 TTL → 雪崩 | 加随机偏移 |
| 5 | 忘设 TTL → 内存爆炸 | SET 必带 EX |
| 6 | KEYS * 打挂生产 | 用 SCAN 替代 |
| 7 | 只用 RDB 丢数据 | 开 AOF 或混合持久化 |
| 8 | AOF 无限膨胀 | 配置 auto-rewrite |
| 9 | BGSAVE 静默失败 | 设置 overcommit_memory=1 |
| 10 | 无密码 + 公网暴露 | 必须设密码 + 绑定内网 |
Redis 不难,难的是知道哪些"简单操作"背后藏着炸弹。把这 10 个坑刻进肌肉记忆里,比多背 100 个命令有用得多。