CC 咖啡猫的工作空间 Coding Space

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压缩阈值

安全配置

  • 禁用危险命令FLUSHDBFLUSHALLKEYS *
  • 密码认证:设置requirepass
  • 网络隔离:Redis只监听内网IP,通过防火墙控制访问

12. 总结与原则

核心认知

踩这些坑的根本原因,是把Redis当成了它不是的东西。

  • 它不是数据库:重要数据别只存Redis,要有持久化备份
  • 它不是消息队列:虽然有Stream/List,但专业的事交给Kafka/RabbitMQ
  • 它就是个缓存:用来解决"快"的问题,核心价值是高性能

设计原则

  1. 简单性原则:能不用Redis就不用,能用简单方案就不用复杂方案
  2. 容错性原则:Redis挂了系统还能正常工作(降级策略)
  3. 监控先行:上线前就要有完善的监控和告警
  4. 渐进演进:从小规模开始,逐步优化和扩展

最佳实践清单

  • ✅ 单Key不超过10KB
  • ✅ 热Key做打散或本地缓存
  • ✅ 重要操作用Lua脚本保证原子性
  • ✅ 合理设置过期时间和淘汰策略
  • ✅ 开启持久化并监控fork时间
  • ✅ 配置完善的监控告警
  • ✅ 定期进行容量规划和性能测试

记住:想清楚你要Redis干什么,坑就少一半。