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()); // 降级不写缓存 + } }