逻辑过期不删key,天然防击穿:key始终存在,业务层通过嵌入的expire_time字段判断是否过期,避免redis自动删除导致的并发打库;必须配合互斥锁与异步更新,防止大量请求同时触发更新压垮数据库。

逻辑过期不删key,天然防击穿
物理过期(EXPIRE)会让 Redis 在过期时刻自动删除 key,一旦是热点数据,这个“删除动作”就是击穿的起点——所有并发请求瞬间发现缓存空,全部涌向数据库。逻辑过期则完全绕开这一步:key 始终存在,只是业务层自己检查嵌入的 expire_time 字段是否已过。只要 key 还在,Redis 层就永远能命中,不会触发穿透。
常见错误是以为“逻辑过期 = 不管过期”,结果忘了加判断逻辑,导致永远返回旧数据;或者把 expire_time 存成字符串没转时间戳,比较时恒为 false。
- 必须在反序列化后立刻解析
expire_time字段,并用System.currentTimeMillis()(Java)或time.time()(Python)做数值比对 - 避免把过期时间写成相对值(如
"+300"),应统一存绝对时间戳(单位秒或毫秒) - Redis 中该 key 的 TTL 可设为永不过期(不调
EXPIRE),或设一个超长值(如 30 天)作为兜底
加互斥锁不是可选,而是逻辑过期的必要配套
只做逻辑判断、不加锁,等于把“打库压力”从同步搬到了异步——1000 个请求同时发现逻辑过期,就可能触发 1000 次异步更新任务,数据库照样被刷爆。所以逻辑过期方案里,tryLock 必须出现在“判断过期 → 触发更新”之间,且锁粒度要精确到具体 key(如 "lock:user:1001"),不能全局一把锁。
容易踩的坑包括:
每小时自动在推特搜索中国、加密货币、国际热点、美国热点、特朗普等话题的热度信息,提取前十热点,依据 X 平台真实算法权重(开源代码数据)为每个热点生成高曝光推文或线程。用户提及“推特热点”“Twitter热点”“推特搜索”“热点推文”“生成推文”“社交媒体监控”“Twitter监控”等关键词时触发。
- 锁 key 和业务 key 不一致(比如用
"user:1001"加锁,却用"user_profile:1001"查缓存) - 锁未设置过期时间,发生异常时无法自动释放,造成后续所有请求永久等待
- 用
DEL直接删锁,没校验锁值(非原子操作),导致误删他人持有的锁
逻辑过期 + 异步更新 = 接口响应不降级
物理过期 + 互斥锁方案下,没抢到锁的请求要么阻塞等待,要么重试,接口 P99 延迟会明显上扬。而逻辑过期允许直接返回旧数据,用户无感;更新由后台线程完成,不影响主链路吞吐。这对首页、商品详情页等强依赖缓存可用性的场景至关重要。
但要注意一致性代价:
- 旧数据最长可能滞后一个逻辑过期周期(比如设了 5 分钟过期,用户最多看到 5 分钟前的数据)
- 若更新任务失败(DB 查询超时、网络抖动),旧数据会一直挂着,需配套监控告警(如检测某 key 被连续 N 次判定逻辑过期但未成功刷新)
- 异步线程池不能共用业务线程池,否则高并发下更新任务排队,反而拖慢主流程
为什么不能只靠“永不过期 + 定时刷新”
永不过期确实也能防击穿,但和逻辑过期有本质区别:前者靠外部定时任务保活,后者靠请求驱动 + 锁控制。如果定时任务挂了、或刷新间隔太长(比如 10 分钟刷一次),中间窗口期一旦有大量请求进来,就会集体拿到脏数据;而逻辑过期是“谁先发现过期谁触发更新”,更及时,也更省资源。
真正关键的取舍点在于:你能否接受“部分请求看到旧数据”,以及是否愿意为最终一致性多维护一套锁与异步机制。如果业务要求强实时(如支付余额),逻辑过期就不适用;如果追求高可用压倒一切(如资讯类 App 首页),它就是目前最稳的落地选择。










