【智能排班系统】缓存组件封装

发布于:2024-09-18 ⋅ 阅读:(117) ⋅ 点赞:(0)

🎯导读:本文档阐述了缓存在提升软件性能与用户体验方面的关键作用,重点讨论了封装缓存组件的设计理念与实践。文档中详述了如何通过配置文件设定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,该项目含金量较高,有兴趣的朋友们建议去学习一下。


网站公告

今日签到

点亮在社区的每一天
去签到