前言
Redis 在 Java 项目中使用非常广泛,常用来做热点缓存、分布式锁、限流、计数器、简单消息队列等。很多开发只会调用 set/get,但在高并发场景下很容易出现超时、数据不一致、宕机、锁失效等问题。本文站在 Java 架构师角度,梳理生产最常见问题、现象、根因以及落地解决方案,适合直接用于项目复盘、技术分享。
说明:文中客户端以 Spring Data Redis、Redisson 为主,适配 SpringBoot 微服务场景。
一、缓存三大经典问题:穿透、击穿、雪崩
1. 缓存穿透
现象 :查询一个数据库不存在的数据,缓存中也没有,请求直接打到数据库。大量恶意请求或者非法参数,会把数据库压垮。 根因 :缓存没有兜底,不存在的 key 不会写入缓存,每次都查询 DB。 解决方案
- 空值缓存:查询结果为空时,缓存一个空标识,设置较短过期时间(30s~5min),避免大量重复穿透;
- 布隆过滤器:提前把合法主键放入布隆过滤器,请求先经过过滤器过滤不存在的 ID;适合数据量大、新增不频繁场景;
- 参数校验:接口层提前校验参数合法性,非法参数直接拒绝。
Java 注意:空值不能设置永久过期,防止大量无效 key 占用内存;布隆过滤器存在误判,不能用于删除操作。
2. 缓存击穿
现象 :某个热点 key 过期的一瞬间,大量并发请求同时涌入数据库,瞬间打满 DB。 根因 :热点 key + 过期时间,并发请求同时 miss 缓存。 解决方案
- 互斥锁:使用 Redisson 分布式锁,只放行一个请求去查库更新缓存,其余请求等待;缺点是会有等待,容易产生超时;
- 逻辑过期:key 不删除,value 内置过期时间,请求直接返回旧数据,后台异步线程更新缓存;适合允许短暂不一致业务;
- 热点数据永不过期:后台定时任务主动刷新缓存,取消 TTL。
3. 缓存雪崩
现象 :大量 key 在同一时间过期,或者 Redis 集群宕机,流量全部涌向数据库,数据库压力雪崩。 根因
- 批量 key 设置相同过期时间,同一时刻失效;
- Redis 实例故障、主从切换、集群分片不可用。 解决方案
- TTL 增加随机偏移量,打散过期时间;
- 多级缓存:JVM 本地缓存(Caffeine)+ Redis;
- 熔断、限流、降级:Sentinel / Resilience4j 保护数据库;
- 搭建高可用架构:哨兵模式 / Redis Cluster。
一句话区分:穿透查不存在数据;击穿是单个热点过期;雪崩是大批量 key 失效或者 Redis 挂了。
二、缓存与数据库一致性问题
现象 :更新数据库之后,缓存数据和数据库不一致,用户读到脏数据。 根因:并发读写场景下,缓存更新时序错乱。 两种常用策略对比:
- 先更新数据库,再删除缓存(推荐)
- 先删除缓存,再更新数据库(风险更高)
问题:即使是先更新DB再删缓存,依然存在极小概率:读请求在DB更新完成、缓存删除之前读到旧数据,写回缓存,造成永久脏数据。
解决方案
- 延时双删:更新DB → 删除缓存 → 休眠几百ms再次删除缓存;降低并发冲突概率,无法保证强一致;
- Canal 监听 binlog:数据库变更后,通过 binlog 异步删除缓存,工业项目主流方案;
- 强一致性业务:放弃缓存,直接查询数据库。
重要结论:Redis 缓存只能做到最终一致性,不适合强一致业务。
三、分布式锁相关问题(Redisson 重点)
1. 业务执行超时,锁提前过期,锁失效
现象 :业务代码执行时间超过锁过期时间,锁自动释放,其他线程抢占锁,出现并发安全问题。 根因 :手写 SETNX 锁没有自动续期。 解决方案:使用 Redisson 可重入锁,内置看门狗(WatchDog),默认30s,每10s自动续期。不要自己手写 SETNX 实现分布式锁。
2. A线程的锁被B线程释放
现象 :线程A拿到锁,业务卡顿超时,锁自动释放;线程B抢到锁,线程A执行完直接 del key,把B的锁删掉。 根因 :直接 del key 删除锁,没有校验持有者。 解决方案:使用 Lua 脚本原子判断锁归属 + 删除;Redisson 原生已经封装该逻辑,不要手动 del。
3. 主从切换导致锁丢失
现象 :主节点拿到锁,数据还没同步到从节点,主节点宕机,从节点晋升为主节点,锁直接消失。 根因 :Redis 主从同步是异步复制。 解决方案
- 普通业务:接受极小概率风险;
- 高可靠场景:使用 RedLock(运维成本高,不推荐大规模使用),或者换 Zookeeper/etcd。
4. 死锁
现象 :服务拿到锁之后宕机,锁永远无法释放。 解决方案:锁必须设置过期时间兜底。
四、大Key、热Key问题
大Key
现象 :单个 key 的 value 几十MB,或者集合元素几十万条;出现命令超时、网络抖动、主从同步慢、集群迁移卡顿。删除大key时阻塞主线程。 根因 :把大JSON、全量列表直接存入 Redis。 解决方案
- 数据拆分,分桶存储;
- Redis4.0+ 使用
unlink异步删除; - 集合类使用
hscan/sscan分批遍历、分批删除; - 上线前做大key扫描,定时巡检。
热Key
现象 :某个key访问量极高,压垮单个Redis分片,CPU打满。 解决方案
- 本地缓存兜底;
- 热key分片:相同数据多副本,key加后缀分散到不同slot;
- 网关层限流。
五、Redis 单线程阻塞问题(非常容易忽略)
Redis 命令执行是单线程模型 ,慢命令会阻塞整个实例,所有请求超时。 高危阻塞命令 :KEYS *、HGETALL、SMEMBERS、LRANGE 0 -1、大key DEL。 现象 :Java客户端大量超时、连接堆积、RT突增。 解决方案
- 线上禁止
KEYS,用SCAN游标迭代; - Hash、Set、List 禁止一次性全量读取,按需分批;
- 开启慢查询日志
slowlog,监控慢命令; - 禁止在主线程执行耗时操作。
六、内存相关问题
1. 内存满,写入报错
根因 :没有设置 maxmemory 和淘汰策略,key不设置TTL,无效数据堆积。 淘汰策略说明:
allkeys-lru:常用,优先淘汰最近最少访问key;volatile-lru:只淘汰带过期时间的key;noeviction:内存满直接拒绝写入,业务报错,生产禁止。
2. 内存碎片过高
现象 :Redis 实际数据很小,但占用操作系统内存很高。频繁增删不同大小value产生碎片。 解决方案 :Redis4.4+ 开启自动碎片整理,手动执行 MEMORY PURGE。
七、持久化 RDB / AOF 问题
- RDB :定时全量快照
- 问题:宕机丢失两次快照之间数据;fork 子进程时消耗内存,大实例卡顿;
- AOF :追加写日志
- 问题:AOF文件持续膨胀,
bgrewriteaof重写消耗CPU、磁盘IO; - 刷盘策略:
everysec(默认,生产推荐,最多丢1秒数据);always性能差;no风险高。
- 问题:AOF文件持续膨胀,
推荐方案:开启混合持久化(RDB+AOF),兼顾恢复速度和数据安全。
八、客户端连接与网络问题(Java客户端)
- 连接池配置不合理:连接数太少,并发阻塞;连接太多,Redis服务端连接数打满; ✅ 方案:SpringDataRedis / Lettuce 合理配置连接池,监控连接泄露;优先使用Lettuce(netty异步,线程安全),Jedis注意线程不安全;
- 超时时间设置不合理:命令超时、读取超时;
- 网络分区:微服务和Redis之间网络抖动,大量超时。 ✅ 方案:增加重试机制(注意幂等)、熔断降级。
坑:Jedis 是非线程安全,不要单例Jedis直接在多线程使用,必须用连接池。
九、Redis Cluster 集群常见问题
- 槽位没有分配完整,集群不可用;
- 迁移大key时阻塞;
- 跨槽批量操作(MGET、MSET)不支持; ✅ 方案:批量操作尽量保证key在同一个slot,使用hash tag;
- 主从异步复制,数据丢失风险;
- 节点上下线、重新分片带来运维复杂度。
十、简单的生产Redis运维清单
✅ 禁止 KEYS、HGETALL 等阻塞命令
✅ 定期巡检大key、热key、慢查询
✅ 合理设置maxmemory + 淘汰策略
✅ 分布式锁优先 Redisson,不要手写SETNX
✅ 热点数据增加本地缓存 + 熔断降级
✅ 高可用使用哨兵或者Cluster,不要单机Redis上生产
✅ 开启混合持久化,AOF刷盘 everysec
✅ 监控指标:CPU、内存、连接数、慢查询、命令超时、主从延迟
结尾总结
Redis 性能强,但它的坑大多来自对模型理解不足 :单线程、异步复制、缓存最终一致、过期淘汰机制。很多问题不是Redis本身bug,而是业务误用。Java开发使用Redis,优先守住三条底线:不阻塞主线程、保护数据库、高并发下做好兜底降级。