RedisCacheManager.setExpires()仅在缓存首次写入时设置TTL,后续读取不会刷新过期时间,因Spring Cache原生不支持自动续期;需通过Lua脚本或自定义RedisCache.get()手动调用EXPIRE实现访问时续期。

为什么 RedisCacheManager 的 setExpires 不支持自动续期
RedisCacheManager.setExpires() 只在缓存首次写入时生效,它把过期时间“固化”进每个 RedisCache 实例的配置里。后续哪怕你用 @Cacheable 再次命中,也不会刷新 TTL —— Redis 里的 key 还是按最初设的那个时间倒计时,到期就删。所谓“自动续期”,Spring Cache 原生根本不提供这个能力。
常见错误现象:@Cacheable 加了 sync = true 或 unless,误以为能触发刷新;或者改了 entryTtl 配置却没重启应用,结果缓存还是不更新。
- 底层调用的是
SET或SETEX,不是GETEX或带PTTL+EXPIRE的组合 - 所有基于
RedisCache的读操作(get()、putIfAbsent())都不会主动调expire() - 如果你依赖
cacheManager.getCache("xxx")手动拿 cache 再调get(),也一样不会续期
手动实现“访问即续期”的两种可靠路径
想让缓存被读取时自动延长生命周期,必须绕开 RedisCache 的封装,直接和 Redis 交互。核心逻辑就两条:读 key → 判断存在 → 续期 → 返回值。不能依赖 RedisTemplate.opsForValue().getAndSet(),它会覆盖原值且不保证原子性。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 推荐用
redisTemplate.execute()+ Lua 脚本:一次性完成GET+EXPIRE,避免竞态。例如:
String script = "local val = redis.call('GET', KEYS[1])\nif val then\n redis.call('EXPIRE', KEYS[1], ARGV[1])\nend\nreturn val";
Object result = redisTemplate.execute(new DefaultRedisScript<>(script, String.class),
Collections.singletonList(key), String.valueOf(seconds));
- 如果不用 Lua,至少分两步做,并检查
expire()返回值:redisTemplate.opsForValue().get(key)后,再调redisTemplate.expire(key, seconds, TimeUnit.SECONDS);注意返回false表示 key 已不存在,得重新加载 - 别用
StringRedisTemplate.opsForValue().set(key, value, duration, TimeUnit.SECONDS)—— 它等价于SETEX,会强制覆盖,丢失原有数据结构(比如 hash 或 list)
自定义 RedisCacheManager 时如何注入续期逻辑
如果你坚持用 @Cacheable 注解,又想带续期语义,就得重写 RedisCache。关键不是改 RedisCacheManager,而是替换它的 createRedisCache() 方法返回的 cache 实例。
- 继承
RedisCache,重写get(Object key):先调父类super.get(key),拿到Cache.ValueWrapper后,再执行redisTemplate.expire(...) - 在自定义
RedisCacheManager中,覆盖getMissingCache(String name),返回你自己的RenewableRedisCache实例 - 注意:每次
get()都续期,可能造成热点 key 永远不淘汰。建议加条件,比如只在剩余 TTL 小于 30 秒时才续:Long ttl = redisTemplate.getExpire(key),再判断 - 序列化必须一致:你的自定义 cache 和原
RedisTemplate共享同一个valueSerializer,否则get()出来是null或乱码
容易被忽略的边界点:空值、穿透、连接泄漏
续期逻辑一加,原来 Spring Cache 自动兜底的几件事就失效了。你得自己补全,否则线上容易出问题。
- 缓存穿透:如果 DB 查不到,你往 Redis 写了
null并设了 2 分钟 TTL,那这 2 分钟内所有请求都会击穿。必须显式处理:if (result == null) { redisTemplate.opsForValue().set(key, "", Duration.ofMinutes(2)); return null; } - 连接泄漏:Lua 脚本执行失败或超时,
redisTemplate.execute()默认不自动 close 连接。确认你用的是 Lettuce(Spring Boot 2.3+ 默认),它基于 Netty,连接复用,但异常路径仍要 try-catch +reset() - 多实例部署:续期只作用于当前节点看到的 key。如果用 Redis Cluster,key 分片后,续期操作仍能路由到正确节点;但如果是主从架构,且你连的是从节点,
EXPIRE会报错 —— 必须确保RedisTemplate连的是主节点
真正难的不是写几行续期代码,而是想清楚:这个 key 到底该不该每次访问都续?续多久?DB 加载成本高不高?有没有更轻量的替代方案(比如本地 Caffeine 缓存 + Redis 做分布式通知)?

















