逻辑过期不能只靠expire,因为expire是物理过期,key到点自动删除,会触发缓存击穿;逻辑过期将expiretime嵌入value中,key永不过期,由业务判断“软失效”,从而支持异步重建与避免锁竞争。

逻辑过期为什么不能只靠 EXPIRE
Redis 原生的 EXPIRE 是物理过期,key 到点就消失。缓存击穿的本质是“热点 key 物理过期瞬间被并发请求围攻”,所以用 EXPIRE 反而会触发问题。逻辑过期把过期时间塞进 value 里(比如 JSON 中加 expireTime 字段),读取时由应用自己判断是否过期——key 永不删除,但数据可“软失效”。这给了你异步重建缓存的空间,也避免了锁竞争。
setWithLogicalExpire 的关键实现细节
这个方法不是简单地存 JSON,而是要构造带过期时间戳的包装对象。常见错误是直接序列化原始对象,导致反序列化时丢失 expireTime 字段。
- 必须定义一个统一的包装类,例如
RedisData,含data和expireTime两个字段 - 写入前手动设置
expireTime = System.currentTimeMillis() + timeUnit.toMillis(time) - 序列化用
JSONUtil.toJsonStr(new RedisData().setData(value).setExpireTime(expireTime)),别漏掉包装 - 不要用
StringRedisTemplate的泛型opsForValue().set(...)直接传对象,它不会自动帮你包一层
queryWithLogicalExpire 怎么避免重复重建
查逻辑过期缓存时,如果发现已过期,得启动异步线程去重建,但绝不能每个请求都起一个线程——那是线程爆炸的节奏。
- 先尝试用
setIfAbsent(lockKey, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS)获取分布式锁 - 拿到锁的线程才去查库、序列化、重写缓存;没拿到锁的线程直接返回旧值(容忍短时脏读)
- 锁 key 要带业务标识,比如
"lock:shop:" + id,防止不同商品互相阻塞 - 异步线程池必须是单例且有界,推荐
Executors.newFixedThreadPool(10),别用newCachedThreadPool
泛型反序列化时 TypeReference 不能省
从 Redis 读出 JSON 字符串后,要反序列化成 Page<productvo></productvo> 这种带泛型的类型,直接用 JSONUtil.toBean(json, clazz) 会失败——Java 泛型擦除后只剩 Object,JSON 库只能还原成 LinkedHashMap。
- 必须用
JSONUtil.toBean(json, new TypeReference<page>>() {})</page> - 工具类里不能声明
<t></t>泛型,而应在每次调用时传入TypeReference实例 - 如果你封装了
queryWithLogicalExpire,它的返回类型参数R无法在运行时保留泛型信息,所以方法签名里得额外加一个TypeReference<r></r>参数,或者强制调用方传Class<r></r>并配合内部构造TypeReference
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











