一、背景与问题
在高并发、高可用的业务场景中,我们常面临这样的数据获取流程:
- 先查缓存(如 Redis)减轻 DB 压力、提升响应速度
- 缓存未命中再查 DB
- DB 也没有则实时计算
- 计算或查询到结果后:先落库,再回写缓存,保证下次命中
如果每个接口都手写「Redis → DB → 计算 → 写 DB → 写 Redis」这一套逻辑,会出现大量重复代码,且容易漏写回写步骤,导致缓存与 DB 不一致。因此需要一套可复用、声明式的执行器,把流程固定下来,只由调用方提供「查什么、怎么算、怎么存」。
二、整体设计思路
核心思想:用函数式接口描述每一步能力,由工具类按固定顺序执行。
- Supplier<R> dbQueryFunc :数据库查询方法,返回查询的结果;
- Function<T, R> computeFunc:实时计算方法,接受参数,返回计算的结果;
- Consumer<R> dbSaveFunc:保存结果到数据库的方法;
流程由工具类统一编排,业务方只负责提供这些 Lambda,从而:
- 减少重复的 if-else 和 null 判断
- 统一日志(如「DB 无数据,实时计算」「缓存和 DB 均无数据,实时计算」)
- 保证「先写 DB 再写 Redis」等顺序,降低出错概率
三、关键实现
流程简述:
- 读 Redis:用
cacheKeyFunc.get()得到 key,按resultType反序列化;命中则直接返回 - 读 DB:调用
dbQueryFunc.get();若有结果,则写入 Redis(带过期时间)并返回 - 实时计算:调用
computeFunc.apply(param),param 由paramFunc提供 - 写回:若结果非空,先
dbSaveFunc.accept(result)写 DB,再写 Redis;
方法签名:
public static <T, R> R execute(
Supplier<String> cacheKeyFunc, // 缓存 key
Long cacheExpireSeconds, // 过期时间(秒)
Supplier<R> dbQueryFunc, // 从数据库查询结果的方法
Function<T, R> computeFunc, // 实时计算的方法
Supplier<T> paramFunc, // 获取参数值的方法,获取的参数传递给computeFunc
Consumer<R> dbSaveFunc, // 保存结果到数据库的方法
TypeReference<R> resultType // 用于 Redis 反序列化
)
要点:
TypeReference<R>解决泛型R在 JSON 反序列化时的类型擦除问题- 写回顺序固定为:先 DB,后 Redis,避免缓存与 DB 不一致
流程示意:
Redis + DB + 计算:
读 Redis → 命中则返回
→ 未命中:读 DB → 有则写 Redis 并返回
→ 仍无:取参数 → 实时计算 → 写 DB → 写 Redis → 返回
关键代码
public static <T, R> R execute(Supplier<String> cacheKeyFunc,
Long cacheExpireSeconds,
Supplier<R> dbQueryFunc,
Function<T, R> computeFunc,
Supplier<T> paramFunc,
Consumer<R> dbSaveFunc,
TypeReference<R> resultType) {
T param = Objects.nonNull(paramFunc) ? paramFunc.get() : null;
if (Objects.isNull(cacheKeyFunc) || Objects.isNull(resultType)) {
throw new RuntimeException("cacheKeyFunc和resultType不能为空");
}
String cacheKey = cacheKeyFunc.get();
if (StringUtils.isBlank(cacheKey)) {
throw new RuntimeException("缓存键不能为空");
}
// 1. 先读 Redis
R result = RedisUtils.queryForObj(cacheKey, resultType);
if (Objects.nonNull(result)) return result;
// 2. 读 DB
result = dbQueryFunc.get();
if (Objects.nonNull(result)) {
//保存到redis
RedisUtils.saveObj(cacheKey, JSON.toJSONString(result), cacheExpireSeconds, TimeUnit.SECONDS);
return result;
}
// 3. 未查询到数据,执行计算逻辑
log.info("缓存和DB均无数据,实时计算数据");
result = computeFunc.apply(param);
// 4. 先保存到 DB,再保存到 Redis
if (Objects.nonNull(result)) {
dbSaveFunc.accept(result);
RedisUtils.saveObj(cacheKey, JSON.toJSONString(result), cacheExpireSeconds, TimeUnit.SECONDS);
} else {
log.info("计算结果为空,无需保存");
}
return result;
}
备注:其中的RedisUtils是我封装的一个redis操作工具类,提供了简化了redis操作的方法;
四、使用示例
// 示例:带 Redis 缓存的用户统计查询
UserStat stat = QueryProcessUtils.execute(
() -> "user:stat:" + userId, // cache key
3600L, // 过期 1 小时
() -> userStatMapper.selectByUserId(userId),
(userId) -> userService.aggregateStat(userId),
() -> userId,
(result) -> userStatMapper.insert(result),
new TypeReference<UserStat>() {}
);