diff --git a/src/main/java/io/github/davidhlp/spring/cache/redis/cache/RedisProCache.java b/src/main/java/io/github/davidhlp/spring/cache/redis/cache/RedisProCache.java index 819a726e..15198892 100644 --- a/src/main/java/io/github/davidhlp/spring/cache/redis/cache/RedisProCache.java +++ b/src/main/java/io/github/davidhlp/spring/cache/redis/cache/RedisProCache.java @@ -37,9 +37,9 @@ public class RedisProCache extends RedisCache { /** - * 指标写侧 seam — 6 个 metric 的注册 + null-safe 记录 + 快照读取全部收口在本字段。 + * 指标写侧 seam — 7 个 metric 的注册 + null-safe 记录 + 快照读取全部收口在本字段。 * - *
{@code MeterRegistry} 缺失时本字段构造为空 registry(全部 6 字段为 null),record 方法全 no-op。 + *
{@code MeterRegistry} 缺失时本字段构造为空 registry(全部 7 字段为 null),record 方法全 no-op。 */ private final RedisProCacheMetricsRegistry metricsRegistry; @@ -69,7 +69,7 @@ public class RedisProCache extends RedisCache { * *
构造期委派 3 个 deep seam: *
与 {@link CacheMetrics} 的对称性:本 seam 是"写侧"(注册 + 记录),与读侧 * 快照({@link CacheMetrics})配对形成指标领域 完整边界。读侧只读、写侧只写 — 关注点分离。 * - *
与 {@link RedisProCacheTimers} 的关系:{@link RedisProCacheTimers} 是 metric 原语 - * helper({@code registerTimer} / {@code registerCounter} / {@code timed} / {@code timedGet} / - * {@code safeIncrement}),本身是工具类无状态。本类是 metric 集合的容器(6 字段 + 6 - * 业务语义方法),把原语按"缓存实例"的语义组装起来。两层 seam 形成 composition — 删除任意 - * 一层,复杂度上浮。 + *
本类在 metric ownership seam 内部封装注册、自增与计时原语,并按缓存实例语义组装。 + * record 方法复用这些私有 helper;recordPut/recordEvict 保留计时启用时将 operation counter + * 放在 finally 中的局部结构,以维持异常路径的计数语义。 * *
公开方法(业务语义): *
null-safe 语义:{@link MeterRegistry} 为 null 时(即未启用指标),全部 6 个内部 + *
null-safe 语义:{@link MeterRegistry} 为 null 时(即未启用指标),全部 7 个内部 * 字段为 null,所有 record 方法走 no-op 路径。 * *
线程安全:本类仅在 cache 构造期由单线程初始化;运行期 record 方法调 @@ -69,7 +67,7 @@ final class RedisProCacheMetricsRegistry { private final String cacheName; - // 写侧 6 字段:3 Timer + 4 Counter + // 写侧 7 字段:3 Timer + 4 Counter // 注:clear 路径无 Counter(batch 操作语义不适合计数),仅 Timer @Nullable private final Timer getTimer; @@ -87,22 +85,21 @@ final class RedisProCacheMetricsRegistry { private final Counter evictCounter; /** - * 构造期一次性注册 6 个 metric — 在 cache 构造期调用一次,运行期 record 路径直接复用。 + * 构造期一次性注册 7 个 metric — 在 cache 构造期调用一次,运行期 record 路径直接复用。 * - *
内部委派 {@link RedisProCacheTimers} 原语(registerTimer / registerCounter)保证 null-safe 语义。 - * - * @param meterRegistry Micrometer 注册表(可为 null → 全部 6 字段为 null) + *
内部注册 helper 保证 {@code meterRegistry == null} 时所有字段保持 null。
+ * @param meterRegistry Micrometer 注册表(可为 null → 全部 7 字段为 null)
* @param cacheName cache 标识,作为 {@code tags("cache", cacheName)} 写入每个 metric
*/
public RedisProCacheMetricsRegistry(@Nullable MeterRegistry meterRegistry, String cacheName) {
this.cacheName = cacheName;
- this.getTimer = RedisProCacheTimers.registerTimer(meterRegistry, TIMER_GET, DESC_GET_TIMER, cacheName);
- this.putTimer = RedisProCacheTimers.registerTimer(meterRegistry, TIMER_PUT, DESC_PUT_TIMER, cacheName);
- this.evictTimer = RedisProCacheTimers.registerTimer(meterRegistry, TIMER_EVICT, DESC_EVICT_TIMER, cacheName);
- this.hitCounter = RedisProCacheTimers.registerCounter(meterRegistry, COUNTER_HIT, DESC_HIT, cacheName);
- this.missCounter = RedisProCacheTimers.registerCounter(meterRegistry, COUNTER_MISS, DESC_MISS, cacheName);
- this.putCounter = RedisProCacheTimers.registerCounter(meterRegistry, COUNTER_PUT, DESC_PUT, cacheName);
- this.evictCounter = RedisProCacheTimers.registerCounter(meterRegistry, COUNTER_EVICT, DESC_EVICT, cacheName);
+ this.getTimer = registerTimer(meterRegistry, TIMER_GET, DESC_GET_TIMER, cacheName);
+ this.putTimer = registerTimer(meterRegistry, TIMER_PUT, DESC_PUT_TIMER, cacheName);
+ this.evictTimer = registerTimer(meterRegistry, TIMER_EVICT, DESC_EVICT_TIMER, cacheName);
+ this.hitCounter = registerCounter(meterRegistry, COUNTER_HIT, DESC_HIT, cacheName);
+ this.missCounter = registerCounter(meterRegistry, COUNTER_MISS, DESC_MISS, cacheName);
+ this.putCounter = registerCounter(meterRegistry, COUNTER_PUT, DESC_PUT, cacheName);
+ this.evictCounter = registerCounter(meterRegistry, COUNTER_EVICT, DESC_EVICT, cacheName);
}
// ==================== 业务方法(get / put / evict / clear) ====================
@@ -120,21 +117,21 @@ public RedisProCacheMetricsRegistry(@Nullable MeterRegistry meterRegistry, Strin
* @return body.get() 的结果
*/
public 把 null-safe timer/counter 行为封装在四个静态入口:
- * 行为保真:
- * 接口是测试面:本类四个方法是单一测试目标。新增 metric(hit-ratio / 复合 timer 等)
- * 只在 seam 内扩展,不污染调用点。
- *
- * deletion test:删本类 → timing/counter 样板在调用点重现 → 真 seam。
- *
- * @see RedisProCache
- */
-final class RedisProCacheTimers {
-
- private RedisProCacheTimers() {
- // 工具类,不可实例化
- }
-
- /**
- * 注册 Timer.{@code registry == null} 时返回 {@code null}(由 {@link #timed} /
- * {@link #timedGet} 静默 no-op 吸收)。
- *
- * @param registry Micrometer 注册中心,生产可为 null
- * @param name Timer 名,如 {@code "resicache.cache.get"}
- * @param description Timer 描述
- * @param cacheName cache tag 值,用于 {@code tags("cache", cacheName)}
- * @return 注册成功的 Timer;{@code registry == null} 时返回 null
- */
- static Timer registerTimer(MeterRegistry registry, String name,
- String description, String cacheName) {
- if (registry == null) {
- return null;
- }
- return Timer.builder(name)
- .tag("cache", cacheName)
- .description(description)
- .register(registry);
- }
-
- /**
- * 注册 Counter.{@code registry == null} 时返回 {@code null}(由
- * {@link #safeIncrement} 静默 no-op 吸收)。
- *
- * @param registry Micrometer 注册中心,生产可为 null
- * @param name Counter 名,如 {@code "resicache.cache.hit"}
- * @param description Counter 描述
- * @param cacheName cache tag 值,用于 {@code tags("cache", cacheName)}
- * @return 注册成功的 Counter;{@code registry == null} 时返回 null
- */
- static Counter registerCounter(MeterRegistry registry, String name,
- String description, String cacheName) {
- if (registry == null) {
- return null;
- }
- return Counter.builder(name)
- .tag("cache", cacheName)
- .description(description)
- .register(registry);
- }
-
- /**
- * Counter null-safe increment.{@code counter == null} 时静默 no-op(等价于
- * {@link RedisProCache#metrics()} 对 null Counter 返回 {@code 0L} 的零返回值路径)。
- *
- * @param counter 待自增 Counter,生产可为 null
- */
- static void safeIncrement(Counter counter) {
- if (counter != null) {
- counter.increment();
- }
- }
-
- /**
- * void body 用 timing wrapper —— 语义与 try-finally 等价:
- * 调用方如需把 body 异常翻译为 {@code Cache.ValueRetrievalException} 或自增 miss 计数,
- * 应当在本 {@code timedGet} 之外再套一层 try-catch —— 调用点的 catch 与本类的
- * {@code finally} 互不干扰。
- *
- * @param 本测试独立验证 registry seam 的 6 大契约:
* 本测试承担原 {@link RedisProCache} 6 处私有 {@code try-finally + safeRecord} 样板的测试覆盖:
- *
- *
- *
- *
- *
- *
- *
- *
- *
- * @param timer 待记录 Timer,生产可为 null
- * @param body 待执行的操作,可抛出 RuntimeException
- */
- static void timed(Timer timer, Runnable body) {
- if (timer == null) {
- body.run();
- return;
- }
- long start = System.nanoTime();
- try {
- body.run();
- } finally {
- timer.record(System.nanoTime() - start, TimeUnit.NANOSECONDS);
- }
- }
-
- /**
- * 返回值 body 用 timing wrapper —— 语义与 try-finally 等价:
- *
- *
- *
- *
*
- *
- *
- */
-@DisplayName("RedisProCacheTimers Tests")
-class RedisProCacheTimersTest {
-
- private MeterRegistry meterRegistry;
-
- @BeforeEach
- void setUp() {
- meterRegistry = new SimpleMeterRegistry();
- }
-
- @Nested
- @DisplayName("registerTimer Tests")
- class RegisterTimerTests {
-
- @Test
- @DisplayName("null registry returns null")
- void nullRegistry_returnsNull() {
- assertThat(RedisProCacheTimers.registerTimer(
- null, "test.timer", "desc", "cache1")).isNull();
- }
-
- @Test
- @DisplayName("non-null registry registers Timer with cache tag")
- void nonNullRegistry_registersTimerWithTag() {
- Timer timer = RedisProCacheTimers.registerTimer(
- meterRegistry, "resicache.cache.get", "Time spent getting", "userCache");
-
- assertThat(timer).isNotNull();
- assertThat(timer.getId().getName()).isEqualTo("resicache.cache.get");
- assertThat(timer.getId().getTag("cache")).isEqualTo("userCache");
- assertThat(timer.getId().getDescription()).isEqualTo("Time spent getting");
- }
- }
-
- @Nested
- @DisplayName("registerCounter Tests")
- class RegisterCounterTests {
-
- @Test
- @DisplayName("null registry returns null")
- void nullRegistry_returnsNull() {
- assertThat(RedisProCacheTimers.registerCounter(
- null, "test.counter", "desc", "cache1")).isNull();
- }
-
- @Test
- @DisplayName("non-null registry registers Counter with cache tag")
- void nonNullRegistry_registersCounterWithTag() {
- Counter counter = RedisProCacheTimers.registerCounter(
- meterRegistry, "resicache.cache.hit", "Cache hit count", "userCache");
-
- assertThat(counter).isNotNull();
- assertThat(counter.getId().getName()).isEqualTo("resicache.cache.hit");
- assertThat(counter.getId().getTag("cache")).isEqualTo("userCache");
- }
- }
-
- @Nested
- @DisplayName("safeIncrement Tests")
- class SafeIncrementTests {
-
- @Test
- @DisplayName("null counter is silent no-op")
- void nullCounter_silentNoOp() {
- // 必须不抛 NPE —— 这是 null-safe 行为的核心承诺
- RedisProCacheTimers.safeIncrement(null);
- }
-
- @Test
- @DisplayName("non-null counter increments")
- void nonNullCounter_increments() {
- Counter counter = meterRegistry.counter("test.counter");
- RedisProCacheTimers.safeIncrement(counter);
- RedisProCacheTimers.safeIncrement(counter);
-
- assertThat(counter.count()).isEqualTo(2.0);
- }
- }
-
- @Nested
- @DisplayName("timed Tests")
- class TimedTests {
-
- @Test
- @DisplayName("null timer executes body without recording")
- void nullTimer_executesBodyWithoutRecording() {
- AtomicInteger invocations = new AtomicInteger();
-
- RedisProCacheTimers.timed(null, () -> invocations.incrementAndGet());
-
- assertThat(invocations.get()).isEqualTo(1);
- }
-
- @Test
- @DisplayName("non-null timer records one sample")
- void nonNullTimer_recordsOneSample() {
- Timer timer = meterRegistry.timer("test.timer");
- AtomicInteger invocations = new AtomicInteger();
-
- RedisProCacheTimers.timed(timer, () -> invocations.incrementAndGet());
-
- assertThat(invocations.get()).isEqualTo(1);
- assertThat(timer.count()).isEqualTo(1);
- assertThat(timer.totalTime(TimeUnit.NANOSECONDS)).isGreaterThanOrEqualTo(0);
- }
-
- @Test
- @DisplayName("body exception is propagated and timer still records (finally semantics)")
- void bodyException_propagatesAndStillRecords() {
- Timer timer = meterRegistry.timer("test.timer");
-
- assertThatThrownBy(() -> RedisProCacheTimers.timed(timer, () -> {
- throw new IllegalStateException("boom");
- })).isInstanceOf(IllegalStateException.class)
- .hasMessage("boom");
-
- // 与原 try-finally 字节级等价:异常仍沿 finally 推进 timer
- assertThat(timer.count()).isEqualTo(1);
- }
- }
-
- @Nested
- @DisplayName("timedGet Tests")
- class TimedGetTests {
-
- @Test
- @DisplayName("null timer returns supplier result without recording")
- void nullTimer_returnsResultWithoutRecording() {
- String result = RedisProCacheTimers.timedGet(null, () -> "hello");
-
- assertThat(result).isEqualTo("hello");
- }
-
- @Test
- @DisplayName("non-null timer records and returns result")
- void nonNullTimer_recordsAndReturnsResult() {
- Timer timer = meterRegistry.timer("test.timer");
-
- String result = RedisProCacheTimers.timedGet(timer, () -> "computed-value");
-
- assertThat(result).isEqualTo("computed-value");
- assertThat(timer.count()).isEqualTo(1);
- }
-
- @Test
- @DisplayName("supplier exception propagates; timer still records")
- void supplierException_propagatesAndStillRecords() {
- Timer timer = meterRegistry.timer("test.timer");
- AtomicReference