🎯导读:本文档阐述了缓存在提升软件性能与用户体验方面的关键作用,重点讨论了封装缓存组件的设计理念与实践。文档中详述了如何通过配置文件设定Redis缓存的基本属性,如超时时间和时间单位,并引入布隆过滤器来有效预防缓存穿透问题。同时,提供了多种安全获取与存储缓存的方法签名,旨在通过多层次的安全措施应对缓存穿透、击穿及雪崩等常见问题,确保系统的稳定运行。
前言
缓存在现代软件架构中起着至关重要的作用,它通过存储数据的副本以供快速访问,极大地提高了应用程序的性能和响应速度。当应用面对大量请求时,缓存能够减轻数据库的压力,减少读取延迟,从而避免因直接访问后端数据库而导致的性能瓶颈。此外,缓存还能降低服务器负载,节省带宽,并改善用户的整体体验,尤其是在处理热点数据或频繁访问的数据时。
封装缓存组件则是为了提供一个更高层次的抽象接口,使得开发者无需关注缓存的具体实现细节即可使用缓存功能。这样的封装通常包括对缓存数据的一致性管理、过期策略设置、缓存穿透预防等机制,以及支持多种缓存存储方案的选择。通过这种方式,不仅可以简化开发流程,提高代码的可维护性和扩展性,还可以增强系统的灵活性,使其能够更轻松地适应不断变化的应用需求和技术环境。封装后的缓存组件使得业务逻辑更加清晰,同时也促进了代码的复用,是构建高效、稳定系统的重要组成部分。
文件结构
缓存组件的文件结构如下
配置文件
sss:
cache:
redis:
value-timeout: 5
value-time-unit: days
读取配置文件
读取缓存基本配置
package com.dam.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.concurrent.TimeUnit;
/**
* 分布式缓存配置
*/
@Data
@ConfigurationProperties(prefix = RedisDistributedProperties.PREFIX)
public class RedisDistributedProperties {
public static final String PREFIX = "sss.cache.redis";
/**
* Key 前缀
*/
private String prefix = "";
/**
* Key 前缀字符集
*/
private String prefixCharset = "UTF-8";
/**
* 默认超时时间
*/
private Long valueTimeout = 30000L;
/**
* 时间单位
*/
private TimeUnit valueTimeUnit = TimeUnit.MILLISECONDS;
}
读取布隆过滤器相关配置文件
package com.dam.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* 缓存穿透布隆过滤器
*/
@Data
@ConfigurationProperties(prefix = BloomFilterPenetrateProperties.PREFIX)
public class BloomFilterPenetrateProperties {
public static final String PREFIX = "sss.cache.redis.bloom-filter.default";
/**
* 布隆过滤器默认实例名称
*/
private String name = "cache_penetration_bloom_filter";
/**
* 每个元素的预期插入量
*/
private Long expectedInsertions = 64L;
/**
* 预期错误概率
*/
private Double falseProbability = 0.03D;
}
配置类
配置类的作用主要是读取redis相关配置文件,并注册一些Bean
package com.dam.config;
import com.dam.RedisKeySerializer;
import com.dam.StringRedisTemplateProxy;
import lombok.AllArgsConstructor;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
/**
* 缓存配置自动装配
*/
@Component
@AllArgsConstructor
// @EnableConfigurationProperties 注解用于激活 RedisDistributedProperties 和 BloomFilterPenetrateProperties 这两个配置类的支持。
// RedisDistributedProperties 类中定义的所有属性都可以从配置文件(如 application.properties 或 application.yml)中读取并绑定到对应的 Java 对象上
// BloomFilterPenetrateProperties 类中的属性也会被读取并绑定
// 这使得你可以在应用中轻松地访问和使用这些配置属性,而不需要手动创建和配置这些 Bean。
// 例如,在 CacheAutoConfiguration 类中,你可以直接注入 RedisDistributedProperties 和 BloomFilterPenetrateProperties 来获取配置值
@EnableConfigurationProperties({RedisDistributedProperties.class, BloomFilterPenetrateProperties.class})
public class CacheAutoConfiguration {
private final RedisDistributedProperties redisDistributedProperties;
/**
* 创建一个自定义的Redis Key序列化器。
* 根据配置属性设置Key前缀和字符集。
* 这可以用来在所有键前加上一个统一的前缀,便于管理和组织键的空间。
*
* @return 自定义的RedisKeySerializer实例
*/
@Bean
public RedisKeySerializer redisKeySerializer() {
String prefix = redisDistributedProperties.getPrefix();
String prefixCharset = redisDistributedProperties.getPrefixCharset();
return new RedisKeySerializer(prefix, prefixCharset);
}
/**
* 创建一个布隆过滤器,用于防止缓存穿透攻击。
* 仅当配置文件中指定了启用布隆过滤器时才创建。
*
* @param redissonClient Redisson客户端,用于连接Redis集群
* @param bloomFilterPenetrateProperties 布隆过滤器的相关配置属性
* @return 初始化好的RBloomFilter实例
*/
@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;
}
/**
* 静态代理模式:建一个StringRedisTemplate的代理类,以增强其功能。
* 例如,可以添加额外的功能,如缓存穿透保护等。
*
* @param redisKeySerializer 自定义的Redis Key序列化器
* @param stringRedisTemplate Spring Data Redis提供的StringRedisTemplate实例
* @param redissonClient Redisson客户端,用于高级Redis操作
* @return 增强后的StringRedisTemplate代理实例
*/
@Bean(name = "distributedCache")
public StringRedisTemplateProxy StringRedisTemplateDamProxy(RedisKeySerializer redisKeySerializer,
StringRedisTemplate stringRedisTemplate,
RedissonClient redissonClient) {
// System.out.println("配置distributedCache");
// 设置StringRedisTemplate的Key序列化器
stringRedisTemplate.setKeySerializer(redisKeySerializer);
// 返回StringRedisTemplate的代理对象
return new StringRedisTemplateProxy(stringRedisTemplate, redisDistributedProperties, redissonClient);
}
}
【序列化器】
package com.dam;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import java.nio.charset.Charset;
/**
* 自定义的 Redis Key 序列化器。
* 此类实现了 RedisSerializer 接口,用于序列化和反序列化 Redis 中的键。
* 它允许为所有键添加一个公共前缀,并支持指定字符集进行编码和解码。
*/
@RequiredArgsConstructor
public class RedisKeySerializer implements InitializingBean, RedisSerializer<String> {
// Redis 键的前缀
private final String keyPrefix;
// 字符集名称
private final String charsetName;
// 实际使用的字符集对象
private Charset charset;
/**
* 序列化方法,将字符串键转换为字节数组。
*
* @param key 要序列化的字符串键
* @return 字节数组形式的键
* @throws SerializationException 如果序列化过程中发生错误
*/
@Override
public byte[] serialize(String key) throws SerializationException {
// 构建带前缀的键
String builderKey = keyPrefix + key;
// 将字符串键转换为字节数组
// return builderKey.getBytes();
return builderKey.getBytes(Charset.defaultCharset()); // 注意这里应该使用 charset 变量
}
/**
* 反序列化方法,将字节数组转换回字符串键。
*
* @param bytes 包含键信息的字节数组
* @return 字符串形式的键
* @throws SerializationException 如果反序列化过程中发生错误
*/
@Override
public String deserialize(byte[] bytes) throws SerializationException {
// 使用指定的字符集将字节数组转换为字符串
return new String(bytes, charset);
}
/**
* 在所有属性被设置之后调用此方法。
* 用于完成初始化工作,如创建字符集对象。
*/
@Override
public void afterPropertiesSet() throws Exception {
// 根据字符集名称创建字符集对象
charset = Charset.forName(charsetName);
}
}
工具类
该工具类主要用了将传入的参数拼接成key
package com.dam.toolkit;
import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import java.util.Optional;
import java.util.stream.Stream;
/**
* 缓存工具类
*/
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);
}
/**
* 判断结果是否为空或空的字符串。
* 此方法用于检查一个对象是否为null,或者如果它是字符串类型的话,是否为空或仅包含空白字符。
*
* @param cacheVal 要检查的对象
* @return 如果对象为null,或者它是字符串且为空或仅包含空白字符,则返回true;否则返回false
*/
public static boolean isNullOrBlank(Object cacheVal) {
return cacheVal == null || (cacheVal instanceof String && Strings.isNullOrEmpty((String) cacheVal));
}
}
函数式接口
用来接收数据库查询或者其他方法来加载数据
package com.dam.core;
/**
* 缓存加载器
*/
@FunctionalInterface
public interface CacheLoader<T> {
/**
* 从外部数据源加载数据。
*
* @return 加载的数据,类型为 T
*/
T load();
}
CacheLoader加载数据为空之后,执行的后续逻辑,例如做一些日志记录
package com.dam.core;
/**
* 缓存查询为空时的处理接口。
* 当缓存中不存在某个键对应的数据时,可以通过实现该接口来定义执行的逻辑。
*/
@FunctionalInterface
public interface CacheGetIfAbsent<T> {
/**
* 当缓存查询结果为空时执行的逻辑。
*
* @param param 用于执行逻辑的参数,类型为 T
*/
void execute(T param);
}
当查询缓存为空的时候,是否需要进一步去数据库查询,可以通过实现该方法来过滤
package com.dam.core;
/**
* 缓存过滤
*/
@FunctionalInterface
public interface CacheGetFilter<T> {
/**
* 缓存过滤
*
* @param param 输出参数
* @return {@code true} 如果输入参数匹配,否则 {@link Boolean#TRUE}
*/
boolean filter(T param);
}
接口
接口主要用来定义缓存的常用方法
package com.dam;
import com.alibaba.fastjson2.TypeReference;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.util.Collection;
/**
* 缓存接口
*/
public interface Cache {
/**
* 获取缓存
*/
<T> Object get(@NotBlank String key, TypeReference typeReference);
/**
* 放入缓存
*/
void put(@NotBlank String key, Object value);
/**
* 如果 keys 全部不存在,则新增,返回 true,反之 false
*/
Boolean putIfAllAbsent(@NotNull Collection<String> keys);
/**
* 删除缓存
*/
Boolean delete(@NotBlank String key);
/**
* 根据前缀批量删除键
*/
Boolean deleteByPrefix(@NotBlank String keyPrefix);
/**
* 删除 keys,返回删除数量
*/
Long delete(@NotNull Collection<String> keys);
/**
* 判断 key 是否存在
*/
Boolean hasKey(@NotBlank String key);
/**
* 获取缓存组件实例
*/
Object getInstance();
}
【分布式缓存】
package com.dam;
import com.alibaba.fastjson2.TypeReference;
import com.dam.core.CacheGetFilter;
import com.dam.core.CacheGetIfAbsent;
import com.dam.core.CacheLoader;
import org.redisson.api.RBloomFilter;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.util.concurrent.TimeUnit;
/**
* 分布式缓存
*/
public interface DistributedCache extends Cache {
/**
* 获取缓存,如查询结果为空,调用 {@link CacheLoader} 加载缓存
*/
<T> Object get(@NotBlank String key, TypeReference typeReference, CacheLoader<T> cacheLoader, long timeout);
/**
* 获取缓存,如查询结果为空,调用 {@link CacheLoader} 加载缓存
*/
<T> Object get(@NotBlank String key, TypeReference typeReference, CacheLoader<T> cacheLoader, long timeout, TimeUnit timeUnit);
/**
* 以一种"安全"的方式获取缓存,如查询结果为空,调用 {@link CacheLoader} 加载缓存
* 通过此方式防止程序中可能出现的:缓存击穿、缓存雪崩场景,适用于不被外部直接调用的接口
*/
<T> Object safeGet(@NotBlank String key, TypeReference typeReference, CacheLoader<T> cacheLoader, long timeout);
/**
* 以一种"安全"的方式获取缓存,如查询结果为空,调用 {@link CacheLoader} 加载缓存
* 通过此方式防止程序中可能出现的:缓存击穿、缓存雪崩场景,适用于不被外部直接调用的接口
*/
<T> Object safeGet(@NotBlank String key, TypeReference typeReference, CacheLoader<T> cacheLoader, long timeout, TimeUnit timeUnit);
/**
* 以一种"安全"的方式获取缓存,如查询结果为空,调用 {@link CacheLoader} 加载缓存
* 通过此方式防止程序中可能出现的:缓存穿透、缓存击穿以及缓存雪崩场景,需要客户端传递布隆过滤器,适用于被外部直接调用的接口
*/
<T> Object safeGet(@NotBlank String key, TypeReference typeReference, CacheLoader<T> cacheLoader, long timeout, RBloomFilter<String> bloomFilter);
/**
* 以一种"安全"的方式获取缓存,如查询结果为空,调用 {@link CacheLoader} 加载缓存
* 通过此方式防止程序中可能出现的:缓存穿透、缓存击穿以及缓存雪崩场景,需要客户端传递布隆过滤器,适用于被外部直接调用的接口
*/
<T> Object safeGet(@NotBlank String key, TypeReference typeReference, CacheLoader<T> cacheLoader, long timeout, TimeUnit timeUnit, RBloomFilter<String> bloomFilter);
/**
* 以一种"安全"的方式获取缓存,如查询结果为空,调用 {@link CacheLoader} 加载缓存
* 通过此方式防止程序中可能出现的:缓存穿透、缓存击穿以及缓存雪崩场景,需要客户端传递布隆过滤器,并通过 {@link CacheGetFilter} 解决布隆过滤器无法删除问题,适用于被外部直接调用的接口
*/
<T> Object safeGet(@NotBlank String key, TypeReference typeReference, CacheLoader<T> cacheLoader, long timeout, RBloomFilter<String> bloomFilter, CacheGetFilter<String> cacheCheckFilter);
/**
* 以一种"安全"的方式获取缓存,如查询结果为空,调用 {@link CacheLoader} 加载缓存
* 通过此方式防止程序中可能出现的:缓存穿透、缓存击穿以及缓存雪崩场景,需要客户端传递布隆过滤器,并通过 {@link CacheGetFilter} 解决布隆过滤器无法删除问题,适用于被外部直接调用的接口
*/
<T> Object safeGet(@NotBlank String key, TypeReference typeReference, CacheLoader<T> cacheLoader, long timeout, TimeUnit timeUnit, RBloomFilter<String> bloomFilter, CacheGetFilter<String> cacheCheckFilter);
/**
* 以一种"安全"的方式获取缓存,如查询结果为空,调用 {@link CacheLoader} 加载缓存
* 通过此方式防止程序中可能出现的:缓存穿透、缓存击穿以及缓存雪崩场景,需要客户端传递布隆过滤器,并通过 {@link CacheGetFilter} 解决布隆过滤器无法删除问题,适用于被外部直接调用的接口
*/
<T> Object safeGet(@NotBlank String key, TypeReference typeReference, CacheLoader<T> cacheLoader, long timeout,
RBloomFilter<String> bloomFilter, CacheGetFilter<String> cacheCheckFilter, CacheGetIfAbsent<String> cacheGetIfAbsent);
/**
* 以一种"安全"的方式获取缓存,如查询结果为空,调用 {@link CacheLoader} 加载缓存
* 通过此方式防止程序中可能出现的:缓存穿透、缓存击穿以及缓存雪崩场景,需要客户端传递布隆过滤器,并通过 {@link CacheGetFilter} 解决布隆过滤器无法删除问题,适用于被外部直接调用的接口
*/
<T> Object safeGet(@NotBlank String key, TypeReference typeReference, 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);
}
实现类
实现接口中定义的方法,这里可以看成是一种代理模式,因为它增强了StringRedisTemplate
package com.dam;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.TypeReference;
import com.dam.config.RedisDistributedProperties;
import com.dam.core.CacheGetFilter;
import com.dam.core.CacheGetIfAbsent;
import com.dam.core.CacheLoader;
import com.dam.toolkit.CacheUtil;
import com.google.common.collect.Lists;
import lombok.RequiredArgsConstructor;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
/**
* 分布式缓存之操作 Redis 模版代理
* 底层通过 {@link RedissonClient}、{@link StringRedisTemplate} 完成外观接口行为
*/
@RequiredArgsConstructor
public class StringRedisTemplateProxy implements DistributedCache {
private final StringRedisTemplate stringRedisTemplate;
private final RedisDistributedProperties redisProperties;
private final RedissonClient redissonClient;
private static final String SAFE_GET_DISTRIBUTED_LOCK_KEY_PREFIX = "safe_get_distributed_lock_get:";
/**
* 从Redis缓存中获取指定类型的对象。
*
* @param key 缓存键名,用于标识缓存条目
* @param typeReference 指定的类类型,用于将字符串形式的缓存转换为目标对象类型
* @return 转换后的对象,如果缓存中没有对应键的值,则返回null
*/
public Object get(String key, TypeReference typeReference) {
String value = stringRedisTemplate.opsForValue().get(key);
Class rawType = typeReference.getRawType();
// 如果目标类型是String,直接返回获取到的字符串值
if (String.class.isAssignableFrom(rawType)) {
return value;
}
// 否则,使用Fastjson2将JSON格式的字符串解析为目标类型对象
return JSON.parseObject(value, typeReference);
}
/**
* 当给定的所有键在Redis中都不存在时执行插入操作
*
* @param keys
* @return
*/
public Boolean putIfAllAbsent(@NotNull Collection<String> keys) {
// 创建或获取一个单例的Lua脚本实例,该脚本用于在Redis中执行特定的操作
// 如果单例中还没有这个脚本,则创建一个新的脚本实例
DefaultRedisScript<Boolean> actual = RedisScriptSingleton.getInstance();
// 使用String类型的RedisTemplate执行Lua脚本
// 第一个参数是脚本实例
// 第二个参数是键的列表
// 第三个参数是Redis配置中的值超时时间(可能用于设置键值对的有效期)
Boolean result = stringRedisTemplate.execute(actual, Lists.newArrayList(keys), redisProperties.getValueTimeout().toString());
// 检查执行结果是否非空且为true,然后返回
return result != null && result;
}
public Boolean delete(String key) {
return stringRedisTemplate.delete(key);
}
public Boolean deleteByPrefix(String keyPrefix) {
// 根据给定的前缀构建匹配模式,* 是一个通配符,匹配任意数量的字符
String pattern = keyPrefix + "*";
// 使用 RedisCallback 执行操作
stringRedisTemplate.execute((RedisCallback<Void>) connection -> {
// 定义扫描选项,设置匹配模式
ScanOptions options = ScanOptions.scanOptions()
.match(pattern)
// 每次返回的最大键数量
.count(100000)
.build();
// 尝试使用 Cursor 迭代器遍历所有匹配的键
try (Cursor<byte[]> cursor = connection.scan(options)) {
// 创建一个列表来存储要删除的所有键
List<byte[]> keysToDelete = new ArrayList<>();
// 遍历 Cursor 中的所有键,存储到列表中
while (cursor.hasNext()) {
keysToDelete.add(cursor.next());
}
// 如果有键需要删除,则打开一个管道并批量执行删除操作
if (!keysToDelete.isEmpty()) {
connection.openPipeline();
// 遍历列表中的每一个键,并执行删除操作
keysToDelete.forEach(key -> connection.del(key));
// 关闭管道,执行所有之前发送到 Redis 服务器的命令
connection.closePipeline();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
// 返回 null,因为 RedisCallback 的泛型指定为 Void
return null;
});
return true;
}
public Long delete(Collection<String> keys) {
return stringRedisTemplate.delete(keys);
}
/**
* 定义一个泛型方法,该方法用于从缓存中获取数据,如果缓存中没有数据,则通过提供的加载器加载数据
*
* @param key
* @param typeReference
* @param cacheLoader
* @param timeout
* @param timeUnit
* @param <T>
* @return
*/
public <T> Object get(@NotBlank String key, TypeReference typeReference, CacheLoader<T> cacheLoader, long timeout, TimeUnit timeUnit) {
// 尝试从缓存中获取与给定键 'key' 相关的对象,并将它转换为类型 'clazz'
Object result = get(key, typeReference);
// 检查获取到的结果是否非空或非空白字符串(如果 'result' 是字符串的话)
if (!CacheUtil.isNullOrBlank(result)) {
// 如果缓存中有数据,则直接返回该数据
return result;
}
// 如果缓存中没有数据,则调用 loadAndSet 方法加载数据并将其设置到缓存中
// 这里 'false' 参数可能是表示是否强制刷新缓存
// 'null' 参数可能是额外的上下文信息或者用于其他目的
return loadAndSet(key, cacheLoader, timeout, timeUnit, false, null);
}
/**
* 安全地从缓存中获取数据,防止缓存穿透,并使用布隆过滤器减少无效查询。
*
* @param <T> 泛型类型,指定返回值的数据类型
* @param key 缓存键名
* @param typeReference 返回值的类类型,用于反序列化缓存值
* @param cacheLoader 缓存加载器,当缓存中没有数据时用于加载数据
* @param timeout 设置缓存项的有效期
* @param timeUnit 指定有效期的时间单位
* @param bloomFilter 布隆过滤器,用于判断某元素是否可能存在于集合中
* @param cacheGetFilter 自定义过滤器,用于决定是否返回空
* @param cacheGetIfAbsent 当缓存及数据库均无数据时,执行的操作
* @return 缓存中的数据或者通过缓存加载器加载的数据
*/
public <T> Object safeGet(String key, TypeReference typeReference, CacheLoader<T> cacheLoader, long timeout, TimeUnit timeUnit,
RBloomFilter<String> bloomFilter, CacheGetFilter<String> cacheGetFilter, CacheGetIfAbsent<String> cacheGetIfAbsent) {
// 尝试从缓存中获取与给定键 'key' 相关联的对象,并尝试将其转换为 'clazz' 类型
Object result = get(key, typeReference);
// 缓存结果不等于空或空字符串直接返回;
// 通过函数判断是否返回空,为了适配布隆过滤器无法删除的场景;
// 两者都不成立,判断布隆过滤器是否存在,检查键是否不在布隆过滤器中(即键可能不存在于缓存中),不存在返回空
if (!CacheUtil.isNullOrBlank(result)
|| Optional.ofNullable(cacheGetFilter).map(each -> each.filter(key)).orElse(false)
|| Optional.ofNullable(bloomFilter).map(each -> !each.contains(key)).orElse(false)) {
return result;
}
// 获取一个分布式锁实例,用于保证并发环境下的数据一致性
RLock lock = redissonClient.getLock(SAFE_GET_DISTRIBUTED_LOCK_KEY_PREFIX + key);
// 上锁,防止多个线程同时加载相同的数据
lock.lock();
try {
// 双重判定锁,减轻获得分布式锁后线程访问数据库压力
if (CacheUtil.isNullOrBlank(result = get(key, typeReference))) {
// 如果访问 cacheLoader 加载数据为空,执行后续操作
if (CacheUtil.isNullOrBlank(result = loadAndSet(key, cacheLoader, timeout, timeUnit, true, bloomFilter))) {
// 后续操作
Optional.ofNullable(cacheGetIfAbsent).ifPresent(each -> each.execute(key));
}
}
} finally {
// 无论是否出现异常,都要释放锁
lock.unlock();
}
return result;
}
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);
}
/**
* 存储数据之后,将key加入到布隆过滤器
*
* @param key
* @param value
* @param timeout
* @param timeUnit
* @param bloomFilter
*/
public void safePut(String key, Object value, long timeout, TimeUnit timeUnit, RBloomFilter<String> bloomFilter) {
put(key, value, timeout, timeUnit);
if (bloomFilter != null) {
bloomFilter.add(key);
}
}
public Boolean hasKey(String key) {
return stringRedisTemplate.hasKey(key);
}
public Object getInstance() {
return stringRedisTemplate;
}
/**
* 统计存在于Redis中的键的数量
*
* @param keys
* @return
*/
public Long countExistingKeys(String... keys) {
// countExistingKeys 方法接受一个键的列表,并返回这些键中存在于 Redis 中的数量
return stringRedisTemplate.countExistingKeys(Lists.newArrayList(keys));
}
/**
* 从外部加载数据,并存储到缓存中
*
* @param key
* @param cacheLoader
* @param timeout
* @param timeUnit
* @param safeFlag
* @param bloomFilter
* @param <T>
* @return
*/
private <T> T loadAndSet(String key, CacheLoader<T> cacheLoader, long timeout, TimeUnit timeUnit, boolean safeFlag, RBloomFilter<String> bloomFilter) {
// 通过缓存加载器加载数据
T result = cacheLoader.load();
// 检查加载的数据是否为空或空白(如果是字符串类型)
if (CacheUtil.isNullOrBlank(result)) {
// 如果数据为空或空白,则直接返回
return result;
}
if (safeFlag) {
// 使用安全的方式将数据存储到缓存中
safePut(key, result, timeout, timeUnit, bloomFilter);
} else {
// 直接将数据存储到缓存中
put(key, result, timeout, timeUnit);
}
return result;
}
//--------------------------------------------重载方法--------------------------------------------
@Override
public <T> Object get(String key, TypeReference typeReference, CacheLoader<T> cacheLoader, long timeout) {
return get(key, typeReference, cacheLoader, timeout, redisProperties.getValueTimeUnit());
}
public <T> Object safeGet(@NotBlank String key, TypeReference typeReference, CacheLoader<T> cacheLoader, long timeout) {
return safeGet(key, typeReference, cacheLoader, timeout, redisProperties.getValueTimeUnit());
}
public <T> Object safeGet(@NotBlank String key, TypeReference typeReference, CacheLoader<T> cacheLoader, long timeout, TimeUnit timeUnit) {
return safeGet(key, typeReference, cacheLoader, timeout, timeUnit, null);
}
public <T> Object safeGet(@NotBlank String key, TypeReference typeReference, CacheLoader<T> cacheLoader, long timeout, RBloomFilter<String> bloomFilter) {
return safeGet(key, typeReference, cacheLoader, timeout, bloomFilter, null, null);
}
public <T> Object safeGet(@NotBlank String key, TypeReference typeReference, CacheLoader<T> cacheLoader, long timeout, TimeUnit timeUnit, RBloomFilter<String> bloomFilter) {
return safeGet(key, typeReference, cacheLoader, timeout, timeUnit, bloomFilter, null, null);
}
public <T> Object safeGet(String key, TypeReference typeReference, CacheLoader<T> cacheLoader, long timeout, RBloomFilter<String> bloomFilter, CacheGetFilter<String> cacheCheckFilter) {
return safeGet(key, typeReference, cacheLoader, timeout, redisProperties.getValueTimeUnit(), bloomFilter, cacheCheckFilter, null);
}
public <T> Object safeGet(String key, TypeReference typeReference, CacheLoader<T> cacheLoader, long timeout, TimeUnit timeUnit, RBloomFilter<String> bloomFilter, CacheGetFilter<String> cacheCheckFilter) {
return safeGet(key, typeReference, cacheLoader, timeout, timeUnit, bloomFilter, cacheCheckFilter, null);
}
public <T> Object safeGet(String key, TypeReference typeReference, CacheLoader<T> cacheLoader, long timeout,
RBloomFilter<String> bloomFilter, CacheGetFilter<String> cacheGetFilter, CacheGetIfAbsent<String> cacheGetIfAbsent) {
return safeGet(key, typeReference, cacheLoader, timeout, redisProperties.getValueTimeUnit(), bloomFilter, cacheGetFilter, cacheGetIfAbsent);
}
public void safePut(String key, Object value, long timeout, RBloomFilter<String> bloomFilter) {
safePut(key, value, timeout, redisProperties.getValueTimeUnit(), bloomFilter);
}
public void put(String key, Object value) {
// 将数据存入缓存,因为没有设置过期时间,默认使用配置文件中的过期时间
put(key, value, redisProperties.getValueTimeout());
}
public void put(String key, Object value, long timeout) {
put(key, value, timeout, redisProperties.getValueTimeUnit());
}
}
lua脚本
-- 假设 KEYS 是一个由 Redis 客户端动态填充的表(数组),包含了需要检查和设置的键名。
-- 同样地,ARGV 是一个参数表,其中 ARGV[1] 指定了设置键值对时的有效期(以毫秒为单位)。
-- 遍历 KEYS 表中的每一个键名
for i, v in ipairs(KEYS) do
-- 使用 exists 命令检查当前键是否存在于 Redis 中
if (redis.call('exists', v) == 1) then
-- 如果键存在,则直接返回 nil
return nil;
end
end
-- 如果上面的循环没有提前返回,说明所有给定的键都不存在
-- 再次遍历 KEYS 表中的每一个键名
for i, v in ipairs(KEYS) do
-- 使用 set 命令设置键的值为 'default'
redis.call('set', v, 'default');
-- 使用 pexpire 命令为每个键设置过期时间,单位是毫秒
redis.call('pexpire', v, ARGV[1]);
end
-- 所有键都已成功设置且没有提前返回,最后返回 true 表示操作成功
return true;
使用
注入
@Autowired
@Qualifier("distributedCache")
private StringRedisTemplateProxy distributedCache;
使用
类型 resultMap = (类型) distributedCache.safeGet(
key名称,
new TypeReference<类型>>() {
},
() -> {
执行业务
return 结果;
},
时间数量,
时间单位);
示例
Map<String, Object> resultMap = (Map<String, Object>) distributedCache.safeGet(
MODULE_SHIFT_SCHEDULING_CALCULATE_ENTERPRISE_STATISTIC + "getAveragePassengerFlow:" + enterpriseId + "_" + year + "_" + month,
new TypeReference<Map<String, Object>>() {
},
() -> {
List<StoreAveragePassengerFlowVo> storeAveragePassengerFlowVoList = shiftSchedulingStatisticsService.getAveragePassengerFlow(year, month, enterpriseId);
List<String> storeNameList = new ArrayList<>();
List<Double> averagePassengerFlowList = new ArrayList<>();
for (StoreAveragePassengerFlowVo storeAveragePassengerFlowVo : storeAveragePassengerFlowVoList) {
storeNameList.add(storeAveragePassengerFlowVo.getStoreName());
averagePassengerFlowList.add(storeAveragePassengerFlowVo.getAveragePassengerFlow());
}
Map<String, Object> resultMap1 = new HashMap<>();
resultMap1.put("storeNameList", storeNameList);
resultMap1.put("averagePassengerFlowList", averagePassengerFlowList);
return resultMap1;
},
1,
TimeUnit.DAYS);
说明
本文代码来源于马哥 12306 的代码,本人只是根据自己的理解进行少量修改并应用到智能排班系统中。代码仓库为12306,该项目含金量较高,有兴趣的朋友们建议去学习一下。