Files
redis-stream-springboot-sta…/src/main/java/com/njcn/middle/cache/template/RedisCacheEnhanceTemplate.java

317 lines
12 KiB
Java
Raw Normal View History

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 缓存增强模板
*
* <p>底层 {@link StringRedisTemplate} 存字符串,fastjson 序列化本类(Task 4)实现读写基础;
* {@code getOrLoad} 三防(穿透/雪崩/击穿) Task 5 追加
*/
@Slf4j
public class RedisCacheEnhanceTemplate {
/** 释放锁 Lua:token 匹配才删,防误删他人锁。 */
private static final DefaultRedisScript<Long> 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);
redis.opsForValue().set(keyBuilder.build(key), codec.encode(value),
applyJitter(ttlSeconds), TimeUnit.SECONDS);
}
// ---- 读 ----
public <T> T get(String key, Class<T> type) {
return doGet(key, raw -> codec.decode(raw, type));
}
public <T> T get(String key, TypeReference<T> type) {
return doGet(key, raw -> codec.decode(raw, type));
}
private <T> T doGet(String key, Function<String, T> 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 + 1);
}
/** 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> T getOrLoad(String key, Class<T> type, Supplier<T> loader) {
return getOrLoad(key, type, props.getDefaultTtlSeconds(), loader);
}
public <T> T getOrLoad(String key, Class<T> type, long ttlSeconds, Supplier<T> loader) {
return doGetOrLoad(key, raw -> codec.decode(raw, type), ttlSeconds, loader);
}
public <T> T getOrLoad(String key, TypeReference<T> type, long ttlSeconds, Supplier<T> loader) {
return doGetOrLoad(key, raw -> codec.decode(raw, type), ttlSeconds, loader);
}
private <T> T doGetOrLoad(String key, Function<String, T> decoder, long ttlSeconds, Supplier<T> loader) {
requirePositiveTtl(ttlSeconds);
String dataKey = keyBuilder.build(key);
Lookup<T> 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 = dataKey + CacheConstant.LOCK_SUFFIX;
String token = IdUtil.fastSimpleUUID();
boolean locked = Boolean.TRUE.equals(redis.opsForValue()
.setIfAbsent(lockKey, token, Duration.ofMillis(bd.getLockTtlMs())));
if (locked) {
try {
Lookup<T> 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<T> 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 <T> Lookup<T> tryGet(String dataKey, Function<String, T> 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> T loadAndCache(String dataKey, long ttlSeconds, Supplier<T> 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<T> {
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 <T> Lookup<T> miss() {
return new Lookup<>(false, false, null);
}
static <T> Lookup<T> nullHit() {
return new Lookup<>(true, false, null);
}
static <T> Lookup<T> hit(T v) {
return new Lookup<>(true, true, v);
}
}
}