From 0facafc745c056611cb4ecec7f87d3d79ff6e5a0 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 26 Jun 2026 09:51:42 +0800 Subject: [PATCH] =?UTF-8?q?fix(cache):=20=E5=87=BB=E7=A9=BF=E7=AD=89?= =?UTF-8?q?=E5=BE=85=E5=93=8D=E5=BA=94=E4=B8=AD=E6=96=AD=20+=20interval=20?= =?UTF-8?q?=E5=85=9C=E5=BA=95,=E6=B6=88=E9=99=A4=E7=A9=BA=E8=BD=AC?= =?UTF-8?q?=E4=B8=8E=E6=97=A0=E9=99=90=E5=BE=AA=E7=8E=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 未抢到锁的自旋等待原以 waited += waitIntervalMs 推进:waitIntervalMs<=0 时永不 前进 → 无限循环 hang;且 sleep 吞中断后不退出,被中断时以 Redis 往返为节奏空转 到超时、不响应协作式取消。改为溢出安全的 deadline 截止判断 + Math.max(1,interval) 兜底,sleep 返回中断信号、被中断即 break 降级并保留中断标志。 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../template/RedisCacheEnhanceTemplate.java | 19 +++++++----- .../RedisCacheEnhanceTemplateTest.java | 30 +++++++++++++++++++ 2 files changed, 42 insertions(+), 7 deletions(-) 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 213c4b3..1857aaf 100644 --- a/src/main/java/com/njcn/middle/cache/template/RedisCacheEnhanceTemplate.java +++ b/src/main/java/com/njcn/middle/cache/template/RedisCacheEnhanceTemplate.java @@ -185,11 +185,13 @@ public class RedisCacheEnhanceTemplate { } } - // 未抢到锁:自旋等待重读,超时降级直接回源 - long waited = 0; - while (waited < bd.getWaitTimeoutMs()) { - sleep(bd.getWaitIntervalMs()); - waited += bd.getWaitIntervalMs(); + // 未抢到锁:自旋等待重读,超时或被中断则降级直接回源 + long intervalMs = Math.max(1, bd.getWaitIntervalMs()); // 兜底:interval<=0 时不前进会空转 + long deadlineNanos = System.nanoTime() + bd.getWaitTimeoutMs() * 1_000_000L; + while (System.nanoTime() - deadlineNanos < 0) { // 溢出安全的截止判断 + if (!sleep(intervalMs)) { + break; // 被中断:停止等待、降级回源(中断标志已保留,响应协作式取消) + } Lookup r3 = tryGet(dataKey, decoder); if (r3.hit) { if (r3.realValue) { @@ -198,7 +200,7 @@ public class RedisCacheEnhanceTemplate { return r3.value; } } - log.warn("[redis-cache] breakdown wait timeout, fallback to loader, key={}", key); + log.warn("[redis-cache] breakdown wait timeout/interrupted, fallback to loader, key={}", key); return loader.get(); } @@ -240,11 +242,14 @@ public class RedisCacheEnhanceTemplate { } } - private static void sleep(long ms) { + /** 休眠;返回 false 表示被中断(已重置中断标志,调用方应停止等待)。 */ + private static boolean sleep(long ms) { try { Thread.sleep(ms); + return true; } catch (InterruptedException e) { Thread.currentThread().interrupt(); + return false; } } 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 e5c7296..7e451b2 100644 --- a/src/test/java/com/njcn/middle/cache/template/RedisCacheEnhanceTemplateTest.java +++ b/src/test/java/com/njcn/middle/cache/template/RedisCacheEnhanceTemplateTest.java @@ -24,12 +24,14 @@ import java.util.function.Supplier; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atMost; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -264,4 +266,32 @@ class RedisCacheEnhanceTemplateTest { assertThrows(IllegalArgumentException.class, () -> template.getOrLoad("user:1", User.class, 0, () -> new User(1L, "a"))); } + + // ---- #3+#4: 击穿等待的中断响应与 interval 兜底 ---- + + @Test + void getOrLoadWaitIntervalZeroDoesNotSpinForever() { + props.getBreakdown().setWaitIntervalMs(0); // 误配 0:旧实现 waited 永不前进 → 无限循环 + props.getBreakdown().setWaitTimeoutMs(50); + when(valueOps.get("cache:user:1")).thenReturn(null); + when(valueOps.setIfAbsent(anyString(), anyString(), any(Duration.class))).thenReturn(false); + User got = assertTimeoutPreemptively(Duration.ofSeconds(2), () -> + template.getOrLoad("user:1", User.class, 60, () -> new User(7L, "fb"))); + assertEquals("fb", got.getName()); + } + + @Test + void getOrLoadInterruptedDuringWaitStopsSpinning() { + props.getBreakdown().setWaitIntervalMs(20); + props.getBreakdown().setWaitTimeoutMs(10000); // 大超时:旧实现中断后空转数百次到超时 + when(valueOps.get("cache:user:1")).thenReturn(null); + when(valueOps.setIfAbsent(anyString(), anyString(), any(Duration.class))).thenReturn(false); + Thread.currentThread().interrupt(); // 当前线程预置中断,getOrLoad 同步执行可感知 + User got = template.getOrLoad("user:1", User.class, 60, () -> new User(7L, "fb")); + boolean wasInterrupted = Thread.interrupted(); // 读取并清除,避免污染后续测试 + assertEquals("fb", got.getName()); + assertTrue(wasInterrupted, "中断标志应被保留(协作式取消)"); + // 中断后立即停止等待:仅首次 tryGet 读一次,而非空转数百次 + verify(valueOps, atMost(2)).get("cache:user:1"); + } }