📚 2.2 商户缓存 - 8. 封装 Redis 工具类学习文档
一、 为什么要封装工具类?(核心动机)
在实际的企业级项目中,需要缓存的数据远不止“商户详情”这一种。可能还有“活动信息”、“商品详情”、“用户配置”等等。
- 痛点: 难道每次遇到新的业务对象,我们都要把“查 Redis -> 查不到 -> 查 MySQL -> 解决穿透(存空值) -> 解决击穿(加锁/逻辑过期)”这几百行防守代码重新写一遍吗?
- 解法: 遵循 DRY (Don't Repeat Yourself) 原则。我们需要将这些共性的、与具体业务无关的缓存逻辑剥离出来,封装成一个通用的
CacheClient工具类。各个 Service 层只需要调用这个工具类即可。
二、 工具类需要具备的 4 大核心能力
一个合格的 Redis 缓存工具类,通常需要对外暴露以下几个核心方法:
- 普通缓存写入: * 将任意 Java 对象序列化为 JSON 并存入 Redis,同时设置 TTL 过期时间。
- 逻辑过期缓存写入: * 将任意 Java 对象封装成带有
expireTime的特制对象(如RedisData),序列化为 JSON 存入 Redis(专门为了解决缓存击穿)。 - 防穿透查询(通用版): * 根据指定的 Key 查询缓存,如果不存在,自动去数据库查询,并自带“缓存空值”的防御机制。
- 防击穿查询(通用版): * 根据指定的 Key 查询缓存,如果遇到高并发热点失效,自动基于“逻辑过期”或“互斥锁”机制去异步重建缓存,并返回旧数据。
三、 核心技术难点解析:Java 泛型与函数式编程
封装这个工具类最大的难点在于:工具类怎么知道去哪里查数据库?
工具类里是没有 shopMapper.selectById() 的,因为它不知道当前查的是商户还是商品。
为了实现“通用”,我们需要用到 Java 8 的两个高级特性:
1. 泛型 (Generics) <R, ID>
因为我们不知道最终要返回什么类型的对象(Shop 还是 User),也不知道查询条件是什么类型(Long 还是 String),所以方法必须声明泛型。
R代表 Return,即最终返回的实体类。ID代表查询条件的类型。
2. 函数式接口 (Function<ID, R>)
既然工具类不知道怎么查数据库,那我们就把“查数据库的逻辑”当作参数传给工具类。
Function<ID, R>的含义是:给我一个ID类型的参数,我会还给你一个R类型的结果。- 实战演练: 在 Service 层调用工具类时,只需要传入一行 Lambda 表达式即可,例如:
id -> getById(id)。工具类内部在需要查数据库时,调用dbFallback.apply(id)即可执行这段传进来的逻辑。
四、 核心方法签名设计 (伪代码解析)
以“防缓存穿透的通用查询方法”为例,我们来看看它华丽的方法签名:
Java
/**
* 处理缓存穿透的通用查询方法
*
* @param keyPrefix Redis 的 key 前缀 (例如 "cache:shop:")
* @param id 查询的 id (例如 1L)
* @param type 返回值的 Class 类型 (为了将 JSON 反序列化为对象,例如 Shop.class)
* @param dbFallback 查数据库的后备方案 (一个函数,例如 id -> getById(id))
* @param time 过期时间
* @param unit 过期时间单位
* @return 泛型对象 R
*/
public <R, ID> R queryWithPassThrough(
String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
// 1. 拼接 Key
// 2. 查 Redis
// 3. 判断是否命中(如果命中是真的数据,直接返回;如果是空字符串 "",说明是穿透防御,返回 null)
// 4. 未命中,调用传入的函数去查数据库:R r = dbFallback.apply(id);
// 5. 数据库查不到,将空字符串写入 Redis 防穿透,返回 null
// 6. 数据库查到了,序列化为 JSON 写入 Redis,设置 TTL
// 7. 返回结果
}