package com.njcn.middle.cache.integration; import com.njcn.middle.cache.template.RedisCacheEnhanceTemplate; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import static org.junit.jupiter.api.Assertions.assertEquals; /** * 集成测试:N 个线程并发 getOrLoad 同一未命中热点 key,慢回源放大竞争窗口, * 互斥锁应保证 loader 仅被调用 1 次,且所有线程拿到同一结果。 */ @ExtendWith(RedisAvailableCondition.class) @SpringBootTest(classes = CacheBreakdownConcurrencyTest.TestApp.class, properties = {"redis-cache.key-prefix=it-cache:"}) class CacheBreakdownConcurrencyTest { @Autowired private RedisCacheEnhanceTemplate cache; @Test void onlyOneLoaderUnderConcurrency() throws Exception { String key = "bd:hot:1"; cache.delete(key); AtomicInteger calls = new AtomicInteger(); java.util.function.Supplier loader = () -> { calls.incrementAndGet(); try { Thread.sleep(300); // 慢回源 } catch (InterruptedException ignored) { Thread.currentThread().interrupt(); } return new User(1L, "hot"); }; int n = 10; ExecutorService pool = Executors.newFixedThreadPool(n); CountDownLatch ready = new CountDownLatch(n); CountDownLatch go = new CountDownLatch(1); List> futures = new ArrayList<>(); for (int i = 0; i < n; i++) { futures.add(pool.submit(() -> { ready.countDown(); go.await(); return cache.getOrLoad(key, User.class, 60, loader); })); } ready.await(); go.countDown(); for (Future f : futures) { assertEquals("hot", f.get(10, TimeUnit.SECONDS).getName()); } assertEquals(1, calls.get(), "并发下 loader 应只回源一次"); pool.shutdownNow(); cache.delete(key); } @SpringBootConfiguration @EnableAutoConfiguration static class TestApp { } @Data @AllArgsConstructor @NoArgsConstructor static class User { private Long id; private String name; } }