From ba2606ea46471e0ffd951185feaf16fbdd2a536b Mon Sep 17 00:00:00 2001 From: root Date: Fri, 26 Jun 2026 09:46:55 +0800 Subject: [PATCH] =?UTF-8?q?fix(cache):=20set/expire/getOrLoad=20=E5=AF=B9?= =?UTF-8?q?=20ttl<=3D0=20fail-fast,=E6=9D=9C=E7=BB=9D=20expire=20=E8=AF=AF?= =?UTF-8?q?=E5=88=A0=20key?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit expire(key, ttl<=0) 经 applyJitter 透传 0/负值,Redis EXPIRE 非正值会立即删 key 并返回 1,使"续期"静默删数据且报 true;set(ttl<=0) 则下发非法过期抛 RedisSystemException。统一在 set/expire/doGetOrLoad 入口 requirePositiveTtl 校验, 非正 TTL 直接抛 IllegalArgumentException。 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../template/RedisCacheEnhanceTemplate.java | 10 ++++++++ .../RedisCacheEnhanceTemplateTest.java | 24 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/src/main/java/com/njcn/middle/cache/template/RedisCacheEnhanceTemplate.java b/src/main/java/com/njcn/middle/cache/template/RedisCacheEnhanceTemplate.java index b1a8688..213c4b3 100644 --- a/src/main/java/com/njcn/middle/cache/template/RedisCacheEnhanceTemplate.java +++ b/src/main/java/com/njcn/middle/cache/template/RedisCacheEnhanceTemplate.java @@ -51,6 +51,7 @@ public class RedisCacheEnhanceTemplate { } public void set(String key, Object value, long ttlSeconds) { + requirePositiveTtl(ttlSeconds); redis.opsForValue().set(keyBuilder.build(key), codec.encode(value), applyJitter(ttlSeconds), TimeUnit.SECONDS); } @@ -94,6 +95,7 @@ public class RedisCacheEnhanceTemplate { // ---- 续期 ---- public boolean expire(String key, long ttlSeconds) { + requirePositiveTtl(ttlSeconds); return Boolean.TRUE.equals( redis.expire(keyBuilder.build(key), applyJitter(ttlSeconds), TimeUnit.SECONDS)); } @@ -125,6 +127,13 @@ public class RedisCacheEnhanceTemplate { return ttlSeconds + ThreadLocalRandom.current().nextLong(bound + 1); } + /** TTL 必须为正:0/负会被 Redis 当作"立即删除"(EXPIRE)或非法过期时间(SET),显式 fail-fast。 */ + private static void requirePositiveTtl(long ttlSeconds) { + if (ttlSeconds <= 0) { + throw new IllegalArgumentException("ttlSeconds must be positive, but was " + ttlSeconds); + } + } + // ---- 读取或回源(三防核心)---- public T getOrLoad(String key, Class type, Supplier loader) { @@ -140,6 +149,7 @@ public class RedisCacheEnhanceTemplate { } private T doGetOrLoad(String key, Function decoder, long ttlSeconds, Supplier loader) { + requirePositiveTtl(ttlSeconds); String dataKey = keyBuilder.build(key); Lookup r = tryGet(dataKey, decoder); diff --git a/src/test/java/com/njcn/middle/cache/template/RedisCacheEnhanceTemplateTest.java b/src/test/java/com/njcn/middle/cache/template/RedisCacheEnhanceTemplateTest.java index c1b0bba..e5c7296 100644 --- a/src/test/java/com/njcn/middle/cache/template/RedisCacheEnhanceTemplateTest.java +++ b/src/test/java/com/njcn/middle/cache/template/RedisCacheEnhanceTemplateTest.java @@ -240,4 +240,28 @@ class RedisCacheEnhanceTemplateTest { User got = template.getOrLoad("user:1", User.class, 60, () -> new User(1L, "fresh")); assertEquals("fresh", got.getName()); } + + // ---- #1+#2: ttl<=0 入口校验(fail-fast,不下发非法过期) ---- + + @Test + void setRejectsNonPositiveTtl() { + User u = new User(1L, "a"); + assertThrows(IllegalArgumentException.class, () -> template.set("user:1", u, 0)); + assertThrows(IllegalArgumentException.class, () -> template.set("user:1", u, -1)); + verify(valueOps, never()).set(anyString(), anyString(), anyLong(), any()); + } + + @Test + void expireRejectsNonPositiveTtl() { + assertThrows(IllegalArgumentException.class, () -> template.expire("user:1", 0)); + assertThrows(IllegalArgumentException.class, () -> template.expire("user:1", -5)); + verify(redis, never()).expire(anyString(), anyLong(), any()); + } + + @Test + void getOrLoadRejectsNonPositiveTtl() { + props.getBreakdown().setEnabled(false); // 避免跑红时走入等待自旋 + assertThrows(IllegalArgumentException.class, + () -> template.getOrLoad("user:1", User.class, 0, () -> new User(1L, "a"))); + } }