简易的查询与缓存的统一执行器

一、背景与问题

在高并发、高可用的业务场景中,我们常面临这样的数据获取流程:

  • 先查缓存(如 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」等顺序,降低出错概率

三、关键实现

流程简述:

  1. 读 Redis :用 cacheKeyFunc.get() 得到 key,按 resultType 反序列化;命中则直接返回
  2. 读 DB :调用 dbQueryFunc.get();若有结果,则写入 Redis(带过期时间)并返回
  3. 实时计算 :调用 computeFunc.apply(param),param 由 paramFunc 提供
  4. 写回 :若结果非空,先 dbSaveFunc.accept(result) 写 DB,再写 Redis;

方法签名:

java 复制代码
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 → 返回

关键代码

scss 复制代码
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操作的方法;


四、使用示例

java 复制代码
// 示例:带 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>() {}
);
相关推荐
犀利豆1 小时前
AI in Harness(三)
人工智能·后端
IT_陈寒2 小时前
Java的Stream.parallel()把我CPU跑爆了,这种优化要谨慎
前端·人工智能·后端
子兮曰2 小时前
Bun 重写为 Rust:11天、64个Claude、16.5万美元,一次改变行业认知的激进实验
前端·后端·bun
子兮曰2 小时前
Node.js v26.5.0 深度解读:import Text、流式 ReadableStreamTee、以及隐藏的安全加固浪潮
前端·后端·node.js
子兮曰2 小时前
TypeScript 7.0 RC 深度解读:Go 重写完成,10 倍提速,编译器的「奇点时刻」
前端·后端·typescript
kosan3 小时前
.NET/C# 实战:我如何把登录验证码从“裸奔”改到“铜墙铁壁”
后端
王中阳Go3 小时前
AI编程实践:规则 + 技能 + 子代理 + 钩子,拆解Trae AI 开发规范的完整落地范式
后端·agent·ai编程
码事漫谈3 小时前
放下代码洁癖,享受挖坑人生
后端
_张有志_3 小时前
手撕 STL:一棵红黑树,凭什么同时撑起 set 和 map?
后端
妙码生花3 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(二十七):管理员登录验证码开关
前端·后端·ai编程