Redis 使用中的常见陷阱与规避策略
Redis 在使用中会遇到哪些坑?如何规避?
用Redis踩过的坑,说几个印象深刻的。
1. 大Key问题
大Key,Redis性能杀手第一名。
典型场景
- 一个String存了10MB的JSON
- 一个Hash存了100万个field
- List存储了数十万条消息
- Set/ZSet包含大量元素
问题表现
- DEL操作阻塞:删除大Key时,整个Redis就卡住了
- 过期清理卡顿:Redis定期清理过期Key,遇到大Key直接卡一下
- 服务抖动:可能发现服务每隔一段时间抖一下,查半天才发现是这个原因
根本原因
Redis是单线程架构。删除一个大Key可能要几百毫秒,这几百毫秒内所有请求都在排队等待。
规避方法
- 大小限制:单个Key别超过10KB,大数据拆小数据
- 异步删除:删大Key用
UNLINK不要用DEL,UNLINK是后台异步删的 - 分批处理:对于List/Hash/Set/ZSet,使用
LPOP/HSCAN/SSCAN/ZSCAN分批处理 - 预估容量:设计阶段就要考虑数据增长,避免无限增长的Key
监控建议
# 查找大Key
redis-cli --bigkeys
# 实时监控阻塞命令
redis-cli monitor | grep -E "(DEL|FLUSHDB|FLUSHALL)"
2. 热Key问题
热Key,打爆单节点。
典型场景
- 秒杀商品的库存
- 微博热搜
- 爆款直播间的在线人数
- 热门文章的阅读数
问题表现
Redis Cluster按Key分片,一个Key只落在一个节点。即使集群有100个节点,热Key那个节点照样被打爆。
解决方案
方案一:Key打散
// 将 hot_key 变成 hot_key_1 到 hot_key_10
String realKey = "hot_key_" + (Math.abs(key.hashCode()) % 10);
读取时随机选择或轮询访问。
方案二:本地缓存
- 应用层加本地缓存(如Caffeine、Guava Cache)
- 热数据缓存1-2秒,大部分请求就不用打Redis了
- 注意本地缓存的内存占用和过期策略
方案三:多级缓存
- L1:本地缓存(毫秒级)
- L2:Redis缓存(秒级)
- L3:数据库(最终一致性)
监控指标
- 单个Key的QPS
- 节点CPU使用率
- 网络带宽使用情况
3. 缓存一致性
缓存一致性,没有银弹。
常见方案及问题
Cache Aside Pattern(旁路缓存)
- 流程:先更新DB,再删除缓存
- 问题:删缓存失败怎么办?需要重试机制
延迟双删
- 流程:删缓存 → 更新DB → 延迟N秒再删缓存
- 问题:延迟多久合适?太短没用,太长浪费时间
先删缓存再更新DB
- 问题:并发情况下可能出现脏读
根本认知
本质问题是:缓存和数据库是两个独立存储,没有分布式事务,必然有不一致的窗口期。
实践建议
- 缩短窗口:你能做的是缩短不一致窗口,不是消灭它
- 业务容忍度:想清楚业务能容忍多久的不一致
- 金融场景:不能容忍?那就别用缓存,或者使用强一致性方案
- 电商展示:几秒不一致无所谓?Cache Aside够用
- 最终一致性:通过消息队列实现最终一致性,但会增加系统复杂度
4. 穿透、击穿、雪崩
面试八股文三连,但真遇到还是能搞死人。
缓存穿透
- 定义:查不存在的数据,缓存没有DB也没有,每次都打DB
- 风险:恶意攻击常用这招,可能导致DB被打挂
- 解决方案:
- 布隆过滤器:快速判断数据是否存在
- 缓存空值:对不存在的数据也缓存空结果,设置较短过期时间
- 参数校验:在应用层增加合法性校验
缓存击穿
- 定义:热Key过期那一瞬间,几万请求同时打到DB
- 解决方案:
- 永不过期:热Key不设过期时间,通过后台任务更新
- 互斥锁:只让一个请求去加载数据,其他请求等待
- 逻辑过期:缓存中存储过期时间,异步更新
缓存雪崩
- 定义:大量Key同时过期,或者Redis挂了
- 解决方案:
- 随机过期:过期时间加随机偏移(如基础时间+随机0-300秒)
- 高可用:Redis做主从复制、哨兵或集群模式
- 熔断降级:DB压力过大时,返回默认值或错误页面
5. 分布式锁陷阱
分布式锁,看起来简单,坑多。
常见陷阱
基础陷阱
- 无过期时间:锁没设过期时间,进程挂了锁永远不释放
- 过期时间不当:锁过期了业务没执行完,另一个进程拿到锁,两个进程同时跑
隐蔽陷阱
- 误删他人锁:DEL释放锁的时候没判断是不是自己的锁,把别人的锁删了
正确实现
获取锁
SET key uuid NX EX 30
NX:只有key不存在时才设置EX 30:30秒后自动过期uuid:唯一标识,用于后续验证
释放锁(Lua脚本保证原子性)
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
进阶方案
- Redlock算法:Redis官方推荐的分布式锁算法
- 看门狗机制:自动续期,避免业务执行时间超过锁过期时间
- 可重入锁:支持同一个线程多次获取同一把锁
6. 持久化问题
RDB快照问题
- Fork阻塞:RDB做快照要fork子进程,数据量大的时候fork很慢
- 内存占用:fork期间主进程和子进程共享内存,写操作会导致内存翻倍
AOF问题
- 文件膨胀:AOF文件越来越大,rewrite的时候也要fork
- 性能影响:
appendfsync always会影响性能,appendfsync no可能丢数据
优化建议
- 主节点配置:用AOF,
appendfsync everysec(平衡性能和安全性) - 从节点配置:开RDB做冷备
- 时间错开:别让RDB和AOF rewrite同时触发
- 监控fork时间:
INFO stats中的latest_fork_usec指标
7. 集群模式限制
命令限制
- MGET/MSET:要求所有Key在同一个slot
- 事务:MULTI/EXEC中的所有Key必须在同一slot
- Lua脚本:脚本中涉及的所有Key必须在同一slot
解决方案:Hash Tag
# 花括号内的部分决定slot
{user_123}_profile
{user_123}_orders
这样两个Key就能放在同一个slot,支持批量操作。
主从切换数据丢失
- 异步复制:Redis主从是异步复制,主节点挂了,没同步到从节点的数据就没了
- 解决方案:
- WAIT命令:确保数据同步到指定数量的从节点(但性能受影响)
- 业务容忍:接受少量数据丢失,通过其他方式补偿
- 监控告警:及时发现主从延迟
8. 内存管理陷阱(补充)
内存碎片
- 问题:频繁的内存分配和释放导致内存碎片
- 监控:
INFO memory中的mem_fragmentation_ratio - 解决:
- 重启Redis实例(最直接)
- 调整
activedefrag参数开启自动碎片整理
内存淘汰策略
- 默认策略:
noeviction(不淘汰,写操作报错) - 推荐策略:
allkeys-lru:适用于热点数据场景volatile-lru:适用于设置了过期时间的场景
- 配置建议:根据业务特点选择合适的淘汰策略
Key过期策略
- 惰性删除:访问时才检查是否过期
- 定期删除:定期随机检查一部分Key
- 内存不足时:主动淘汰过期Key
9. 连接池配置(补充)
连接泄漏
- 问题:忘记关闭Redis连接,导致连接池耗尽
- 解决:
- 使用try-with-resources(Java)
- 连接池配置合理的最大连接数和超时时间
- 监控连接池使用情况
连接池参数优化
# Jedis连接池示例
maxTotal=50 # 最大连接数
maxIdle=20 # 最大空闲连接数
minIdle=5 # 最小空闲连接数
maxWaitMillis=2000 # 获取连接最大等待时间
testOnBorrow=true # 借出时测试连接有效性
客户端选择
- Jedis:简单直接,但需要自己管理连接池
- Lettuce:基于Netty,支持异步和响应式,连接复用更好
- Redisson:高级功能丰富,内置分布式锁、信号量等
10. 监控与告警(补充)
关键监控指标
- 内存使用率:
used_memory_rss/maxmemory - 命中率:
keyspace_hits/ (keyspace_hits+keyspace_misses) - 阻塞客户端数:
blocked_clients - 连接数:
connected_clients - CPU使用率:Redis进程CPU使用情况
告警阈值建议
- 内存使用率 > 80%:预警
- 内存使用率 > 90%:严重告警
- 命中率 < 90%:检查缓存策略
- 阻塞客户端数 > 0:立即处理
- 连接数 > 最大连接数的80%:扩容或优化
监控工具
- Redis自带:
INFO命令、SLOWLOG - 开源工具:Redis Exporter + Prometheus + Grafana
- 云服务:阿里云Redis监控、AWS ElastiCache监控
11. 配置优化建议(补充)
内核参数优化
# /etc/sysctl.conf
vm.overcommit_memory = 1 # 避免fork失败
net.core.somaxconn = 65535 # 提高TCP连接队列
Redis配置优化
# redis.conf
tcp-backlog 65535 # TCP连接队列长度
timeout 300 # 客户端空闲超时
tcp-keepalive 60 # TCP保活检测
maxmemory-policy allkeys-lru # 内存淘汰策略
hash-max-ziplist-entries 512 # Hash压缩阈值
安全配置
- 禁用危险命令:
FLUSHDB、FLUSHALL、KEYS * - 密码认证:设置
requirepass - 网络隔离:Redis只监听内网IP,通过防火墙控制访问
12. 总结与原则
核心认知
踩这些坑的根本原因,是把Redis当成了它不是的东西。
- 它不是数据库:重要数据别只存Redis,要有持久化备份
- 它不是消息队列:虽然有Stream/List,但专业的事交给Kafka/RabbitMQ
- 它就是个缓存:用来解决"快"的问题,核心价值是高性能
设计原则
- 简单性原则:能不用Redis就不用,能用简单方案就不用复杂方案
- 容错性原则:Redis挂了系统还能正常工作(降级策略)
- 监控先行:上线前就要有完善的监控和告警
- 渐进演进:从小规模开始,逐步优化和扩展
最佳实践清单
- ✅ 单Key不超过10KB
- ✅ 热Key做打散或本地缓存
- ✅ 重要操作用Lua脚本保证原子性
- ✅ 合理设置过期时间和淘汰策略
- ✅ 开启持久化并监控fork时间
- ✅ 配置完善的监控告警
- ✅ 定期进行容量规划和性能测试
记住:想清楚你要Redis干什么,坑就少一半。