package com.njcn.middle.cache.template; import cn.hutool.core.util.IdUtil; 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; import com.njcn.middle.cache.support.CacheValueCodec; import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import java.time.Duration; import java.util.Collections; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.function.Supplier; /** * Redis 缓存增强模板。 * *

底层 {@link StringRedisTemplate} 存字符串,fastjson 序列化。本类(Task 4)实现读写基础; * {@code getOrLoad} 三防(穿透/雪崩/击穿)由 Task 5 追加。 */ @Slf4j public class RedisCacheEnhanceTemplate { /** 释放锁 Lua:token 匹配才删,防误删他人锁。 */ private static final DefaultRedisScript UNLOCK_SCRIPT = new DefaultRedisScript<>( "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end", Long.class); private final StringRedisTemplate redis; private final CacheKeyBuilder keyBuilder; private final CacheValueCodec codec; private final RedisCacheProperties props; public RedisCacheEnhanceTemplate(StringRedisTemplate redis, CacheKeyBuilder keyBuilder, CacheValueCodec codec, RedisCacheProperties props) { validateProps(props); this.redis = redis; this.keyBuilder = keyBuilder; this.codec = codec; this.props = props; } // ---- 写 ---- public void set(String key, Object value) { set(key, value, props.getDefaultTtlSeconds()); } public void set(String key, Object value, long ttlSeconds) { requirePositiveTtl(ttlSeconds); // 空值(写入空标记)按短 null-ttl 处理,与回源 null 一致,避免空标记长期占位 long ttl = (value == null) ? props.getNullTtlSeconds() : applyJitter(ttlSeconds); redis.opsForValue().set(keyBuilder.build(key), codec.encode(value), ttl, TimeUnit.SECONDS); } // ---- 读 ---- public T get(String key, Class type) { return doGet(key, raw -> codec.decode(raw, type)); } public T get(String key, TypeReference type) { return doGet(key, raw -> codec.decode(raw, type)); } private T doGet(String key, Function decoder) { String dataKey = keyBuilder.build(key); String raw = redis.opsForValue().get(dataKey); if (raw == null || codec.isNullSentinel(raw)) { return null; } T value; try { value = decoder.apply(raw); } catch (RuntimeException e) { throw new IllegalStateException("cache decode failed, key=" + dataKey, e); } slidingTouch(dataKey); return value; } // ---- 删 / 判存 ---- public boolean delete(String key) { return Boolean.TRUE.equals(redis.delete(keyBuilder.build(key))); } public boolean exists(String key) { // 与 get 口径一致:空值标记不算"存在"(物理 hasKey 会把空标记算存在,与 get→null 打架) String raw = redis.opsForValue().get(keyBuilder.build(key)); return raw != null && !codec.isNullSentinel(raw); } // ---- 续期 ---- public boolean expire(String key, long ttlSeconds) { requirePositiveTtl(ttlSeconds); return Boolean.TRUE.equals( redis.expire(keyBuilder.build(key), applyJitter(ttlSeconds), TimeUnit.SECONDS)); } // ---- 内部 ---- /** 滑动过期:开启时对已存在的真实值 key 续期(直接对 dataKey 操作,失败忽略)。 */ private void slidingTouch(String dataKey) { if (!props.isSlidingExpireEnabled()) { return; } try { redis.expire(dataKey, applyJitter(props.getDefaultTtlSeconds()), TimeUnit.SECONDS); } catch (RuntimeException e) { log.warn("[redis-cache] sliding expire failed key={}", dataKey, e); } } /** 在原 TTL 上叠加 [0, ttl*ratio) 的随机抖动(只增不减,半开区间);ratio<=0 或 ttl<=0 时原样返回。 */ long applyJitter(long ttlSeconds) { double ratio = props.getJitterRatio(); if (ratio <= 0 || ttlSeconds <= 0) { return ttlSeconds; } long bound = (long) (ttlSeconds * ratio); if (bound <= 0) { return ttlSeconds; } return ttlSeconds + ThreadLocalRandom.current().nextLong(bound); } /** TTL 必须为正:0/负会被 Redis 当作"立即删除"(EXPIRE)或非法过期时间(SET),显式 fail-fast。 */ private static void requirePositiveTtl(long ttlSeconds) { if (ttlSeconds <= 0) { throw new IllegalArgumentException("ttlSeconds must be positive, but was " + ttlSeconds); } } /** 构造期校验配置项,误配在装配阶段即 fail-fast,而非运行期才暴露。 */ private static void validateProps(RedisCacheProperties props) { if (props.getDefaultTtlSeconds() <= 0) { throw new IllegalArgumentException( "redis-cache.default-ttl-seconds must be positive, but was " + props.getDefaultTtlSeconds()); } if (props.getNullTtlSeconds() <= 0) { throw new IllegalArgumentException( "redis-cache.null-ttl-seconds must be positive, but was " + props.getNullTtlSeconds()); } double ratio = props.getJitterRatio(); if (ratio < 0 || ratio >= 1) { throw new IllegalArgumentException( "redis-cache.jitter-ratio must be in [0,1), but was " + ratio); } RedisCacheProperties.Breakdown bd = props.getBreakdown(); if (bd == null) { throw new IllegalArgumentException("redis-cache.breakdown must not be null"); } if (bd.getLockTtlMs() <= 0) { throw new IllegalArgumentException( "redis-cache.breakdown.lock-ttl-ms must be positive, but was " + bd.getLockTtlMs()); } if (bd.getWaitTimeoutMs() < 0) { throw new IllegalArgumentException( "redis-cache.breakdown.wait-timeout-ms must be >= 0, but was " + bd.getWaitTimeoutMs()); } if (bd.getWaitIntervalMs() <= 0) { throw new IllegalArgumentException( "redis-cache.breakdown.wait-interval-ms must be positive, but was " + bd.getWaitIntervalMs()); } } // ---- 读取或回源(三防核心)---- public T getOrLoad(String key, Class type, Supplier loader) { return getOrLoad(key, type, props.getDefaultTtlSeconds(), loader); } public T getOrLoad(String key, Class type, long ttlSeconds, Supplier loader) { return doGetOrLoad(key, raw -> codec.decode(raw, type), ttlSeconds, loader); } public T getOrLoad(String key, TypeReference type, long ttlSeconds, Supplier loader) { return doGetOrLoad(key, raw -> codec.decode(raw, type), ttlSeconds, loader); } private T doGetOrLoad(String key, Function decoder, long ttlSeconds, Supplier loader) { requirePositiveTtl(ttlSeconds); String dataKey = keyBuilder.build(key); Lookup r = tryGet(dataKey, decoder); if (r.hit) { if (r.realValue) { slidingTouch(dataKey); } return r.value; } RedisCacheProperties.Breakdown bd = props.getBreakdown(); if (!bd.isEnabled()) { return loadAndCache(dataKey, ttlSeconds, loader); } String lockKey = keyBuilder.buildLock(key); // 复用 keyBuilder,锁 key 规则单一来源 String token = IdUtil.fastSimpleUUID(); boolean locked = Boolean.TRUE.equals(redis.opsForValue() .setIfAbsent(lockKey, token, Duration.ofMillis(bd.getLockTtlMs()))); if (locked) { try { Lookup r2 = tryGet(dataKey, decoder); // double-check if (r2.hit) { if (r2.realValue) { slidingTouch(dataKey); } return r2.value; } return loadAndCache(dataKey, ttlSeconds, loader); } finally { releaseLock(lockKey, token); } } // 未抢到锁:自旋等待重读,超时或被中断则降级直接回源 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) { slidingTouch(dataKey); } return r3.value; } } log.warn("[redis-cache] breakdown wait timeout/interrupted, fallback to loader, key={}", key); return loader.get(); } /** 读取三态:未命中 / 命中空值标记 / 命中真实值;反序列化失败当未命中降级。 */ private Lookup tryGet(String dataKey, Function decoder) { String raw = redis.opsForValue().get(dataKey); if (raw == null) { return Lookup.miss(); } if (codec.isNullSentinel(raw)) { return Lookup.nullHit(); } try { return Lookup.hit(decoder.apply(raw)); } catch (RuntimeException e) { log.warn("[redis-cache] decode failed, treat as miss key={}", dataKey, e); return Lookup.miss(); } } /** 回源并写缓存:null → 空值标记(穿透);非 null → 抖动 TTL(雪崩)。 */ private T loadAndCache(String dataKey, long ttlSeconds, Supplier loader) { T value = loader.get(); if (value == null) { redis.opsForValue().set(dataKey, CacheConstant.NULL_SENTINEL, props.getNullTtlSeconds(), TimeUnit.SECONDS); } else { redis.opsForValue().set(dataKey, codec.encode(value), applyJitter(ttlSeconds), TimeUnit.SECONDS); } return value; } private void releaseLock(String lockKey, String token) { try { redis.execute(UNLOCK_SCRIPT, Collections.singletonList(lockKey), token); } catch (RuntimeException e) { log.warn("[redis-cache] release lock failed lockKey={}", lockKey, e); } } /** 休眠;返回 false 表示被中断(已重置中断标志,调用方应停止等待)。 */ private static boolean sleep(long ms) { try { Thread.sleep(ms); return true; } catch (InterruptedException e) { Thread.currentThread().interrupt(); return false; } } /** 读取三态载体(对外不暴露)。 */ private static final class Lookup { final boolean hit; // 命中(真实值或空值标记) final boolean realValue; // 命中且为真实值 final T value; private Lookup(boolean hit, boolean realValue, T value) { this.hit = hit; this.realValue = realValue; this.value = value; } static Lookup miss() { return new Lookup<>(false, false, null); } static Lookup nullHit() { return new Lookup<>(true, false, null); } static Lookup hit(T v) { return new Lookup<>(true, true, v); } } }