config
BloomFilterPenetrateProperties.java
布隆过滤器配置类
@Data
@ConfigurationProperties(prefix = BloomFilterPenetrateProperties.PREFIX)
public class BloomFilterPenetrateProperties {
public static final String PREFIX = "framework.cache.redis.bloom-filter.default";
/**
* 布隆过滤器默认实例名称
*/
private String name = "cache_penetration_bloom_filter";
/**
* 每个元素的预期插入量
*/
private Long expectedInsertions = 64L;
/**
* 预期错误概率
*/
private Double falseProbability = 0.03D;
}
prefix = BloomFilterPenetrateProperties.PREFIX表示在配置文件中配置的信息与此类绑定,此类中设置的值都是默认值。
例如framework.cache.redis.bloom-filter.default.expected-insertions=1000
CacheAutoConfiguration.java
/**
* 缓存配置自动装配
* 公众号:马丁玩编程,回复:加群,添加马哥微信(备注:12306)获取项目资料
*/
@AllArgsConstructor
@EnableConfigurationProperties({RedisDistributedProperties.class, BloomFilterPenetrateProperties.class})
public class CacheAutoConfiguration {
private final RedisDistributedProperties redisDistributedProperties;
/**
* 创建 Redis Key 序列化器,可自定义 Key Prefix
*/
@Bean
public RedisKeySerializer redisKeySerializer() {
String prefix = redisDistributedProperties.getPrefix();
String prefixCharset = redisDistributedProperties.getPrefixCharset();
return new RedisKeySerializer(prefix, prefixCharset);
}
/**
* 防止缓存穿透的布隆过滤器
*/
@Bean
@ConditionalOnProperty(prefix = BloomFilterPenetrateProperties.PREFIX, name = "enabled", havingValue = "true")
public RBloomFilter<String> cachePenetrationBloomFilter(RedissonClient redissonClient, BloomFilterPenetrateProperties bloomFilterPenetrateProperties) {
RBloomFilter<String> cachePenetrationBloomFilter = redissonClient.getBloomFilter(bloomFilterPenetrateProperties.getName());
cachePenetrationBloomFilter.tryInit(bloomFilterPenetrateProperties.getExpectedInsertions(), bloomFilterPenetrateProperties.getFalseProbability());
return cachePenetrationBloomFilter;
}
@Bean
// 静态代理模式: Redis 客户端代理类增强
public StringRedisTemplateProxy stringRedisTemplateProxy(RedisKeySerializer redisKeySerializer,
StringRedisTemplate stringRedisTemplate,
RedissonClient redissonClient) {
stringRedisTemplate.setKeySerializer(redisKeySerializer);
return new StringRedisTemplateProxy(stringRedisTemplate, redisDistributedProperties, redissonClient);
}
}
RedisDistributedProperties.java
/**
* 分布式缓存配置
* 公众号:马丁玩编程,回复:加群,添加马哥微信(备注:12306)获取项目资料
*/
@Data
@ConfigurationProperties(prefix = RedisDistributedProperties.PREFIX)
public class RedisDistributedProperties {
public static final String PREFIX = "framework.cache.redis";
/**
* Key 前缀
*/
private String prefix = "";
/**
* Key 前缀字符集
*/
private String prefixCharset = "UTF-8";
/**
* 默认超时时间
*/
private Long valueTimeout = 30000L;
/**
* 时间单位
*/
private TimeUnit valueTimeUnit = TimeUnit.MILLISECONDS;
}
core
CacheGetFilter(接口)
/**
* 缓存过滤
* 公众号:马丁玩编程,回复:加群,添加马哥微信(备注:12306)获取项目资料
*/
@FunctionalInterface
public interface CacheGetFilter<T> {
/**
* 缓存过滤
*
* @param param 输出参数
* @return {@code true} 如果输入参数匹配,否则 {@link Boolean#TRUE}
*/
boolean filter(T param);
}
CacheGetIfAbsent(接口)
/**
* 缓存查询为空
* 公众号:马丁玩编程,回复:加群,添加马哥微信(备注:12306)获取项目资料
*/
@FunctionalInterface
public interface CacheGetIfAbsent<T> {
/**
* 如果查询结果为空,执行逻辑
*/
void execute(T param);
}
CacheLoader(接口)
/**
* 缓存加载器
* 公众号:马丁玩编程,回复:加群,添加马哥微信(备注:12306)获取项目资料
*/
@FunctionalInterface
public interface CacheLoader<T> {
/**
* 加载缓存
*/
T load();
}
toolkit
CacheUtil.java
/**
* 缓存工具类
* 公众号:马丁玩编程,回复:加群,添加马哥微信(备注:12306)获取项目资料
*/
public final class CacheUtil {
private static final String SPLICING_OPERATOR = "_";
/**
* 构建缓存标识
*
* @param keys
* @return
*/
public static String buildKey(String... keys) {
Stream.of(keys).forEach(each -> Optional.ofNullable(Strings.emptyToNull(each)).orElseThrow(() -> new RuntimeException("构建缓存 key 不允许为空")));
return Joiner.on(SPLICING_OPERATOR).join(keys);
}
/**
* 判断结果是否为空或空的字符串
*
* @param cacheVal
* @return
*/
public static boolean isNullOrBlank(Object cacheVal) {
return cacheVal == null || (cacheVal instanceof String && Strings.isNullOrEmpty((String) cacheVal));
}
}
FastJson2Util.java
public final class FastJson2Util {
/**
* 构建类型
*
* @param types
* @return
*/
public static Type buildType(Type... types) {
ParameterizedTypeImpl beforeType = null;//定义了一个 beforeType 变量,用来保存前一次创建的 ParameterizedTypeImpl 实例。
if (types != null && types.length > 0) {
if (types.length == 1) {
return new ParameterizedTypeImpl(new Type[]{null}, null, types[0]);
}
for (int i = types.length - 1; i > 0; i--) {
beforeType = new ParameterizedTypeImpl(new Type[]{beforeType == null ? types[i] : beforeType}, null, types[i - 1]);
}
}
return beforeType;
}
}
该工具类用来构建复杂的Type类型
Type... types
: 这是一个可变参数列表,表示可以传入多个 Type
类型的参数。
if(types.length == 1)假如types数组只有一个元素就创建arameterizedTypeImpl
实例并返回
ParameterizedTypeImpl
的构造方法接受三个参数:
- 第一个参数是一个
Type[]
类型的数组,表示泛型参数。 - 第二个参数是
ownerType
,表示外部类的类型,对于一般的类型为null
。 - 第三个参数是
rawType
,即最外层的原始类型(比如List
、Map
等)
for (int i = types.length - 1; i > 0; i--) {...}
如果传入的 types
数组有多个元素,倒序遍历 types
数组,从最后一个元素开始,依次向前构建嵌套的 ParameterizedTypeImpl
实例。
buildType
方法的核心作用是为 Type
类型提供递归嵌套支持。它通过 ParameterizedTypeImpl
实现了 Java 泛型类型的组合,从而能够表示复杂的泛型结构。这样构建的 Type
可以被 JSON.parseObject
等方法使用,以实现对复杂嵌套泛型类型的 JSON 反序列化。
Cache(接口)
此接口实际上定义了许多方法,是为了别的接口继承此接口,进而重写里面的方法,进行不同功能的实现,统一了标准
public interface Cache {
/**
* 获取缓存
*/
<T> T get(@NotBlank String key, Class<T> clazz);
/**
* 放入缓存
*/
void put(@NotBlank String key, Object value);
/**
* 如果 keys 全部不存在,则新增,返回 true,反之 false
*/
Boolean putIfAllAbsent(@NotNull Collection<String> keys);
/**
* 删除缓存
*/
Boolean delete(@NotBlank String key);
/**
* 删除 keys,返回删除数量
*/
Long delete(@NotNull Collection<String> keys);
/**
* 判断 key 是否存在
*/
Boolean hasKey(@NotBlank String key);
/**
* 获取缓存组件实例
*/
Object getInstance();
}
@NotNull注解表示参数不能为空
@noyBlank注解表示字符串参数不能为空,或者为空格以及制表符
<T> T get(@NotBlank String key, Class<T> clazz);
Class<T> clazz表示你希望从缓存中得到什么类型的数据,使用的时候是
如果调用 get("user:1", User.class);
,返回类型将是 User
类型的实例。
DistributedCache(接口)
实现分布式缓存,继承了Cache接口,重写了里面的方法,对基本的操作进行了扩展,比如安全获取以及等等,使用到了CacheLoader,CacheGetFilter,CacheGetIfAbsent
public interface DistributedCache extends Cache {
/**
* 获取缓存,如查询结果为空,调用 {@link CacheLoader} 加载缓存
*/
<T> T get(@NotBlank String key, Class<T> clazz, CacheLoader<T> cacheLoader, long timeout);
/**
* 获取缓存,如查询结果为空,调用 {@link CacheLoader} 加载缓存
*/
<T> T get(@NotBlank String key, Class<T> clazz, CacheLoader<T> cacheLoader, long timeout, TimeUnit timeUnit);
/**
* 以一种"安全"的方式获取缓存,如查询结果为空,调用 {@link CacheLoader} 加载缓存
* 通过此方式防止程序中可能出现的:缓存击穿、缓存雪崩场景,适用于不被外部直接调用的接口
*/
<T> T safeGet(@NotBlank String key, Class<T> clazz, CacheLoader<T> cacheLoader, long timeout);
/**
* 以一种"安全"的方式获取缓存,如查询结果为空,调用 {@link CacheLoader} 加载缓存
* 通过此方式防止程序中可能出现的:缓存击穿、缓存雪崩场景,适用于不被外部直接调用的接口
*/
<T> T safeGet(@NotBlank String key, Class<T> clazz, CacheLoader<T> cacheLoader, long timeout, TimeUnit timeUnit);
/**
* 以一种"安全"的方式获取缓存,如查询结果为空,调用 {@link CacheLoader} 加载缓存
* 通过此方式防止程序中可能出现的:缓存穿透、缓存击穿以及缓存雪崩场景,需要客户端传递布隆过滤器,适用于被外部直接调用的接口
*/
<T> T safeGet(@NotBlank String key, Class<T> clazz, CacheLoader<T> cacheLoader, long timeout, RBloomFilter<String> bloomFilter);
/**
* 以一种"安全"的方式获取缓存,如查询结果为空,调用 {@link CacheLoader} 加载缓存
* 通过此方式防止程序中可能出现的:缓存穿透、缓存击穿以及缓存雪崩场景,需要客户端传递布隆过滤器,适用于被外部直接调用的接口
*/
<T> T safeGet(@NotBlank String key, Class<T> clazz, CacheLoader<T> cacheLoader, long timeout, TimeUnit timeUnit, RBloomFilter<String> bloomFilter);
/**
* 以一种"安全"的方式获取缓存,如查询结果为空,调用 {@link CacheLoader} 加载缓存
* 通过此方式防止程序中可能出现的:缓存穿透、缓存击穿以及缓存雪崩场景,需要客户端传递布隆过滤器,并通过 {@link CacheGetFilter} 解决布隆过滤器无法删除问题,适用于被外部直接调用的接口
*/
<T> T safeGet(@NotBlank String key, Class<T> clazz, CacheLoader<T> cacheLoader, long timeout, RBloomFilter<String> bloomFilter, CacheGetFilter<String> cacheCheckFilter);
/**
* 以一种"安全"的方式获取缓存,如查询结果为空,调用 {@link CacheLoader} 加载缓存
* 通过此方式防止程序中可能出现的:缓存穿透、缓存击穿以及缓存雪崩场景,需要客户端传递布隆过滤器,并通过 {@link CacheGetFilter} 解决布隆过滤器无法删除问题,适用于被外部直接调用的接口
*/
<T> T safeGet(@NotBlank String key, Class<T> clazz, CacheLoader<T> cacheLoader, long timeout, TimeUnit timeUnit, RBloomFilter<String> bloomFilter, CacheGetFilter<String> cacheCheckFilter);
/**
* 以一种"安全"的方式获取缓存,如查询结果为空,调用 {@link CacheLoader} 加载缓存
* 通过此方式防止程序中可能出现的:缓存穿透、缓存击穿以及缓存雪崩场景,需要客户端传递布隆过滤器,并通过 {@link CacheGetFilter} 解决布隆过滤器无法删除问题,适用于被外部直接调用的接口
*/
<T> T safeGet(@NotBlank String key, Class<T> clazz, CacheLoader<T> cacheLoader, long timeout,
RBloomFilter<String> bloomFilter, CacheGetFilter<String> cacheCheckFilter, CacheGetIfAbsent<String> cacheGetIfAbsent);
/**
* 以一种"安全"的方式获取缓存,如查询结果为空,调用 {@link CacheLoader} 加载缓存
* 通过此方式防止程序中可能出现的:缓存穿透、缓存击穿以及缓存雪崩场景,需要客户端传递布隆过滤器,并通过 {@link CacheGetFilter} 解决布隆过滤器无法删除问题,适用于被外部直接调用的接口
*/
<T> T safeGet(@NotBlank String key, Class<T> clazz, CacheLoader<T> cacheLoader, long timeout, TimeUnit timeUnit,
RBloomFilter<String> bloomFilter, CacheGetFilter<String> cacheCheckFilter, CacheGetIfAbsent<String> cacheGetIfAbsent);
/**
* 放入缓存,自定义超时时间
*/
void put(@NotBlank String key, Object value, long timeout);
/**
* 放入缓存,自定义超时时间
*/
void put(@NotBlank String key, Object value, long timeout, TimeUnit timeUnit);
/**
* 放入缓存,自定义超时时间
* 通过此方式防止程序中可能出现的:缓存穿透、缓存击穿以及缓存雪崩场景,需要客户端传递布隆过滤器,适用于被外部直接调用的接口
*/
void safePut(@NotBlank String key, Object value, long timeout, RBloomFilter<String> bloomFilter);
/**
* 放入缓存,自定义超时时间,并将 key 加入步隆过滤器。极大概率通过此方式防止:缓存穿透、缓存击穿、缓存雪崩
* 通过此方式防止程序中可能出现的:缓存穿透、缓存击穿以及缓存雪崩场景,需要客户端传递布隆过滤器,适用于被外部直接调用的接口
*/
void safePut(@NotBlank String key, Object value, long timeout, TimeUnit timeUnit, RBloomFilter<String> bloomFilter);
/**
* 统计指定 key 的存在数量
*/
Long countExistingKeys(@NotNull String... keys);
}
RedisKeySerializer.java
StringRedisTemplateProxy.java
public class StringRedisTemplateProxy implements DistributedCache {
实现了DistributedCache接口,然后重写了其中的方法
@RequiredArgsConstructor
private final StringRedisTemplate stringRedisTemplate;
private final RedisDistributedProperties redisProperties;
private final RedissonClient redissonClient;
加上注解对需要使用到的实例进行了引入
private static final String LUA_PUT_IF_ALL_ABSENT_SCRIPT_PATH = "lua/putIfAllAbsent.lua";
private static final String SAFE_GET_DISTRIBUTED_LOCK_KEY_PREFIX = "safe_get_distributed_lock_get:";
第一个是lua脚本路径
现在对每个方法进行详细的讲解
第一个(get)
@Override
public <T> T get(String key, Class<T> clazz) {
String value = stringRedisTemplate.opsForValue().get(key);
if (String.class.isAssignableFrom(clazz)) {
return (T) value;
}
return JSON.parseObject(value, FastJson2Util.buildType(clazz));
}
是从redis中获取指定Key对应的值
if (String.class.isAssignableFrom(clazz))判断目标类型 clazz
是否为 String
类型或其子类。如果是,则直接将缓存值 value
作为字符串返回。
假如不是String类型或者其子类的话,SON.parseObject(value, FastJson2Util.buildType(clazz))
是在使用 FastJSON 库将 JSON 字符串 value
反序列化为一个特定的 Java 类型 clazz
的对象
第二个(put)
@Override
public void put(String key, Object value) {
put(key, value, redisProperties.getValueTimeout());
}
@Override
public void put(String key, Object value, long timeout) {
put(key, value, timeout, redisProperties.getValueTimeUnit());
}
@Override
public void put(String key, Object value, long timeout, TimeUnit timeUnit) {
String actual = value instanceof String ? (String) value : JSON.toJSONString(value);
stringRedisTemplate.opsForValue().set(key, actual, timeout, timeUnit);
}
此方法是放入缓存
instanceof
运算符用于测试一个对象是否是某个特定类或其子类的实例,或者是否实现了某个接口
设置过期时间以及时间单位
第三个(putIfAllAbsent)
@Override
public Boolean putIfAllAbsent(@NotNull Collection<String> keys) {
DefaultRedisScript<Boolean> actual = Singleton.get(LUA_PUT_IF_ALL_ABSENT_SCRIPT_PATH, () -> {
DefaultRedisScript redisScript = new DefaultRedisScript();
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource(LUA_PUT_IF_ALL_ABSENT_SCRIPT_PATH)));
redisScript.setResultType(Boolean.class);
return redisScript;
});
Boolean result = stringRedisTemplate.execute(actual, Lists.newArrayList(keys), redisProperties.getValueTimeout().toString());
return result != null && result;
}
该方法名为 putIfAllAbsent
,它接受一个 Collection<String>
类型的参数 keys
,表示要放入缓存的键。该方法返回一个 Boolean
类型的值,表示操作的结果。
这个 putIfAllAbsent
方法的主要功能是尝试将一组键放入 Redis 中,仅当所有键都不存在时才会执行插入操作。它通过 Lua 脚本实现了原子性,确保在高并发场景下的安全性。这种方式能够有效防止缓存穿透问题,并提高性能,因为它减少了与 Redis 的交互次数。
第四个(delete)
@Override
public Boolean delete(String key) {
return stringRedisTemplate.delete(key);
}
@Override
public Long delete(Collection<String> keys) {
return stringRedisTemplate.delete(keys);
}
分别是在redis中删除键和键的集合