From 0657126f60e538ed3f466d245f88fabae5fa4bee Mon Sep 17 00:00:00 2001 From: root Date: Fri, 26 Jun 2026 10:11:48 +0800 Subject: [PATCH] =?UTF-8?q?test(cache):=20=E8=A1=A5=E5=85=B3=E9=94=AE?= =?UTF-8?q?=E8=B7=AF=E5=BE=84=E8=A6=86=E7=9B=96(loader=20=E5=BC=82?= =?UTF-8?q?=E5=B8=B8/double-check/=E6=B3=9B=E5=9E=8B=E9=87=8D=E8=BD=BD/sli?= =?UTF-8?q?ding=20=E5=9B=9E=E6=BA=90/=E9=99=8D=E7=BA=A7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 补齐 review 指出的覆盖缺口,均为既有正确行为的回归保护:loader 抛异常时锁 finally 释放 + 异常透传 + 不写缓存、抢锁后 double-check 命中跳过回源、getOrLoad 的 TypeReference 泛型重载往返、sliding 开启时回源新写入不重复续期、等待超时降级不写缓存。 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../RedisCacheEnhanceTemplateTest.java | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) 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 88215b4..7887207 100644 --- a/src/test/java/com/njcn/middle/cache/template/RedisCacheEnhanceTemplateTest.java +++ b/src/test/java/com/njcn/middle/cache/template/RedisCacheEnhanceTemplateTest.java @@ -1,6 +1,7 @@ package com.njcn.middle.cache.template; import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.TypeReference; import com.njcn.middle.cache.autoconfig.RedisCacheProperties; import com.njcn.middle.cache.constant.CacheConstant; import com.njcn.middle.cache.support.CacheKeyBuilder; @@ -17,6 +18,8 @@ import org.springframework.data.redis.core.ValueOperations; import org.springframework.data.redis.core.script.RedisScript; import java.time.Duration; +import java.util.Arrays; +import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; @@ -25,6 +28,7 @@ import java.util.function.Supplier; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -349,4 +353,75 @@ class RedisCacheEnhanceTemplateTest { verify(valueOps).set("cache:user:1", CacheConstant.NULL_SENTINEL, props.getNullTtlSeconds(), TimeUnit.SECONDS); } + + // ---- #11: 补关键路径覆盖(loader 异常 / double-check / TypeReference / sliding 回源 / 降级不写缓存) ---- + + @Test + void getOrLoadLoaderThrowsReleasesLockAndPropagates() { + when(valueOps.get("cache:user:1")).thenReturn(null); + when(valueOps.setIfAbsent(eq("cache:user:1:__lock__"), anyString(), any(Duration.class))) + .thenReturn(true); + RuntimeException boom = new RuntimeException("boom"); + RuntimeException thrown = assertThrows(RuntimeException.class, + () -> template.getOrLoad("user:1", User.class, 60, () -> { + throw boom; + })); + assertSame(boom, thrown); // 异常原样透传 + verify(redis).execute(any(RedisScript.class), anyList(), any()); // 锁在 finally 释放 + verify(valueOps, never()).set(anyString(), anyString(), anyLong(), any()); // 未写缓存 + } + + @Test + void getOrLoadDoubleCheckHitSkipsLoader() { + props.setJitterRatio(0); + when(valueOps.get("cache:user:1")) + .thenReturn(null) // 首次 tryGet miss + .thenReturn(JSON.toJSONString(new User(1L, "a"))); // 抢锁后 double-check 命中 + when(valueOps.setIfAbsent(eq("cache:user:1:__lock__"), anyString(), any(Duration.class))) + .thenReturn(true); + AtomicInteger calls = new AtomicInteger(); + User got = template.getOrLoad("user:1", User.class, 60, () -> { + calls.incrementAndGet(); + return new User(9L, "x"); + }); + assertEquals("a", got.getName()); + assertEquals(0, calls.get()); // double-check 命中,未回源 + verify(redis).execute(any(RedisScript.class), anyList(), any()); // 锁仍释放 + } + + @Test + void getOrLoadTypeReferenceRoundTrip() { + props.setJitterRatio(0); + when(valueOps.get("cache:list:1")).thenReturn(null); + when(valueOps.setIfAbsent(eq("cache:list:1:__lock__"), anyString(), any(Duration.class))) + .thenReturn(true); + List loaded = Arrays.asList(1, 2, 3); + List got = template.getOrLoad("list:1", + new TypeReference>() { + }, 60, () -> loaded); + assertEquals(loaded, got); + verify(valueOps).set("cache:list:1", JSON.toJSONString(loaded), 60L, TimeUnit.SECONDS); + } + + @Test + void getOrLoadSlidingDoesNotRenewOnFreshLoad() { + props.setSlidingExpireEnabled(true); + props.setJitterRatio(0); + when(valueOps.get("cache:user:1")).thenReturn(null); + when(valueOps.setIfAbsent(eq("cache:user:1:__lock__"), anyString(), any(Duration.class))) + .thenReturn(true); + template.getOrLoad("user:1", User.class, 60, () -> new User(1L, "a")); + // 回源新写入已自带 TTL,不应再触发滑动续期 expire + verify(redis, never()).expire(eq("cache:user:1"), anyLong(), any()); + } + + @Test + void getOrLoadWaitTimeoutFallbackDoesNotWriteCache() { + props.getBreakdown().setWaitIntervalMs(1); + props.getBreakdown().setWaitTimeoutMs(5); + when(valueOps.get("cache:user:1")).thenReturn(null); + when(valueOps.setIfAbsent(anyString(), anyString(), any(Duration.class))).thenReturn(false); + template.getOrLoad("user:1", User.class, 60, () -> new User(7L, "fb")); + verify(valueOps, never()).set(anyString(), anyString(), anyLong(), any()); // 降级不写缓存 + } }