117 lines
3.8 KiB
Java
117 lines
3.8 KiB
Java
|
|
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 缓存增强模板。
|
||
|
|
*
|
||
|
|
* <p>底层 {@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> 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) {
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
}
|