package com.njcn.middle.cache.template; import com.alibaba.fastjson.TypeReference; import com.njcn.middle.cache.autoconfig.RedisCacheProperties; 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 java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; import java.util.function.Function; /** * Redis 缓存增强模板。 * *

底层 {@link StringRedisTemplate} 存字符串,fastjson 序列化。本类(Task 4)实现读写基础; * {@code getOrLoad} 三防(穿透/雪崩/击穿)由 Task 5 追加。 */ @Slf4j public class RedisCacheEnhanceTemplate { 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) { 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) { redis.opsForValue().set(keyBuilder.build(key), codec.encode(value), applyJitter(ttlSeconds), 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) { return Boolean.TRUE.equals(redis.hasKey(keyBuilder.build(key))); } // ---- 续期 ---- public boolean expire(String key, long 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 + 1); } }