@ -1,426 +1,243 @@
package cn.org.alan.exam.util ;
import cn.hutool.core.util.BooleanUtil ;
// 导入 `BooleanUtil` 类,它来自 `cn.hutool.core.util` 包,这个工具类主要用于对 `Boolean` 类型进行一些实用的操作,例如在与 Redis 交互获取锁操作时(如代码中 `tryLock` 方法内),可以使用它来安全地判断返回的 `Boolean` 值是否为 `true`,避免自动拆箱可能导致的空指针异常,方便对布尔类型的结果进行更稳健的处理,使代码在处理这类情况时更加健壮。
import cn.hutool.core.util.StrUtil ;
// 导入 `StrUtil` 类,同样来自 `cn.hutool.core.util` 包,它提供了丰富的字符串操作方法。在代码中多处用于判断字符串是否为空(如 `isNotBlank` 和 `isBlank` 方法的使用场景),相较于 Java 原生的字符串判断方式,它提供了更便捷、功能更全面的字符串判空以及其他常用字符串操作功能,帮助更方便地处理和验证缓存数据获取到的 JSON 字符串等相关操作中的字符串情况。
import cn.hutool.json.JSONObject ;
// 导入 `JSONObject` 类,属于 `cn.hutool.json` 包,`JSONObject` 可以看作是一个类似 Java 中 `Map` 结构的对象,用于操作 JSON 数据中的键值对信息。在代码里常用于对从 Redis 中获取到的缓存数据(以 JSON 字符串形式存储,解析后就常以 `JSONObject` 形式来操作)进行提取具体属性值、获取 JSON 数组等操作,方便后续将其转换为业务所需的具体 Java 对象或者构建更复杂的数据结构(如分页数据结构等)。
import cn.hutool.json.JSONUtil ;
// 导入 `JSONUtil` 类,也是 `cn.hutool.json` 包中的工具类,它提供了一系列用于 JSON 数据与 Java 对象之间相互转换的便捷方法。比如在向 Redis 存储数据时,会使用它将 Java 对象序列化为 JSON 字符串(通过 `toJsonStr` 方法);从 Redis 获取到 JSON 字符串数据后,又会用它将 JSON 字符串反序列化为 Java 对象(通过 `toBean` 方法)或者转换为 `List` 等其他数据结构(通过 `toList` 等相关方法),极大地简化了 JSON 数据在缓存操作中的处理流程。
import cn.org.alan.exam.model.entity.RedisData ;
// 导入 `RedisData` 类,它属于项目自定义的 `cn.org.alan.exam.model.entity` 包,这个类应该是专门用于封装在 Redis 中存储的数据以及其相关属性(例如逻辑过期时间等信息)的实体类,使得在缓存操作中可以通过这个统一的实体类来管理和传递缓存相关的数据,方便进行逻辑过期判断以及数据的存储和提取等操作,增强了缓存数据结构的规范性和可维护性。
import cn.org.alan.exam.model.vo.GradeVO ;
// 导入 `GradeVO` 类,来自 `cn.org.alan.exam.model.vo` 包,通常 `VO`( Value Object, 值对象) 是用于在不同层之间传递数据的简单对象, 这里的 `GradeVO` 大概率是用于表示特定业务场景下(可能与成绩相关等,具体取决于业务含义)的数据传输对象,在缓存操作涉及到与成绩相关的数据获取、存储以及返回等流程中,会使用这个类来明确数据的结构和类型,便于进行类型转换以及符合业务逻辑的操作。
import com.baomidou.mybatisplus.core.metadata.IPage ;
// 导入 `IPage` 类,它是 `com.baomidou.mybatisplus.core.metadata` 包中的接口,在 MyBatis Plus 框架中用于表示分页数据的相关信息,包含了如总记录数、每页大小、当前页码以及实际的数据记录列表等分页相关的属性和操作方法。在缓存操作涉及分页数据获取和返回时(比如从 Redis 中获取缓存的分页数据并进行相应处理后返回符合 `IPage` 格式的数据给调用者),会基于这个接口来构建和操作分页数据结构,以满足业务中对分页查询缓存数据的需求。
import com.baomidou.mybatisplus.extension.plugins.pagination.Page ;
// 导入 `Page` 类,来自 `com.baomidou.mybatisplus.extension.plugins.pagination` 包,它是 `IPage` 接口的一个具体实现类,在代码中用于创建实际的分页对象实例(如在处理缓存中分页数据时,通过 `new Page<>()` 创建分页对象,然后设置相应的分页属性,像记录列表、总记录数等,来构建完整的分页数据结构用于返回符合业务要求的分页数据结果),方便在 MyBatis Plus 框架下进行分页相关的操作以及与缓存操作结合处理分页数据的缓存情况。
import jakarta.annotation.Resource ;
// 导入 `Resource` 注解,它来自 `jakarta.annotation` 包(这是 Java EE 规范中的一部分,用于实现依赖注入等功能),在代码中用于告诉 Spring 容器查找并注入相应类型的资源(例如 `StringRedisTemplate` 实例)到对应的字段上,通过这个注解实现了依赖注入的功能,使得代码遵循松耦合的设计原则,方便获取和使用其他组件提供的功能,提高代码的可维护性和扩展性。
import lombok.extern.slf4j.Slf4j ;
// 导入 `Slf4j` 注解,来自 `lombok.extern.slf4j` 包, Lombok 是一个通过注解来简化 Java 代码编写的工具,`Slf4j` 注解会在编译期自动为这个类生成一个名为“log”的日志记录对象, 方便在类中使用这个对象来记录各种运行时的信息, 例如在缓存操作出现异常、缓存命中情况等场景下记录相关日志, 便于后续的调试和监控工作, 提升代码在生产环境运行时的可观察性和问题排查能力。
import org.apache.catalina.Pipeline ;
// 导入 `Pipeline` 类,它属于 `org.apache.catalina` 包,在 Tomcat 服务器的架构中,`Pipeline` 用于表示一个处理请求的管道, 包含了一系列的阀门( Valve) , 用于对请求进行不同阶段的处理, 不过在当前展示的缓存相关代码中, 它可能并没有直接被使用到, 也许所在的类所在的项目环境中有和 Tomcat 集成等相关情况才引入了这个类,或者只是作为一种通用的依赖引入(但从当前代码逻辑看暂时未体现其作用)。
import org.apache.catalina.connector.Response ;
// 导入 `Response` 类,来自 `org.apache.catalina.connector` 包,在 Tomcat 服务器中,`Response` 类用于封装对客户端请求的响应信息,例如设置响应头、响应状态码、响应体内容等,同样在当前缓存相关代码里,它可能没有直接参与到主要的缓存操作逻辑中,也许是所在项目环境基于 Tomcat 相关架构时有潜在的关联或者只是作为一种通用依赖引入(目前从代码逻辑角度未看到其实际作用)。
import org.springframework.data.redis.core.StringRedisTemplate ;
// 导入 `StringRedisTemplate` 类,它是 `org.springframework.data.redis.core` 包中的重要类,是 Spring Data Redis 提供的用于操作 Redis 的模板类,专门用于处理 Redis 中字符串类型的数据(实际上 Redis 中存储的数据大多可以以字符串形式来操作,其他复杂结构也常基于字符串序列化来存储)。在代码中通过注入这个模板类的实例后,就可以利用它提供的各种方法(如 `opsForValue` 方法获取操作字符串值的相关操作对象,进而进行数据的存储、查询等操作)来与 Redis 进行交互,实现缓存数据的读写等功能,是整个缓存操作代码与 Redis 进行通信的核心工具类。
import org.springframework.data.redis.core.script.DefaultRedisScript ;
// 导入 `DefaultRedisScript` 类,属于 `org.springframework.data.redis.core.script` 包,它是 Spring Data Redis 中用于定义 Redis 脚本的一个默认实现类。在一些复杂的 Redis 操作场景中(比如需要执行自定义的 Lua 脚本实现特定的业务逻辑,像分布式锁的复杂判断和操作等情况),可以通过这个类来创建 Redis 脚本对象,设置脚本内容以及期望的返回类型等信息,然后结合 `StringRedisTemplate` 来执行脚本,实现更灵活、功能更强大的 Redis 操作功能,不过从当前展示的代码来看,可能部分相关使用代码被注释掉了或者暂时未体现完整的使用场景。
import org.springframework.data.redis.core.script.RedisScript ;
// 导入 `RedisScript` 接口,来自 `org.springframework.data.redis.core.script` 包,它定义了 Redis 脚本相关的通用行为和属性,`DefaultRedisScript` 就是它的一个具体实现类。通过面向接口编程的方式,使得代码在使用 Redis 脚本时更具灵活性和可扩展性,可以方便地切换不同的脚本实现或者进行统一的脚本操作处理,虽然在当前代码中可能没有全面展示其所有使用方式,但遵循接口规范有助于代码更好地适应未来可能的功能扩展和变化。
import org.springframework.stereotype.Component ;
// 导入 `@Component` 注解,它是 `org.springframework.stereotype` 包中的注解,在 Spring 框架中用于将一个类标记为 Spring 组件,意味着 Spring 容器在进行组件扫描时能够发现并管理这个类,将其作为一个 Bean 纳入到 Spring 的 IoC( Inversion of Control, 控制反转) 容器中, 便于后续在需要的地方通过依赖注入等方式使用该类对应的功能, 实现了组件化管理, 让这个类能更好地融入到整个 Spring 项目的体系架构中,方便与其他 Spring 组件协作完成缓存相关的业务功能。
import java.time.LocalDateTime ;
// 导入 `LocalDateTime` 类,来自 `java.time` 包,它是 Java 8 引入的新的日期和时间 API 中的一部分,用于表示不带时区信息的日期时间对象,在代码中常用于表示缓存数据的逻辑过期时间(比如在设置逻辑过期的缓存数据时,通过 `LocalDateTime.now()` 获取当前时间并加上一定时长来计算出逻辑过期时间,以此来实现更灵活的缓存过期判断逻辑,区别于 Redis 原生的简单基于时间戳的物理过期机制,提高缓存管理的灵活性和业务适应性)。
import java.util.* ;
// 导入 `java.util` 包,这是 Java 标准库中非常基础且常用的一个包,包含了大量用于处理集合(如 `List`、`Map`、`Set` 等)、日期时间(部分旧的日期时间相关类等,不过在 Java 8 后推荐使用新的 `java.time` 包中的类)、随机数生成(`Random` 类用于生成随机数,在代码中用于给缓存过期时间添加一定随机性以避免缓存雪崩等问题)等各种常用功能的类和接口,为代码中的很多操作(比如缓存数据存储时使用 `Map` 结构来整理数据、生成随机的缓存过期时间等情况)提供了基础的工具支持。
import java.util.concurrent.* ;
// 导入 `java.util.concurrent` 包,它提供了用于支持并发编程的各种类和接口,例如线程池相关的 `ExecutorService`(代码中创建了固定线程数量的线程池 `CACHE_REBUILD_EXECUTOR` 用于在缓存击穿处理时异步执行缓存重建任务,避免主线程阻塞,提高系统并发处理能力)、`Callable` 和 `Future` 等用于支持更灵活的异步任务执行和结果获取等功能的类,在处理缓存相关操作中涉及到多线程、异步任务(如缓存重建等场景)时会依赖这个包中的相关功能来实现高效、稳定的并发操作。
import java.util.function.Function ;
// 导入 `Function` 接口,它属于 `java.util.function` 包,是 Java 8 引入的函数式接口之一,用于表示接受一个参数并返回一个结果的函数,在代码中常用于传递数据库查询逻辑(例如在 `queryWithPassThrough` 和 `queryWithLogicalExpire` 等方法中,通过传入 `Function<ID, R>` 类型的参数 `dbFallback`,可以使用 Lambda 表达式或者方法引用来指定当缓存未命中或者缓存数据过期时从数据库中获取对应数据的具体实现方法,体现了函数式编程风格在缓存操作代码中的应用,使得代码更加简洁、灵活且易于扩展)。
import java.util.stream.Collectors ;
// 导入 `Collectors` 类,来自 `java.util.stream` 包,它在 Java 8 的 Stream API 中用于对数据流进行各种收集操作,比如将流中的元素收集到 `List`、`Map`、`Set` 等集合类型中(在代码里有多处使用,例如将经过处理后的字符串键通过 `collect(Collectors.toList())` 收集到 `List<String>` 类型的列表中,方便后续批量操作缓存数据时使用,利用 Stream API 和 `Collectors` 类可以更方便、高效地处理集合数据以及进行数据转换和整理等操作)。
/ * *
* @Author Alan
* @Version
* @Date 2024 / 6 / 9 11 : 03 PM
* /
@Slf4j
// 使用Lombok的@Slf4j注解, 会在编译期自动为这个类生成一个名为“log”的日志记录对象, 方便在类中使用这个对象来记录各种运行时的信息, 例如在缓存操作出现异常、命中情况等场景下记录相关日志, 便于后续的调试和监控工作。
@Component
// 使用Spring的@Component注解将这个类标记为一个Spring组件, 使得Spring容器在进行组件扫描时能够发现并管理它, 便于后续在需要的地方通过依赖注入等方式使用该类对应的功能, 实现了组件化管理, 让这个类能更好地融入到整个Spring项目的体系架构中, 方便与其他Spring组件协作完成缓存相关的业务功能。
public class CacheClient {
// 注入StringRedisTemplate
@Resource
// 使用Jakarta EE中的@Resource注解进行资源注入, 告诉Spring容器查找并注入一个StringRedisTemplate类型的实例到当前类的这个字段中, 使得本类可以使用这个已经配置好的StringRedisTemplate对象来与Redis进行交互操作, 例如进行数据的存储、查询等操作, 符合依赖注入的开发模式, 方便获取和使用其他组件提供的功能。
private StringRedisTemplate stringRedisTemplate ;
private Random random = new Random ( ) ;
// 创建一个Random实例, 用于生成随机数, 在这里主要是在设置缓存过期时间等操作时, 添加一定的随机时长, 以避免大量缓存同时过期引发缓存雪崩等问题, 通过增加这个随机性可以让缓存过期时间更加分散, 提高缓存系统的稳定性和可靠性。
// 方法1 解决穿透
public void set ( String key , Object value , Long time , TimeUnit unit ) {
// 我们往Redis存的时候不能是Object类型, 我们需要把Object序列化为JSON字符串
// 使用注入的StringRedisTemplate对象的opsForValue方法获取操作字符串值的相关操作对象, 然后调用其set方法将键值对存入Redis。
// 其中, JSONUtil.toJsonStr(value)是利用Hutool工具库中的JSONUtil工具类, 将传入的Java对象( value) 转换为JSON字符串格式, 这样才能符合Redis存储要求, 将数据以合适的格式保存到Redis中, time参数表示缓存的时长, unit参数表示时长的时间单位( 例如秒、分钟等) , 用于设置缓存的过期策略, 确保缓存数据在一定时间后自动失效, 避免数据长期占用内存空间且能保证数据的时效性。
stringRedisTemplate . opsForValue ( ) . set ( key , JSONUtil . toJsonStr ( value ) , time , unit ) ;
}
// 方法2 解决击穿 使用逻辑过期 比上面的方法的操作多了一个逻辑过期字段而已
public void setWithLogicalExpire ( String key , Object value , Long time , TimeUnit unit ) {
// 设置逻辑过期
// 创建一个RedisData对象, 这个对象应该是项目自定义的用于封装在Redis中存储的数据以及其逻辑过期时间等相关信息的实体类, 通过它可以更好地管理带有逻辑过期特性的数据存储结构, 以便后续能方便地判断数据是否逻辑过期, 实现更灵活的缓存策略。
RedisData redisData = new RedisData ( ) ;
// 将传入的业务数据( value) 设置到RedisData对象的data属性中, 这里的设计是将实际要缓存的数据作为一个属性封装在RedisData对象内, 方便后续统一处理和扩展, 例如可以在RedisData类中添加更多与缓存相关的元数据等信息。
redisData . setData ( value ) ;
// 计算逻辑过期时间, 通过获取当前时间( LocalDateTime.now()) 并加上指定的时长( unit.toSeconds(time)先将传入的时间单位转换为秒数, 再进行时间相加操作) , 得到一个未来的时间点, 表示该缓存数据的逻辑过期时间, 以此来实现逻辑过期的功能, 区别于Redis原生的基于时间戳的物理过期机制, 逻辑过期可以在代码层面更灵活地控制缓存数据的有效性判断。
redisData . setExpireTime ( LocalDateTime . now ( ) . plusSeconds ( unit . toSeconds ( time ) ) ) ;
// 我们往Redis存的时候不能是Object类型, 我们需要把Object序列化为JSON字符串
// 同样使用StringRedisTemplate的opsForValue操作对象将数据存入Redis, 不过这里是将封装好数据和逻辑过期时间的RedisData对象转换为JSON字符串后再存入, 这样Redis中存储的内容就包含了业务数据以及对应的逻辑过期时间信息, 方便后续读取出来进行相关的逻辑过期判断等操作。
stringRedisTemplate . opsForValue ( ) . set ( key , JSONUtil . toJsonStr ( redisData ) ) ;
}
// 方法3 解决穿透
// 返回值不确定,我们要使用泛型,比如<R>R,具体是什么类型由用户传入Class<R> type
public < R , ID , T > R queryWithPassThrough ( String keyPrefix , ID id , Class < R > type , Function < ID , R > dbFallback , Class < T > voClass , Long time , TimeUnit unit ) {
// 定义了一个泛型方法 `queryWithPassThrough`, 用于处理缓存穿透问题并从缓存( Redis) 或数据库中查询数据并返回。
// `<R, ID, T>` 表示这是一个带有三个泛型参数的泛型方法,`R` 是最终期望返回的数据类型参数,其具体类型由调用者根据业务需求指定,比如可能是某个实体类类型,表示最终要返回给调用者的业务对象类型;`ID` 是用于唯一标识要查询数据的标识信息的类型参数,通常依据业务场景来确定,例如可以是整数类型作为数据库记录的主键等;`T` 则是在处理特定类型(如分页相关类型数据时涉及的列表元素类型等情况)时使用的类型参数,同样由业务场景决定,通过这三个泛型参数使得方法能够灵活适应多种不同类型数据的查询及处理需求。
// `keyPrefix` 参数是一个字符串类型,用于构建缓存数据在 Redis 中的键的前缀部分,按照业务模块、数据分类等规则来设置不同的前缀,有助于对缓存数据进行分类管理,方便准确地定位到特定范围的缓存项,后续它会和具体的标识 `id` 拼接成完整的 Redis 键,用于查找对应的缓存数据。
// `id` 参数就是前面提到的用于唯一标识要查询数据的标识信息,其类型由泛型参数 `ID` 确定,通过它与 `keyPrefix` 拼接生成准确的 Redis 键,以此来指向特定的缓存数据记录,实现对不同个体数据的缓存操作及查询定位。
// `type` 参数是 `Class<R>` 类型,用于明确告知方法期望返回的数据最终应该反序列化为什么具体的 Java 类型(通过传入对应的 `Class` 对象,例如 `User.class` 表示要返回的是 `User` 类型的数据),以便方法内部根据这个类型信息进行正确的反序列化操作,将从缓存或数据库获取到的数据转换为符合业务要求的对象类型后返回给调用者。
// `dbFallback` 参数是一个函数式接口 `Function<ID, R>` 类型,代表了一个接受 `ID` 类型参数并返回 `R` 类型结果的函数,在这里它用于在缓存未命中(即缓存中不存在对应数据)的情况下,从数据库中获取对应的数据的逻辑实现,一般可以通过 Lambda 表达式或者方法引用来传递具体的从数据库查询数据的实现方法,这体现了缓存穿透问题的一种解决策略,当缓存无法提供有效数据时,依靠数据库来获取数据,保证数据的完整性,避免直接返回空值给调用者。
// `voClass` 参数是 `Class<T>` 类型,在处理某些特定复杂数据结构(如分页数据等情况)时,用于指定列表中元素的具体类型,方便将缓存中获取到的 JSON 数据准确地转换为相应类型的列表元素,使得数据在反序列化和后续使用过程中类型是符合业务预期的,增强了方法对不同数据结构处理的灵活性和准确性。
// `time` 参数是 `Long` 类型,用于指定缓存数据的过期时间相关设置,结合 `TimeUnit` 参数来精确确定缓存数据在 Redis 中应该保持有效的时长,合理控制缓存的时效性,既能让缓存数据在一定时间内发挥作用提高查询性能,又能避免数据长期占用内存资源导致内存浪费等问题。
// `TimeUnit` 参数是 `TimeUnit` 类型,它是 Java 中用于表示时间单位的枚举类型(比如 `TimeUnit.SECONDS` 表示秒、`TimeUnit.MINUTES` 表示分钟等),与 `time` 参数配合使用,明确缓存数据的过期时长设置,根据业务场景中数据的更新频率、重要性等因素来选择合适的时间单位和时长进行配置,使得缓存过期时间的设置更贴合实际业务需求。
// 根据传入的键前缀( keyPrefix) 和具体的标识( id) 拼接成完整的Redis键, 用于后续在Redis中查找对应的缓存数据, 这种方式方便对不同类型、不同标识的数据进行统一的缓存管理, 通过键的规则来区分不同的缓存项。
String key = keyPrefix + id ;
// 通过简单的字符串拼接操作,将传入的键前缀 `keyPrefix` 和具体的标识 `id`( `id` 会根据其自身的 `toString` 方法转换为字符串形式参与拼接)组合成一个完整的 Redis 键 `key`,这个键在后续与 Redis 进行交互操作(如查询缓存数据、写入缓存数据等)时作为唯一的标识,依据不同的前缀和标识可以清晰地区分各种不同的缓存数据,便于进行统一的、有条理的缓存管理,确保能够准确地从 Redis 众多缓存数据中定位到目标数据。
// TODO 1. 从Redis查询商铺缓存
// 可以选择Hash结构, 没问题, 也能String
// 使用StringRedisTemplate的opsForValue操作对象的get方法, 根据前面生成的键( key) 从Redis中获取对应的缓存数据, 获取到的数据是以JSON字符串形式存在( 因为之前存入时进行了序列化操作) , 如果缓存中不存在该键对应的数据, 则返回null, 这里先尝试从缓存中获取数据, 以利用缓存来提升性能, 避免每次都直接访问数据库。
String json = stringRedisTemplate . opsForValue ( ) . get ( key ) ;
// 借助之前注入的 `stringRedisTemplate` 对象(它是 Spring Data Redis 提供的用于操作 Redis 的模板类)的 `opsForValue()` 方法获取操作 Redis 字符串值的相关操作对象,然后调用其 `get` 方法,传入前面生成的完整 Redis 键(`key`),尝试从 Redis 中获取对应的缓存数据。
// 由于在之前向 Redis 存入数据时,通常会先将 Java 对象进行序列化操作转换为 JSON 字符串后再存入(这是符合 Redis 存储字符串数据的要求以及方便后续反序列化还原数据的常见做法),所以这里通过 `get` 方法获取到的数据(如果存在的话)也会是以 JSON 字符串的形式返回。
// 若 Redis 中不存在该键对应的缓存数据,那么 `get` 方法就会返回 `null`,这个返回结果后续会用于判断缓存是否命中,进而依据不同的情况决定下一步的操作逻辑,例如是直接返回数据(缓存命中且数据有效时)、进行额外判断(缓存命中但数据为空值时)还是从数据库获取数据(缓存未命中时)等不同的处理方式,先从缓存中获取数据的目的在于利用缓存来提升整体的数据查询性能,避免每次都直接去访问相对较慢的数据库操作。
// TODO 2. 判断时Redis是否命中
if ( StrUtil . isNotBlank ( json ) ) {
// 使用 `StrUtil` 工具类(假设是 `Hutool` 工具库中的字符串工具类,用于方便地进行字符串相关操作和判断)的 `isNotBlank` 方法来判断获取到的 JSON 字符串(`json`)是否不为空。
// `isNotBlank` 方法会判断字符串是否不为 `null` 且长度大于 0 并且不只是包含空白字符(如空格、制表符等),如果满足这个条件,就认为字符串是有实际内容的,也就是缓存命中(获取到了有效的缓存数据),此时按照业务逻辑进入下面根据期望返回的数据类型进行不同处理的流程。
// 如果获取到的JSON字符串不为空( 即缓存命中) , 需要根据期望返回的数据类型( type) 进行不同的处理, 因为不同类型的数据在反序列化和返回时可能有不同的操作要求。
if ( type . equals ( IPage . class ) ) {
// List<T> dataList = JSONUtil.toList(JSONUtil.parseArray(), voClass);
// IPage<T> page = new Page<>();
// page.setRecords(dataList); // 假设Page类有此方法设置数据列表
// // 设置其他分页属性, 如total、current等, 这取决于IPage的具体实 施
// // 设置其他分页属性, 如total、current等, 这取决于IPage的具体实 现
// return (R) page; // 强制转换为 R 类型返回
// 解析JSON , 使用Hutool的JSONUtil工具类的parseObj方法将获取到的JSON字符串解析为JSONObject对象, 方便后续从中提取具体的属性值, JSONObject可以看作是一个类似Map结构的对象, 用于操作JSON数据中的键值对信息。
// 解析JSON
JSONObject jsonObject = JSONUtil . parseObj ( json ) ;
// 调用 `JSONUtil` 工具类(假设使用了相关的 JSON 处理工具库,用于方便地进行 JSON 数据与 Java 对象之间的转换操作)的 `parseObj` 方法,传入获取到的 JSON 字符串(`json`),这个方法会将 JSON 字符串解析为 `JSONObject` 类型的对象,`JSONObject` 在功能上类似 Java 中的 `Map`,可以方便地通过键来获取对应的值,便于后续从解析后的 JSON 数据中提取出分页数据相关的各个属性值,比如记录列表、总记录数、每页大小、当前页码等信息,为构建完整的 `IPage` 对象做准备。
// 获取记录列表并转换为List<T> , 通过从解析后的JSONObject对象中获取名为"records"的JSON数组( 对应IPage中的数据记录列表) , 再利用JSONUtil的toList方法将其转换为指定类型( voClass) 的列表, 这样就得到了分页数据中的实际记录列表数据, 用于后续构建IPage对象。
// 获取记录列表并转换为List<T>
List < T > dataList = JSONUtil . toList ( jsonObject . getJSONArray ( "records" ) , voClass ) ;
// 首先通过前面解析得到的 `JSONObject` 对象(`jsonObject`)调用 `getJSONArray` 方法获取名为 `"records"` 的 JSON 数组,这个数组对应着分页数据中的实际数据记录部分(在 `IPage` 数据结构中通常会有一个属性用于存储具体的数据记录列表)。
// 然后调用 `JSONUtil` 的 `toList` 方法,传入获取到的 JSON 数组以及指定的类型信息 `voClass`(用于明确列表中元素的具体类型),这个方法会将 JSON 数组中的每个元素按照 `voClass` 指定的类型进行反序列化转换,最终得到一个 `List<T>` 类型的列表对象 `dataList`,这个列表就包含了分页数据中的实际记录数据,后续可以将其设置到 `IPage` 对象中作为其数据记录部分。
// 创建Page对象并设置属性 , 创建一个新的Page对象( 这里假设Page类是MyBatis Plus中用于表示分页数据的类, 有相应的方法来设置分页相关的属性) , 先将前面获取到的记录列表设置到Page对象中, 然后分别从解析后的JSONObject对象中获取"total"(总记录数)、"size"(每页大小)、"current"( 当前页码) 等属性值, 并设置到Page对象相应的属性上, 这样就构建好了一个完整的IPage对象, 用于返回符合分页数据格式要求的结果。
// 创建Page对象并设置属性
IPage < T > page = new Page < > ( ) ;
page . setRecords ( dataList ) ; // 设置记录列表
page . setTotal ( jsonObject . getInt ( "total" ) ) ; // 设置总记录数
page . setSize ( jsonObject . getInt ( "size" ) ) ; // 设置每页大小
page . setCurrent ( jsonObject . getInt ( "current" ) ) ; // 设置当前页码
// 创建一个新的 `IPage<T>` 类型的对象 `page`(这里假设 `Page` 类是来自 MyBatis Plus 框架中用于表示分页数据结构的类,它提供了相应的方法来设置和操作分页相关的各种属性)。
// 首先调用 `page` 对象的 `setRecords` 方法,将前面获取并转换得到的包含实际记录数据的 `List<T>` 类型的 `dataList` 设置为 `page` 对象的记录列表属性,这样就填充了分页数据中的具体数据内容部分。
// 接着通过 `jsonObject`(前面解析 JSON 字符串得到的 `JSONObject` 对象)分别调用 `getInt` 方法获取 `"total"`(表示总记录数)、`"size"`(表示每页大小)、`"current"`(表示当前页码)等属性值,并依次调用 `page` 对象的 `setTotal`、`setSize`、`setCurrent` 等相应方法将这些属性值设置到 `page` 对象对应的属性上,通过这样一系列的操作,就构建好了一个完整的符合分页数据格式要求的 `IPage<T>` 对象,最后可以将其强制转换为泛型要求的 `R` 类型并返回给调用者,使得调用者能够获取到符合业务需求的分页数据格式的结果数据,用于后续的业务操作,比如展示分页列表等情况。
return ( R ) page ; // 强制转换为 R 类型返回
// 由于方法期望返回的类型是由泛型参数 `R` 指定的类型,而在这里构建好的是 `IPage<T>` 类型的对象 `page`,所以需要将 `page` 对象强制转换为 `R` 类型后再返回给调用者,这里的强制转换要求 `R` 类型在实际调用时必须是与 `IPage<T>` 类型兼容(比如 `R` 就是 `IPage<T>` 或者是它的父类等情况),否则会在运行时出现类型转换异常,通过这样的返回操作,将符合业务要求和分页数据格式的结果返回给调用者,完成在缓存命中且期望返回类型为 `IPage` 时的数据处理及返回流程。
} else if ( type . equals ( List . class ) ) {
} else if ( type . equals ( List . class ) ) {
// TODO 3. 存在,返回商户信息
// 如果期望返回的数据类型是List, 直接使用JSONUtil的toList方法将获取到的JSON字符串转换为指定类型( voClass) 的列表, 然后将其强制转换为泛型要求的类型( R) 并返回, 这样就将缓存中存储的JSON格式的数据转换为符合业务需求的列表类型数据返回给调用者。
List < T > gradeVOList = JSONUtil . toList ( json , voClass ) ;
// 调用 `JSONUtil` 的 `toList` 方法,传入获取到的 JSON 字符串(`json`)以及指定的类型信息 `voClass`(用于明确列表中元素的具体类型),这个方法会将 JSON 字符串中的数据按照 `voClass` 指定的类型进行反序列化转换,最终得到一个 `List<T>` 类型的列表对象 `gradeVOList`,这个列表就是从缓存中获取并转换后的符合业务需求的列表类型数据,可用于后续直接返回给调用者进行相应的业务操作,比如展示商户信息列表等情况。
return ( R ) gradeVOList ;
// 由于期望返回的类型是由泛型参数 `R` 指定的类型,而在这里得到的是 `List<T>` 类型的 `gradeVOList`,所以需要将 `gradeVOList` 强制转换为 `R` 类型后再返回给调用者,同样这里要求 `R` 类型在实际调用时必须是与 `List<T>` 类型兼容(比如 `R` 就是 `List<T>` 或者是它的父类等情况),否则会出现类型转换异常,通过这样的返回操作,将符合业务要求的列表类型数据返回给调用者,完成在缓存命中且期望返回类型为 `List` 时的数据处理及返回流程。
} else {
// 如果是其他普通的Java对象类型( 非IPage和List类型) , 则使用JSONUtil的toBean方法将JSON字符串直接反序列化为指定类型( type) 的Java对象, 然后返回该对象, 实现从缓存中的JSON数据到Java对象的转换, 方便后续在业务逻辑中使用该对象进行操作。
return ( R ) gradeVOList ;
} else {
return JSONUtil . toBean ( json , type ) ;
// 调用 `JSONUtil` 的 `toBean` 方法,传入获取到的 JSON 字符串(`json`)以及期望的业务数据类型(通过泛型参数 `type` 指定,传入对应的 `Class` 对象),这个方法会根据 JSON 字符串的内容以及指定的目标类型结构,将 JSON 数据反序列化为符合 `type` 指定类型的 Java 对象,并直接将其返回给调用者,使得调用者可以获取到符合业务要求的具体业务对象,方便在后续的业务逻辑中使用该对象进行诸如展示、修改、关联其他业务操作等各种操作,完成在缓存命中且期望返回的是普通 Java 对象类型时的数据处理及返回流程。
}
}
// TODO 多判断一步,命中的是否是空值
// 运行到这里, 说明上面的if没有进去, ->说明StrUtil.isNotBlank(shopJson)是false ->shopJson两种情况 空白字符串或者null
if ( json ! = null ) {
if ( json ! = null ) {
// 不能等于null, 就一定是一个空字符串
// 如果获取到的json字符串不是null, 而是一个空字符串, 说明之前缓存中存储的就是一个空值( 可能是数据库中对应数据不存在等原因导致写入了空值到缓存) , 此时按照业务逻辑直接返回null, 表示没有有效的数据可返回给调用者。
return null ;
// 当判断获取到的 `json` 字符串不为 `null` 但却是空字符串时,意味着之前缓存中存储的就是一个空值情况,这可能是由于之前在某些情况下(比如首次查询数据时数据库中对应的数据不存在,然后将空值写入了缓存)导致的。按照业务逻辑,在这种情况下直接返回 `null` 给调用者,表示当前没有有效的数据可以提供给调用者使用,调用者可以根据这个返回值在其业务代码中采取相应的措施,例如给用户展示相应的提示信息告知没有找到对应的数据等操作。
}
// TODO 4. 不存在,向数据库进行查询
// 如果缓存中没有命中( 即获取到的json为null) , 则调用传入的dbFallback函数式接口实现的方法( 通常是一个Lambda表达式或者具体的方法引用, 用于从数据库中获取对应的数据) , 传入对应的标识( id) 来查询数据库获取数据, 将数据库查询的结果存储在变量r中, 这里体现了缓存穿透的解决思路, 即当缓存中不存在数据时, 通过查询数据库来获取数据, 避免直接返回空值给调用者, 保证数据的完整性。
R r = dbFallback . apply ( id ) ;
// TODO 5. 数据库不存在,返回错误
if ( r = = null ) {
// 将空值写入redis
// 如果从数据库中查询到的数据也是null, 说明数据库中也不存在对应的数据, 此时将一个空字符串写入Redis( 设置一个较短的过期时间, 如2分钟, 避免长期占用缓存空间且后续如果数据库有数据更新了可以及时更新缓存) , 这样下次再查询时就不会一直穿透到数据库去查询, 而是直接从缓存中获取到这个空值返回, 然后返回null给调用者, 表示没有获取到有效的数据。
stringRedisTemplate . opsForValue ( ) . set ( key , "" , 2 , TimeUnit . MINUTES ) ;
// 返回错误信息
return null ;
}
// TODO 6. 存在,写入Redis
// 如果从数据库中查询到了有效数据( r不为null) , 先将数据转换为JSON字符串格式, 以便能够存入Redis中, 同样是利用JSONUtil的toJsonStr方法进行序列化操作, 准备将数据存入缓存, 实现数据的缓存更新, 方便下次查询时可以直接从缓存中获取数据, 提高查询性能。
String shopTOJson = JSONUtil . toJsonStr ( r ) ;
stringRedisTemplate . opsForValue ( ) .
set ( key , shopTOJson , time + random . nextInt ( 20 ) , unit ) ;
set ( key , shopTOJson , time + random . nextInt ( 20 ) , unit ) ;
// TODO 7. 返回最终结果
// 最后将从数据库中获取到的数据(已经序列化并存入缓存了)直接返回给调用者,完成整个查询并缓存数据的流程,使得调用者可以获取到期望的数据进行后续的业务操作。
return r ;
}
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors . newFixedThreadPool ( 10 ) ;
;
// 创建一个固定线程数量为10的线程池( ExecutorService) , 用于后续在缓存击穿处理中, 当缓存过期需要重建缓存时, 将缓存重建的任务提交到这个线程池中执行, 通过使用线程池可以更好地管理线程资源, 避免频繁创建和销毁线程带来的性能开销, 并且可以控制并发线程的数量, 保证系统的稳定性和资源的合理利用, 这里的缓存重建任务通常是指从数据库中重新获取数据并更新到缓存中的操作, 通过异步执行可以减少对主线程的阻塞, 提高系统的响应性能。
// 方法4: 解决缓存击穿
public < R , ID > R queryWithLogicalExpire ( String keyPrefix , ID id , Class < R > type , Function < ID , R > dbFallback , Long time , TimeUnit unit ) {
// 定义了一个泛型方法 `queryWithLogicalExpire`, 用于从缓存( Redis) 中查询数据, 并处理逻辑过期的情况。
// `<R, ID>` 表示这是一个泛型方法,其中 `R` 是期望返回的数据类型参数,由调用者根据实际业务需求指定具体类型,比如可能是某个实体类类型,表示要获取的具体业务对象类型;`ID` 则是用于标识要查询数据的唯一标识的类型参数,同样根据业务场景而定,例如可能是整数类型作为数据库记录的主键等,通过这两个泛型参数使得方法可以灵活适应不同类型数据的查询需求。
// `keyPrefix` 参数是一个字符串类型,用于构建缓存数据在 Redis 中的键的前缀部分,通常根据业务模块、数据分类等因素来设置不同的前缀,方便对缓存数据进行分类管理以及准确地定位到特定范围的缓存项,后续会和具体的标识 `id` 拼接成完整的 Redis 键来查找对应的缓存数据。
// `id` 参数就是前面提到的用于唯一标识要查询数据的标识信息,其类型由泛型参数 `ID` 确定,通过它与 `keyPrefix` 拼接能生成准确的 Redis 键,指向特定的缓存数据记录。
// `type` 参数是 `Class<R>` 类型,用于明确告知方法期望返回的数据最终应该反序列化为什么具体的 Java 类型(通过传入对应的 `Class` 对象,例如 `User.class` 表示要返回的是 `User` 类型的数据),以便方法内部根据这个类型信息进行正确的反序列化操作,将从缓存中获取到的数据转换为符合业务要求的对象类型返回给调用者。
// `dbFallback` 参数是一个函数式接口 `Function<ID, R>` 类型,它代表了一个接受 `ID` 类型参数并返回 `R` 类型结果的函数,在这里用于在缓存未命中或者缓存数据过期等情况下,从数据库中获取对应的数据的逻辑实现,通常可以通过 Lambda 表达式或者方法引用来传递具体的从数据库查询数据的实现方法,体现了缓存与数据库交互的一种策略,即当缓存无法提供有效数据时,依靠数据库来获取数据,保证数据的可用性。
// `time` 参数是 `Long` 类型,用于指定缓存数据的过期时间相关设置,具体的含义和使用方式取决于后续的业务逻辑以及与 `TimeUnit` 参数配合来确定缓存数据在多长时间后应该被视为过期(可能是逻辑过期的时间设置等情况),以便对缓存的时效性进行合理控制。
// `TimeUnit` 参数是 `TimeUnit` 类型,它是 Java 中用于表示时间单位的枚举类型(如 `TimeUnit.SECONDS` 表示秒、`TimeUnit.MINUTES` 表示分钟等),与 `time` 参数配合使用,精确地确定缓存数据的过期时长设置,使得缓存过期时间的设置更符合业务场景的实际需求,可根据数据的特性、业务操作频率等因素来选择合适的时间单位和时长进行配置。
String key = keyPrefix + id ;
// 根据传入的键前缀(`keyPrefix`)和具体的标识(`id`)拼接成完整的 Redis 键(`key`),用于后续在 Redis 中查找对应的缓存数据。这种通过前缀和具体标识组合成键的方式,方便对不同类型、不同标识的数据进行统一的缓存管理,依据键的规则可以清晰地区分不同的缓存项,确保能够准确地从 Redis 众多缓存数据中定位到目标数据,这是与 Redis 进行数据交互操作的基础步骤,后续的获取缓存、判断缓存状态等操作都依赖这个正确生成的键。
// TODO 1.从redis查询商铺缓存
// 使用StringRedisTemplate的opsForValue操作对象的get方法, 根据生成的键( key) 从Redis中获取缓存数据( 以JSON字符串形式存储的, 之前通过设置逻辑过期的方式存入的数据) , 如果缓存中不存在对应的数据则返回null, 这里首先尝试从缓存中获取数据, 以便后续判断缓存是否命中以及是否过期等情况, 进而采取相应的处理措施。
String json = stringRedisTemplate . opsForValue ( ) . get ( key ) ;
// 通过之前注入的 `stringRedisTemplate` 对象(它是 Spring Data Redis 提供的用于操作 Redis 的模板类)的 `opsForValue()` 方法获取操作 Redis 字符串值的相关操作对象,然后调用其 `get` 方法,传入前面生成的完整 Redis 键(`key`),尝试从 Redis 中获取对应的缓存数据。
// 由于之前存入 Redis 时(在使用逻辑过期策略存储数据的相关方法中)是将数据转换为 JSON 字符串后存入的,所以这里获取到的数据(如果存在的话)也会是以 JSON 字符串的形式返回,若 Redis 中不存在该键对应的缓存数据,则 `get` 方法会返回 `null`,这个返回结果后续会用于判断缓存是否命中,进而决定下一步的操作逻辑,比如是直接返回数据、进行缓存重建还是从数据库获取数据等不同的处理方式。
// TODO 2. 判断时Redis是否命中
if ( StrUtil . isBlank ( json ) ) {
// 使用 `StrUtil` 工具类(假设是 `Hutool` 工具库中的字符串工具类,用于方便地进行字符串相关操作和判断)的 `isBlank` 方法来判断获取到的 JSON 字符串(`json`)是否为空。
// `isBlank` 方法会判断字符串是否为 `null`、空字符串(长度为 0) 或者只包含空白字符( 如空格、制表符等) , 如果满足这些情况之一, 就认为字符串是空白的, 也就是缓存未命中( 没有获取到有效的缓存数据) , 此时按照业务逻辑进入下面的处理流程。
// TODO 3.缓存不存在,直接返回空
// 如果获取到的JSON字符串为空( 即缓存未命中) , 按照业务逻辑直接返回null, 表示没有从缓存中获取到有效的数据, 调用者可以根据这个返回值进行相应的处理( 比如提示用户数据不存在等情况) 。
return null ;
// 当判断缓存未命中(`json` 为空)时,直接返回 `null` 给调用者,这符合业务逻辑中对于缓存不存在情况的处理方式,调用者可以根据这个返回值在其业务代码中采取相应的措施,例如给用户展示相应的提示信息,告知用户请求的数据暂时不存在或者需要重新加载等操作,同时也避免了在缓存未命中情况下进行不必要的后续复杂操作,简化了处理流程。
}
// TODO 4.存在,需要先把JSON反序列化为对象
// 使用JSONUtil的toBean方法将获取到的JSON字符串反序列化为RedisData对象, 因为之前存入Redis时是将包含业务数据和逻辑过期时间等信息的RedisData对象转换为JSON字符串后存入的, 所以这里需要先反序列化还原出RedisData对象, 以便后续获取其中的业务数据以及判断逻辑过期时间等操作, 从缓存数据中提取出关键的信息用于后续的逻辑处理。
RedisData redisData = JSONUtil . toBean ( json , RedisData . class ) ;
// 调用 `JSONUtil` 工具类(假设使用了相关的 JSON 处理工具库,用于方便地进行 JSON 数据与 Java 对象之间的转换操作)的 `toBean` 方法,传入获取到的 JSON 字符串(`json`)以及 `RedisData.class`(表示要将 JSON 字符串反序列化为 `RedisData` 类型的对象),这个方法会根据 JSON 字符串的内容以及 `RedisData` 类的结构定义,将 JSON 数据转换为对应的 `RedisData` 类型的 Java 对象,并将其赋值给 `redisData` 变量。
// 因为之前在将数据存入 Redis 时(在相关的设置缓存逻辑过期的方法中),是把包含业务数据和逻辑过期时间等信息的 `RedisData` 对象先转换为 JSON 字符串后再存入的,所以现在从 Redis 获取到数据( JSON 字符串形式)后,需要通过这个反序列化操作还原出 `RedisData` 对象,这样才能进一步获取其中封装的业务数据以及逻辑过期时间等关键信息,为后续判断缓存数据是否过期以及提取正确的业务数据返回给调用者等操作做好准备。
// 因为我们在RedisData中设置data属性就是Object类型, 所以当我们取的时候程序并不知道我们是什么类型, 我们加一个强转就好了
// 从RedisData对象中获取存储业务数据的data属性, 由于其类型为Object, 在实际使用时需要将其强制转换为JSONObject类型( 这里假设业务数据在存入时是以JSONObject形式封装在RedisData中的, 方便操作其中的键值对数据) , 以便后续能准确地将其转换为期望的业务数据类型( type) , 这里体现了在处理通用数据结构时, 需要根据实际存储和使用的情况进行类型的转换和适配操作。
JSONObject shopData = ( JSONObject ) redisData . getData ( ) ;
// 从前面反序列化得到的 `RedisData` 对象中获取 `data` 属性,由于在 `RedisData` 类的定义中,`data` 属性的类型被设置为 `Object`(这样设计是为了能够存储各种不同类型的业务数据,具有通用性),所以从这个属性获取到的值在程序中默认被当作 `Object` 类型对待。
// 但在实际业务操作中,为了方便后续准确地将其转换为期望的业务数据类型(由泛型参数 `type` 指定的类型),这里假设业务数据在存入时是以 `JSONObject` 形式(一种方便操作键值对数据的 JSON 数据结构表示形式,类似 Java 中的 `Map`,便于获取和设置具体的属性值)封装在 `RedisData` 中的,所以需要将获取到的 `Object` 类型的 `data` 属性值强制转换为 `JSONObject` 类型,并将其赋值给 `shopData` 变量,通过这样的类型转换和适配操作,才能更好地利用其中存储的业务数据进行后续的处理,比如提取具体的业务属性值等操作。
R r = JSONUtil . toBean ( shopData , type ) ;
// 调用 `JSONUtil` 的 `toBean` 方法,将前面转换得到的 `JSONObject` 类型的 `shopData`(其中包含了实际的业务数据信息)以及期望的业务数据类型(通过泛型参数 `type` 指定,传入对应的 `Class` 对象)作为参数传入,这个方法会根据 `JSONObject` 中的数据内容以及指定的目标类型结构,将 `shopData` 中的数据反序列化为符合 `type` 指定类型的 Java 对象,并将其赋值给变量 `r`,这样就得到了最终可以在业务中使用的具体业务数据对象,方便后续根据缓存是否过期等情况来决定是直接返回该数据还是进行其他相关操作(如缓存重建后再返回等)。
LocalDateTime expireTime = redisData . getExpireTime ( ) ;
// 从前面反序列化得到的 `RedisData` 对象中获取 `expireTime` 属性,这个属性存储的是缓存数据的逻辑过期时间(在之前将数据存入 Redis 时,通过设置逻辑过期的相关操作设置了这个时间,它是一个 `LocalDateTime` 类型的时间点,表示缓存数据在逻辑上应该过期的时间),将其赋值给 `expireTime` 变量,以便后续通过与当前时间进行比较,来判断缓存数据是否已经过期,进而决定相应的业务处理逻辑,例如是直接返回数据还是进行缓存重建等操作。
// TODO 5.判断是否过期
if ( expireTime . isAfter ( LocalDateTime . now ( ) ) ) {
// 使用获取到的缓存数据的逻辑过期时间(`expireTime`)与当前时间(通过 `LocalDateTime.now()` 获取当前系统的时间点)进行比较,调用 `isAfter` 方法判断逻辑过期时间是否在当前时间之后。
// 如果 `expireTime` 所表示的时间点在当前时间之后,意味着缓存数据的逻辑过期时间还未到达,即缓存数据还未过期,仍然有效,此时按照业务逻辑进入下面的处理流程,直接将之前反序列化得到的业务数据(`r`)返回给调用者,使得调用者可以使用缓存中的有效数据进行后续业务操作,避免了不必要的数据库查询操作,提高了查询性能。
// TODO 5.1 未过期,返回商铺信息
// 如果缓存数据的逻辑过期时间( expireTime) 在当前时间之后, 说明缓存数据还未过期, 仍然有效, 直接将之前反序列化得到的业务数据( r) 返回给调用者, 使得调用者可以使用缓存中的有效数据进行后续业务操作, 避免了不必要的数据库查询操作, 提高了查询性能。
return r ;
// 当判断缓存数据未过期(`expireTime` 在当前时间之后)时,直接将前面已经反序列化得到的符合业务要求的业务数据对象(`r`)返回给调用者,这样调用者就可以使用这个有效的缓存数据在其业务逻辑中进行后续的操作,比如展示数据、进行业务计算等。
// 通过直接返回缓存中的有效数据,避免了再次去数据库中查询相同数据的操作,减少了数据库的访问压力,同时提高了整个数据查询的性能,充分发挥了缓存的作用,使得系统在处理频繁查询相同数据的场景下能够更高效地运行。
}
// TODO 5.2 已过期,需要缓存重建
// 当判断缓存数据已经逻辑过期后,需要进行缓存重建操作,即从数据库中重新获取最新的数据并更新到缓存中,以保证缓存数据的有效性和及时性,同时要考虑并发情况下的缓存一致性等问题,避免多个线程同时重建缓存造成的数据不一致或者资源浪费等情况,下面的步骤就是围绕缓存重建这个过程展开的相关操作。
// TODO 6. 缓存重建
// TODO 6.1 获取互斥锁
// 构建一个用于表示互斥锁的键( lockKey) , 其格式通常是固定的( 这里采用"lock:shop:"加上具体的标识id来构建, 方便区分不同数据对应的锁, 避免锁的冲突) , 通过调用tryLock方法尝试获取这个互斥锁,
String lockKey = "lock:shop:" + id ;
// 构建一个用于表示互斥锁的键( lockKey) , 其格式通常是固定的( 这里采用"lock:shop:"加上具体的标识id来构建, 方便区分不同数据对应的锁, 避免锁的冲突) , 通过这个唯一的键来在Redis中标识和操作对应的互斥锁, 后续基于这个键来进行获取锁、释放锁等操作, 以实现对缓存重建过程的并发控制, 保证在同一时刻只有一个线程能够进行缓存重建操作, 避免多个线程同时重建缓存造成的数据不一致或者资源浪费等情况。
boolean isLock = tryLock ( lockKey ) ;
// 调用tryLock方法尝试获取这个互斥锁, tryLock方法会与Redis进行交互, 尝试在Redis中设置一个具有特定键( 这里就是刚刚构建的lockKey) 的锁, 返回一个布尔值表示是否成功获取到锁, 获取到锁意味着当前线程获得了对缓存重建操作的独占权限, 后续可以安全地进行缓存重建相关的操作; 若获取锁失败, 则说明可能已经有其他线程正在进行缓存重建, 当前线程需要等待或者采取其他相应的处理策略, 通过这种方式利用锁机制来协调多个线程对缓存重建的并发访问。
// TODO 6.2 判断是否获取锁成功
// TODO 6.2 判断是否获取锁成功
if ( isLock ) {
// 如果成功获取到锁( isLock为true) , 说明当前线程获得了进行缓存重建的权限, 接下来需要再次检测Redis缓存是否过期, 这一步骤被称为“DoubleCheck”( 双重检查) , 主要目的是防止在获取锁的短暂时间间隔内, 其他线程已经完成了缓存重建并更新了缓存, 使得当前线程无需再重复进行缓存重建操作, 进一步优化性能并保证缓存数据的准确性和一致性。
// 再次从Redis中获取缓存数据对应的JSON字符串, 这里的键( "cache:shop:" + id) 可能是用于标识该缓存数据在Redis中的存储位置的另一种形式( 具体取决于项目中的缓存键设计规范) , 通过重新获取数据来进行后续的过期判断等操作, 确保基于最新的缓存状态来决定是否真正需要进行缓存重建工作。
// TODO 6.3 成功, 获取锁成功应该再次检测Redis缓存是否过期, 做DoubleCheck, 如果存在则无序重建缓存
json = stringRedisTemplate . opsForValue ( ) . get ( "cache:shop:" + id ) ;
// 将获取到的JSON字符串反序列化为RedisData对象, 与之前从缓存中获取数据并反序列化的操作类似, 目的是提取出其中包含的业务数据以及逻辑过期时间等关键信息, 以便后续判断缓存是否过期以及获取正确的业务数据进行返回等操作。
redisData = JSONUtil . toBean ( json , RedisData . class ) ;
// 从RedisData对象中获取存储业务数据的data属性, 并强制转换为JSONObject类型, 方便后续准确地将其转换为期望的业务数据类型( type) , 进行相应的业务操作或者判断操作, 这是对缓存中存储的数据结构进行适配和提取有效信息的必要步骤。
shopData = ( JSONObject ) redisData . getData ( ) ;
// 将从JSONObject中提取出的数据反序列化为期望的业务数据类型( type) , 得到最终可以在业务中使用的对象, 方便后续根据缓存是否过期等情况来决定是直接返回该数据还是进行缓存重建后再返回等操作。
r = JSONUtil . toBean ( shopData , type ) ;
expireTime = redisData . getExpireTime ( ) ;
if ( expireTime . isAfter ( LocalDateTime . now ( ) ) ) {
// 如果缓存数据的逻辑过期时间( expireTime) 在当前时间之后, 说明在获取锁后再次检查发现缓存数据仍然未过期, 仍然有效, 直接将之前反序列化得到的业务数据( r) 返回给调用者, 使得调用者可以使用缓存中的有效数据进行后续业务操作, 避免了不必要的数据库查询和缓存重建操作, 提高了查询性能, 同时也保证了数据的及时性和准确性。
// TODO 未过期,返回商铺信息
return r ;
}
// TODO 成功,但是缓存过期了,开启独立线程,实现缓存重建(建议使用线程池)
// 当经过双重检查确认缓存确实已经过期后, 将缓存重建的任务提交到之前创建的固定线程数量为10的线程池( CACHE_REBUILD_EXECUTOR) 中执行, 通过使用线程池可以实现异步操作, 避免缓存重建过程阻塞主线程, 提高系统的并发处理能力和响应性能。
// 这里使用Lambda表达式创建一个Runnable任务, 在任务内部实现缓存重建的具体逻辑, 即先从数据库中查询最新的数据, 再将带有逻辑过期时间设置的数据写入到Redis缓存中, 完成缓存的更新操作, 同时在任务执行完毕后( 无论是否出现异常) , 都需要通过调用unlock方法来释放之前获取的互斥锁, 以允许其他等待的线程有机会获取锁并进行缓存相关的操作, 保证锁资源的合理使用和并发控制的有效性。
CACHE_REBUILD_EXECUTOR . submit ( ( ) - > {
try {
// TODO 重建缓存 先查数据库, 再写入Redis
// 调用传入的dbFallback函数式接口实现的方法( 通常是一个Lambda表达式或者具体的方法引用, 用于从数据库中获取对应的数据) , 传入对应的标识( id) 来查询数据库获取最新的数据, 将数据库查询的结果存储在变量r1中, 这一步是获取最新的业务数据, 为后续更新缓存做准备, 体现了缓存重建时从数据源( 数据库) 获取最新数据的关键操作。
R r1 = dbFallback . apply ( id ) ;
// TODO 写入缓存要带有逻辑过期
// 调用本类的setWithLogicalExpire方法, 将从数据库中获取到的最新数据( r1) 按照带有逻辑过期时间的方式写入到Redis缓存中, 其中设置的过期时间是在传入的固定过期时间( time) 基础上加上一个随机时长( random.nextInt(20)),这样做可以进一步分散缓存过期时间,避免大量缓存同时过期引发缓存雪崩等问题,实现更合理、更稳定的缓存更新操作,保证缓存系统的可靠性和数据的时效性。
this . setWithLogicalExpire ( key , r1 , time + random . nextInt ( 20 ) , unit ) ;
this . setWithLogicalExpire ( key , r1 , time + random . nextInt ( 20 ) , unit ) ;
} catch ( Exception e ) {
// 如果在缓存重建过程( 包括数据库查询或者写入Redis操作) 中出现异常, 将异常包装在一个RuntimeException中抛出, 这样可以使得异常能够在合适的地方被捕获和处理( 例如在调用queryWithLogicalExpire方法的上层业务逻辑中可以根据具体情况进行日志记录、返回错误信息给客户端等操作) , 保证系统在出现异常情况时也能有相应的处理机制, 维持一定的稳定性。
throw new RuntimeException ( e ) ;
} finally {
// 释放锁
// 在缓存重建任务执行完毕后, 无论是否出现异常, 都需要调用unlock方法来释放之前获取的互斥锁( lockKey对应的锁) , 使得其他等待获取该锁的线程能够有机会获取锁并进行相应的缓存操作, 保证锁资源的合理循环使用, 维持并发环境下缓存操作的有序性和正确性。
unlock ( lockKey ) ;
}
} ) ;
}
// TODO 6.4 失败,返回已经过期的商品信息
// 如果获取锁失败( isLock为false) , 说明当前线程没有获得缓存重建的权限, 可能已经有其他线程正在进行缓存重建操作, 此时直接将之前已经获取到的( 虽然已经过期) 业务数据( r) 返回给调用者, 这样调用者可以先使用这个过期的数据进行一些展示或者其他不依赖最新数据的操作( 具体根据业务需求而定) , 同时也避免了当前线程长时间等待或者重复尝试获取锁等不必要的操作, 在一定程度上保证了系统的响应性能和可用性。
// TODO 6.4 失败,返回已经过期的商品信息
return r ;
}
// 拿到锁
private boolean tryLock ( String key ) {
// setIfAbsent方法就是Redis中的setnx
// 解释了Java中通过StringRedisTemplate操作Redis的opsForValue().setIfAbsent方法与Redis原生的SETNX命令是等效的功能, SETNX命令( SET if Not eXists) 用于在Redis中设置一个键值对, 但只有当键不存在时才会设置成功, 如果键已经存在则设置操作不会执行, 返回结果用于表示设置操作是否成功。
// 在Redis命令行中的运行结果就是0或者1( 0表示设置失败, 因为键已存在; 1表示设置成功) , 但是在Java代码中通过Spring Data Redis操作Redis时, 其返回的是Boolean类型( true表示设置成功, false表示设置失败) , 这里的Boolean类型是对底层Redis命令返回结果的一种封装, 方便在Java代码中进行逻辑判断和处理。
// 在Redis命令行中的运行结果就是0或者1, 但是在这的运行结果是true或false, 但是返回的是Boolean类型, 封装类
Boolean flag = stringRedisTemplate . opsForValue ( ) . setIfAbsent ( key , "1" , 10 , TimeUnit . SECONDS ) ;
// 不建议直接返回:会自动拆箱,有时候会出现空指针
// 如果直接返回flag( 即直接返回stringRedisTemplate.opsForValue().setIfAbsent的返回结果) , 在Java中会进行自动拆箱操作( 将Boolean对象转换为基本数据类型boolean) , 当flag为null时( 例如Redis连接出现问题等异常情况导致返回结果无法正常获取时) , 自动拆箱就会抛出空指针异常, 所以这里通过调用BooleanUtil.isTrue方法来进行判断并返回, 避免了自动拆箱可能带来的空指针问题, 提高代码的健壮性, 确保返回的结果是经过合理判断和处理后的正确布尔值, 表示是否成功获取到锁。
return BooleanUtil . isTrue ( flag ) ;
}
// 释放锁
private void unlock ( String key ) {
// 通过调用StringRedisTemplate的delete方法, 根据传入的键( key) 来删除Redis中对应的锁记录, 实现释放锁的操作, 使得其他线程后续可以尝试获取该锁进行相应的缓存操作, 完成对互斥锁资源在Redis中的删除和释放, 保证锁资源的正确管理和循环使用, 维持并发环境下缓存操作的正常秩序。
stringRedisTemplate . delete ( key ) ;
}
// public Map<Integer, GradeVO> batchGet(List<Integer> gradeIds, Class<GradeVO> gradeVOClass) {
//
// }
// public Map<Integer, GradeVO> batchGet(List<Integer> gradeIds, Class<GradeVO> gradeVOClass) {
//
// }
/ * *
* 批 量 从 Redis 获 取 缓 存 对 象
* 该 方 法 用 于 批 量 从 Redis 中 获 取 缓 存 对 象 , 接 收 缓 存 键 列 表 ( keys 参 数 , 类 型 为 List < Integer > , 但 实 际 在 Redis 中 键 是 字 符 串 类 型 , 所 以 需 要 进 行 类 型 转 换 ) 以 及 缓 存 对 象 的 期 望 类 型 ( clazz 参 数 ) 作 为 参 数 , 返 回 一 个 包 含 键 值 对 的 Map , 其 中 键 为 原 始 传 入 的 Integer 类 型 的 键 , 值 为 反 序 列 化 后 的 对 应 类 型 ( clazz 指 定 的 类 型 ) 的 缓 存 对 象 , 未 找 到 的 键 对 应 的 键 值 对 不 会 包 含 在 返 回 的 Map 中 , 方 便 一 次 性 获 取 多 个 缓 存 数 据 进 行 后 续 的 业 务 操 作 , 提 高 获 取 缓 存 数 据 的 效 率 。
* @param keys 缓 存 键 列 表 ( 注 意 这 里 的 key 类 型 为 Integer , 实 际 Redis 中 key 为 String , 因 此 在 使 用 时 需 要 转 换 )
* @param clazz 缓 存 对 象 的 类 型
* @return 包 含 键 值 对 的 Map , 未 找 到 的 键 不 会 包 含 在 内
* /
public < T > Map < Integer , T > batchGet ( String prefix , List < Integer > keys , Class < T > clazz ) {
// 定义了一个泛型方法 `batchGet`,用于从 Redis 中批量获取缓存对象。
// `<T>` 表示这是一个泛型方法,`T` 是一个类型参数,代表要获取的缓存对象的具体类型,这个类型由调用者在调用该方法时指定,使得方法可以灵活地返回不同类型的缓存对象集合,以适应各种业务场景下对不同类型缓存数据的获取需求。
// `prefix` 参数是一个字符串类型,用于给要获取的 Redis 键添加一个统一的前缀。在实际应用中,可能会根据不同的业务模块、数据分类等设置不同的前缀,通过传入这个前缀,可以准确地定位到属于特定范围的一批缓存键,便于从 Redis 中筛选出期望的缓存数据,增强了缓存管理的灵活性和可区分性。
// `keys` 参数是一个 `List<Integer>` 类型,代表了一组整数类型的键,这些键是用于在 Redis 中标识要获取的缓存数据的标识信息,但由于 Redis 的键要求是字符串类型,所以后续需要对这些整数键进行类型转换处理,使其符合 Redis 的键格式要求,这个列表包含了所有需要批量获取缓存数据对应的键信息,通过遍历这个列表来逐个获取对应的缓存对象。
// `clazz` 参数是 `Class<T>` 类型,它用于指定要获取的缓存对象最终反序列化后的具体类型,通过传入对应的类型信息(例如 `User.class` 表示要获取的缓存对象是 `User` 类型),方法内部就能根据这个类型信息将从 Redis 中获取到的缓存数据(通常是以 JSON 字符串形式存储的)准确地反序列化为期望的 Java 对象类型,方便后续在业务逻辑中直接使用这些对象进行相应的操作,确保获取到的数据类型符合业务要求。
try {
// 使用 `try-catch` 语句块来捕获在批量获取缓存数据过程中可能出现的异常情况,将可能出现异常的代码逻辑放在 `try` 块中执行,一旦发生异常,就会被 `catch` 块捕获并进行相应的处理,这样可以保证程序在遇到异常时不会意外终止,而是按照预设的异常处理逻辑进行应对,维持系统的稳定性,避免因个别异常导致整个系统崩溃或者出现不可预期的行为,提高系统的健壮性。
// 将Integer类型的keys转换为String类型, 并加上前缀, 因为Redis的key是字符串
// 使用Java 8的Stream API对传入的整数类型的键列表( keys) 进行操作, 通过map方法将每个整数键转换为带有指定前缀( prefix) 的字符串形式, 然后使用collect方法将转换后的字符串键收集到一个新的List<String>中, 这样就完成了将整数键转换为符合Redis键要求的字符串键的操作, 方便后续基于这些字符串键从Redis中批量获取缓存数据。
List < String > stringKeys = keys . stream ( )
. map ( key - > prefix + key . toString ( ) )
. collect ( Collectors . toList ( ) ) ;
// 首先调用 `keys` 列表的 `stream()` 方法,将 `List<Integer>` 类型的键列表转换为一个 `Stream<Integer>` 类型的流对象,这样就可以使用流的各种操作方法来处理数据。
// 接着使用 `map` 操作,它会对流中的每个元素(即每个整数键)应用一个给定的函数(这里是通过 Lambda 表达式 `key -> prefix + key.toString()` 表示的函数,其作用是将每个整数键 `key` 转换为带有指定前缀 `prefix` 的字符串形式),实现对每个整数键的类型转换和前缀添加操作,得到一个新的 `Stream<String>` 类型的流,其中每个元素都是符合 Redis 键要求的字符串键形式。
// 最后使用 `collect` 方法结合 `Collectors.toList()` 收集器,将经过 `map` 操作后的流中的所有字符串键元素收集起来,转换为一个 `List<String>` 类型的列表对象 `stringKeys`,这个列表就包含了所有准备好用于从 Redis 中批量获取缓存数据的字符串键,后续就可以基于这些键来与 Redis 进行交互获取对应的缓存内容。
// 执行批量获取操作
// 使用StringRedisTemplate的opsForValue操作对象的multiGet方法, 传入转换后的字符串键列表( stringKeys) , 一次性从Redis中获取多个键对应的缓存数据, 返回一个List<String>类型的结果列表, 其中每个元素对应一个键的缓存数据( 如果键不存在则对应位置为null) , 通过这种批量获取的方式可以减少与Redis的交互次数, 提高获取缓存数据的效率, 尤其在需要获取多个缓存数据的场景下性能优势明显。
List < String > values = stringRedisTemplate . opsForValue ( ) . multiGet ( stringKeys ) ;
// 通过之前注入的 `stringRedisTemplate` 对象(它是 Spring Data Redis 提供的用于操作 Redis 的模板类)的 `opsForValue()` 方法获取操作 Redis 字符串值的相关操作对象,然后调用其 `multiGet` 方法,这个方法用于一次性获取多个键对应的缓存数据。
// 将前面生成的包含所有字符串键的 `stringKeys` 列表作为参数传入 `multiGet` 方法, Redis 会根据这些键去查找对应的缓存数据,返回一个 `List<String>` 类型的结果列表 `values`,列表中的每个元素对应着传入的键列表中相应位置键所对应的缓存数据,如果某个键在 Redis 中不存在对应的缓存数据,那么在 `values` 列表中对应位置就会是 `null`,通过这种批量获取的方式,相比逐个获取键对应的缓存数据,可以显著减少与 Redis 的交互次数,降低网络开销等,在需要获取多个缓存数据的场景下能有效提高获取数据的效率,提升系统的整体性能。
Map < Integer , T > resultMap = new HashMap < > ( ) ;
// 创建一个 `HashMap` 类型的空映射对象 `resultMap`,用于存储最终从 Redis 中批量获取到的缓存数据,键的类型为 `Integer`(对应原始传入的整数类型的键),值的类型为泛型 `T`(即前面通过 `clazz` 参数指定的要获取的缓存对象的类型),通过后续的操作,会将从 Redis 中获取到并反序列化后的缓存对象按照键值对的形式存入这个映射中,最终返回给调用者,方便调用者根据原始的整数键来获取对应的缓存对象进行后续的业务操作。
for ( int i = 0 ; i < stringKeys . size ( ) ; i + + ) {
// 开始一个循环,循环次数由 `stringKeys` 列表的大小决定(也就是前面转换后的字符串键的数量),通过遍历这个列表的索引,依次处理每个键对应的缓存数据,确保能对所有批量获取到的缓存数据进行相应的检查和处理,将有效的缓存数据存入结果映射中,完成批量获取缓存数据并整理的操作流程。
String value = values . get ( i ) ;
// 从前面通过 `multiGet` 方法获取到的缓存数据列表 `values` 中,根据当前循环的索引 `i` 获取对应位置的缓存数据(以字符串形式存在,如果对应键在 Redis 中不存在缓存数据,则此处获取到的就是 `null`),将其存储在变量 `value` 中,方便后续判断该缓存数据是否存在以及进行反序列化等操作。
if ( value ! = null ) {
// 如果获取到的某个缓存数据( value) 不为null, 说明Redis中存在对应键的缓存数据, 此时调用deserialize方法( 本类中自定义的用于将JSON字符串反序列化为指定类型对象的方法) , 将缓存数据的JSON字符串( value) 反序列化为期望的类型( clazz指定的类型) 的对象, 以便后续可以在业务逻辑中直接使用该对象进行操作。
if ( value ! = null ) {
// 反序列化字符串为对象
T deserializedObject = deserialize ( value , clazz ) ;
// 调用 `deserialize` 方法,传入当前获取到的非空的缓存数据字符串 `value` 和指定的类型信息 `clazz`,这个 `deserialize` 方法(其具体实现应该是根据所采用的序列化/反序列化机制,例如使用 JSON 相关库来将 JSON 字符串转换为指定类型的 Java 对象)会将缓存数据的 JSON 字符串反序列化为符合 `clazz` 指定类型的 Java 对象,并将其存储在变量 `deserializedObject` 中,这个对象就是从 Redis 中获取并成功反序列化后的缓存对象,后续可以将其存入结果映射中用于返回给调用者使用。
// 将反序列化后的对象放入结果Map中, 键为原始传入的整数类型的键( keys.get(i)) , 值为反序列化后的对象, 这样就构建好了一个包含从Redis中批量获取到的缓存数据的键值对Map, 方便后续根据键来获取对应的缓存对象进行业务处理。
resultMap . put ( keys . get ( i ) , deserializedObject ) ;
// 将前面反序列化得到的缓存对象 `deserializedObject` 存入 `resultMap` 中,键使用原始传入的整数类型的键列表 `keys` 中对应位置的键(通过 `keys.get(i)` 获取),这样就构建好了一个键值对,将从 Redis 中获取到的缓存数据以符合业务要求的键值对形式整理到 `resultMap` 中,随着循环的进行,所有有效的缓存数据都会依次存入这个映射,最终形成一个包含从 Redis 中批量获取到的所有缓存数据的完整映射,方便后续根据原始的整数键来获取对应的缓存对象进行各种业务操作,如展示数据、进行数据处理等。
}
}
return resultMap ;
// 在完成对所有批量获取到的缓存数据的处理(将有效的缓存数据反序列化并存入 `resultMap` 中)后,将这个包含了键值对形式的缓存数据的 `resultMap` 返回给调用者,调用者可以根据需要使用这个映射来获取对应的缓存对象,实现了从 Redis 中批量获取缓存数据并转换为合适的业务对象形式返回的功能,满足了批量获取缓存数据用于后续业务操作的业务需求。
} catch ( Exception e ) {
// 异常处理逻辑,根据需要进行日志记录或抛出异常
// 如果在批量获取缓存数据的过程中出现任何异常( 例如Redis连接异常、反序列化异常等) , 将异常包装在一个RuntimeException中并抛出, 同时可以根据具体业务需求在合适的地方( 比如调用batchGet方法的上层业务逻辑) 捕获这个异常进行相应的日志记录、返回错误信息给客户端等操作, 保证系统在出现异常情况时有相应的处理机制, 维持一定的稳定性和可靠性。
throw new RuntimeException ( "Failed to batch get from Redis" , e ) ;
// 在 `try` 块中如果出现异常,会立即跳转到 `catch` 块中执行这里的代码。将出现的异常 `e` 包装在一个 `RuntimeException` 中,并添加一个自定义的错误信息 `"Failed to batch get from Redis"`,这样做一方面可以在不改变原有异常信息的基础上,添加更明确的业务相关的错误提示,方便后续排查问题时快速定位到是在批量获取 Redis 缓存数据这个环节出现的异常;另一方面,将包装后的异常抛出,使得调用这个 `batchGet` 方法的上层业务逻辑可以捕获到这个异常,然后根据具体业务需求进行相应的处理,比如记录日志、向客户端返回合适的错误信息等操作,通过这种异常处理机制,确保系统在面对各种异常情况时能够有相应的应对措施,维持系统的稳定性和可靠性,避免因异常导致系统出现不可预期的行为或者直接崩溃。
}
}
@ -429,77 +246,39 @@ public class CacheClient {
* /
private static < T > T deserialize ( String value , Class < T > clazz ) {
// 这里需要根据您的序列化方式来实现, 例如使用JSON库如Jackson、Gson等
// 指出了当前的反序列化方法只是一个简单的示意,在实际项目中需要根据具体采用的序列化/反序列化库( 如常用的Jackson、Gson等) 以及对应的配置和使用方式来准确地实现将JSON字符串反序列化为指定类型对象的功能, 不同的库有不同的使用方法和特点, 这里只是简单假设使用了fastjson库进行示意性的反序列化操作。
// 以下仅为示意,实际请使用正确的反序列化逻辑
return JSONUtil . toBean ( value , clazz ) ; // 假设使用了fastjson库
// 使用Hutool工具库中的JSONUtil工具类的toBean方法, 尝试将传入的JSON字符串( value) 反序列化为指定类型( clazz) 的Java对象, 这是基于假设使用fastjson库来实现反序列化功能的简单示例操作, 在实际应用中如果使用其他库或者有更复杂的反序列化需求( 比如包含对象嵌套、特殊数据类型处理等情况) , 则需要相应地调整和完善这个反序列化逻辑, 确保能够正确地将缓存中的JSON格式数据转换为业务中可用的Java对象。
}
/ * *
* 批 量 放 入 缓 存 的 方 法 , 支 持 泛 型 键 值 类 型
* 该 方 法 用 于 批 量 将 键 值 对 数 据 放 入 到 Redis 缓 存 中 , 支 持 泛 型 的 键 ( < K > ) 和 值 ( < V > ) 类 型 , 不 过 要 求 键 和 值 的 类 型 都 需 要 能 够 转 换 为 字 符 串 ( 因 为 Redis 中 存 储 的 数 据 最 终 是 以 字 符 串 形 式 保 存 的 , 通 常 需 要 进 行 序 列 化 操 作 ) , 同 时 可 以 指 定 缓 存 的 过 期 时 间 ( expireTime 参 数 ) 和 时 间 单 位 ( timeUnit 参 数 ) , 以 便 对 放 入 的 缓 存 数 据 设 置 合 理 的 过 期 策 略 , 保 证 缓 存 数 据 的 时 效 性 和 内 存 资 源 的 有 效 利 用 。
* @param data 待 放 入 缓 存 的 键 值 对 映 射
* @param < K > 键 的 类 型 , 需 要 可 转 换 为 String
* @param < V > 值 的 类 型 , 需 要 可 转 换 为 String
* /
// public <K, V> void batchPut(Map<K, V> data) {
// List<String> allArgs = new ArrayList<>();
// for (Map.Entry<K, V> entry : data.entrySet()) {
// allArgs.add(entry.getKey().toString());
// allArgs.add(JSONUtil.toJsonStr(entry.getValue()));
// }
//
// // 使用MSET命令进行批量插入
// RedisScript<Void> script = new DefaultRedisScript<>("return redis.call('MSET', unpack(ARGV));", Void.class);
// // 将所有参数整合为一个String数组传递给execute方法
// stringRedisTemplate.execute(script, Arrays.asList(""), allArgs.toArray(new String[0]));
// }
// public <K, V> void batchPut(Map<K, V> data) {
// List<String> allArgs = new ArrayList<>();
// for (Map.Entry<K, V> entry : data.entrySet()) {
// allArgs.add(entry.getKey().toString());
// allArgs.add(JSONUtil.toJsonStr(entry.getValue()));
// }
//
// // 使用MSET命令进行批量插入
// RedisScript<Void> script = new DefaultRedisScript<>("return redis.call('MSET', unpack(ARGV));", Void.class);
// // 将所有参数整合为一个String数组传递给execute方法
// stringRedisTemplate.execute(script, Arrays.asList(""), allArgs.toArray(new String[0]));
// }
public < K , V > void batchPut ( String prefix , Map < K , V > data , long expireTime , TimeUnit timeUnit ) {
// 定义了一个泛型方法 `batchPut`,用于批量将键值对数据放入到 Redis 缓存中。
// `<K, V>` 表示这是支持泛型的方法,其中 `K` 是键的类型参数,`V` 是值的类型参数,意味着可以传入不同类型的键值对映射作为参数,只要它们满足后续代码中的相关要求(例如能够转换为合适的字符串形式用于 Redis 存储等)。
// `prefix` 参数是一个字符串类型,用于给要存入 Redis 的键添加一个统一的前缀,方便在 Redis 中对缓存数据进行分类管理或者区分不同业务模块下的缓存项等,例如可以根据不同功能模块设置不同的前缀,便于后续的查找、清理等操作。
// `data` 参数是一个 `Map<K, V>` 类型,表示要批量存入 Redis 的键值对映射集合,其中键的类型为 `K`,值的类型为 `V`,这个映射集合包含了所有需要缓存的数据内容,通过遍历这个映射来逐个将键值对存入 Redis。
// `expireTime` 参数是一个长整型(`long`),用于指定缓存数据的基础过期时间,即从存入 Redis 开始,经过多长时间后缓存数据应该自动失效,具体的时间单位由 `timeUnit` 参数来明确,这样可以根据业务需求灵活设置缓存的有效期,保证缓存数据的时效性,避免长期占用内存资源同时又能满足一定时间内的重复使用需求。
// `timeUnit` 参数是 `TimeUnit` 类型,它是 Java 中用于表示时间单位的枚举类型(例如可以是秒、分钟、小时、天等),与 `expireTime` 参数配合使用,精确地确定缓存数据的过期时长设置,使得缓存的过期时间设置更加符合业务场景的实际需求,比如可以根据数据的更新频率、重要性等因素来选择合适的时间单位和时长进行设置。
List < String > allArgs = new ArrayList < > ( ) ;
// 创建一个 `ArrayList` 类型的列表对象 `allArgs`,用于存储后续要传递给 Redis 命令的所有参数。
// 在这里,这些参数主要是由经过处理后的 Redis 键(带有前缀的键)以及对应的值(转换为 JSON 字符串后的缓存值)组成,通过将这些参数收集到一个列表中,方便后续统一传递给相关的 Redis 操作方法,以实现批量插入缓存数据的功能,并且以列表形式组织参数也符合 Redis 某些批量操作命令对参数格式的要求(具体取决于后续实际执行的 Redis 命令实现方式)。
for ( Map . Entry < K , V > entry : data . entrySet ( ) ) {
// 开始一个循环,遍历传入的 `data` 参数(即 `Map<K, V>` 类型的键值对映射集合)中的每一个键值对元素。
// `Map.Entry<K, V>` 表示键值对映射中的每一个单独的条目(包含一个键和对应的值),`entrySet()` 方法返回由所有这些键值对条目组成的集合,通过遍历这个集合,就能依次获取到每一个键值对,方便后续对每个键值对进行处理,将它们逐个转换为适合 Redis 存储的参数形式并添加到 `allArgs` 列表中,为批量插入缓存数据做准备。
String prefixedKey = prefix + entry . getKey ( ) . toString ( ) ;
// 对于当前遍历到的键值对(`entry`),获取其键(`entry.getKey()`),并将其转换为字符串类型(通过 `toString()` 方法),然后与传入的前缀(`prefix`)进行拼接,得到一个带有前缀的 Redis 键(`prefixedKey`)。
// 这样做的目的是为了给每个键添加统一的前缀标识,符合前面提到的便于在 Redis 中分类管理缓存数据的需求,同时确保生成的键符合 Redis 中键的格式要求( Redis 键必须是字符串类型),使得后续可以通过这个完整的键准确地在 Redis 中定位和操作对应的缓存数据。
allArgs . add ( prefixedKey ) ;
// 将刚刚生成的带有前缀的 Redis 键(`prefixedKey`)添加到 `allArgs` 列表中,以便后续作为参数传递给 Redis 操作命令,这个键在 Redis 中用于唯一标识要存储的缓存数据,通过不断添加每个键值对对应的键到列表中,逐步构建好批量操作所需的完整参数列表。
allArgs . add ( JSONUtil . toJsonStr ( entry . getValue ( ) ) ) ;
// 获取当前遍历到的键值对(`entry`)中的值(`entry.getValue()`),这个值通常是一个 Java 对象(类型为 `V`),由于 Redis 只能存储字符串类型的数据,所以需要使用 `JSONUtil`(假设这里使用的是 `Hutool` 工具库中的 JSON 工具类,用于方便地进行 JSON 相关操作)的 `toJsonStr` 方法将这个 Java 对象转换为 JSON 字符串形式。
// 然后将转换后的 JSON 字符串形式的值添加到 `allArgs` 列表中,与前面添加的键相对应,这样列表中就依次存储了键和对应的值的字符串表示,按照顺序为后续批量插入缓存数据时提供完整且符合要求的参数内容,使得 Redis 能够正确地解析并存储这些键值对数据作为缓存内容。
// 在放置每个值之后立即设置过期时间
// 对于传入的键值对映射( data) 中的每一组键值对, 先将键转换为带有指定前缀( prefix) 的字符串形式( prefixedKey) , 然后将键和对应的值转换后的JSON字符串( 通过JSONUtil.toJsonStr方法将值序列化为JSON字符串) 分别添加到一个列表( allArgs) 中, 用于后续在Redis中设置键值对数据。
// 同时, 使用StringRedisTemplate的opsForValue操作对象的set方法, 为每个键值对立即设置过期时间, 设置的过期时间是在传入的固定过期时间( expireTime) 基础上加上一个随机时长( 1L + random.nextInt(20)),这样做的目的同样是为了
// 在放置每个值之后立即设置过期时间
stringRedisTemplate . opsForValue ( ) . set ( prefixedKey , JSONUtil . toJsonStr ( entry . getValue ( ) ) , expireTime + ( 1L + random . nextInt ( 20 ) ) , timeUnit ) ;
// 使用注入的 `stringRedisTemplate` 对象的 `opsForValue()` 方法获取操作字符串值的相关操作对象,然后调用其 `set` 方法向 Redis 中设置键值对数据以及对应的过期时间策略。
// `prefixedKey` 是经过处理后的 Redis 键,它由传入的 `prefix` 和当前遍历到的键值对中的键(通过 `entry.getKey().toString()` 转换为字符串后拼接而成),这个键用于在 Redis 中唯一标识该条缓存数据。
// `JSONUtil.toJsonStr(entry.getValue())` 这部分是利用 `Hutool` 工具库中的 `JSONUtil` 工具类,将当前键值对中的值(其类型为泛型 `V`,通常是一个 Java 对象)转换为 JSON 字符串格式,因为 Redis 存储数据时一般要求数据以字符串形式存储,所以需要先将对象进行序列化操作,使其能正确存入 Redis 中。
// `expireTime+(1L+random.nextInt(20))` 这里是在设置缓存的过期时间,`expireTime` 是传入的基础过期时长,在此基础上加上一个随机的时长(`1L + random.nextInt(20)`,其中 `1L` 表示长整型的数字 `1`,确保后续加法运算结果为长整型,`random.nextInt(20)` 会生成一个 `0` 到 `19` 的随机整数,两者相加后就得到了一个在 `expireTime` 基础上增加了一定随机范围的新的过期时长)。添加随机时长的目的是为了避免大量缓存同时过期引发缓存雪崩问题,通过让缓存过期时间更加分散,提高缓存系统的稳定性和可靠性,使缓存过期的时间分布更加均匀,减轻 Redis 在某一时刻集中处理大量缓存过期的压力。
// `timeUnit` 参数则明确了前面设置的过期时长所使用的时间单位(例如秒、分钟、小时等),它与 `expireTime` 以及随机增加的时长共同确定了该条缓存数据在 Redis 中具体的过期时间设置,保证缓存数据在合适的时间后自动失效,既能满足数据时效性要求,又能合理利用内存资源,避免数据长期占用内存空间。
}
// 这个大括号是 `public <K, V> void batchPut(String prefix, Map<K, V> data, long expireTime, TimeUnit timeUnit)` 方法的结束括号,表示该方法中批量向 Redis 放入键值对并设置过期时间的逻辑执行完毕,方法结束后,所有传入的键值对数据都会按照设定的规则逐个存入 Redis 中,并带有相应的过期时间设置,完成批量缓存数据的放置操作。
}
}
// 这个大括号是整个 `CacheClient` 类的结束括号,表示该类的定义结束,类中包含了多个用于缓存操作的方法,如缓存数据的设置、查询(包括解决缓存穿透、击穿等不同场景下的查询方法)以及批量获取、批量放入等功能相关的代码逻辑都在这个类的范围之内,类整体实现了与缓存交互的一系列功能,为项目中的缓存应用提供了相应的操作支持。
}