一、前言

在高并发项目(如 12306 购票、秒杀、海量查询场景)中,Redis 缓存使用不当,极易出现三大经典问题:

  1. 缓存穿透:查询不存在的数据,绕过缓存直接打数据库;
  2. 缓存击穿:热点 Key 过期瞬间,大量并发请求直连 DB;
  3. 缓存雪崩:大量 Key 同时过期,数据库压力骤增。

普通缓存 get -> 判断 -> 查库 简易写法,完全无法应对线上高并发流量。本文基于 12306 开源项目生产级 safeGet 安全缓存查询方法,完整拆解「布隆过滤器 + 分布式锁 + 双重检查 + 自动回填」的万能缓存方案,附带执行流程图、核心原理、适用场景,可直接复用至项目。

二、核心设计思想

该方法是通用泛型缓存查询工具,整合多层防护体系,分层拦截风险请求:

  1. 第一层:直接查询 Redis 缓存,命中直接返回,保证高性能;
  2. 第二层:布隆过滤器 + 自定义过滤器,拦截非法空 Key,杜绝缓存穿透;
  3. 第三层:缓存未命中时加分布式锁,限制单线程查库,防止缓存击穿;
  4. 第四层:DCL 双重检查,避免重复查库、重复回填缓存;
  5. 第五层:数据库查询 + 缓存自动回填,兜底业务数据;
  6. 第六层:空数据自定义回调,完善空值业务处理。

三、完整执行流程图

flowchart TD
    A["调用safeGet方法"] --> B["查询Redis缓存"]
    B --> C{缓存是否命中?}
    C -- 是 --> Z["直接返回结果"]
    
    C -- 否 --> D{布隆过滤器判定Key不存在?<br/>自定义过滤规则拦截?}
    D -- 是 --> Z
    D -- 否 --> E["加分布式锁Redisson Lock"]
    
    E --> F["二次查询缓存(DCL双重检查)"]
    F --> G{缓存是否存在?}
    G -- 是 --> H["释放锁"] --> Z
    G -- 否 --> I["执行cacheLoader查询数据库"]
    
    I --> J["查询结果写入Redis缓存+设置过期时间"]
    J --> K{DB是否无数据?}
    K -- 是 --> L["执行空值回调cacheGetIfAbsent"]
    K -- 否 --> M["缓存回填完成"]
    
    L & M --> H

四、完整核心源码

/**
 * 安全获取缓存(防穿透、防击穿、高性能通用方案)
 *
 * @param key               缓存Key
 * @param clazz             返回数据泛型类型
 * @param cacheLoader       缓存未命中时,DB数据加载器
 * @param timeout           缓存过期时间
 * @param timeUnit          过期时间单位
 * @param bloomFilter       布隆过滤器:防缓存穿透
 * @param cacheGetFilter    自定义Key过滤规则
 * @param cacheGetIfAbsent  数据库无数据时自定义回调
 * @return 缓存/数据库查询结果
 */
@Override
public <T> T safeGet(
        String key,
        Class<T> clazz,
        CacheLoader<T> cacheLoader,
        long timeout,
        TimeUnit timeUnit,
        RBloomFilter<String> bloomFilter,
        CacheGetFilter<String> cacheGetFilter,
        CacheGetIfAbsent<String> cacheGetIfAbsent) {

    // 1. 无锁查询Redis缓存,优先走缓存,保证高并发性能
    T result = get(key, clazz);

    // 2. 快速熔断:满足任一条件直接返回,拦截无效请求
    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;
    }

    // 3. 缓存未命中:加分布式锁,防止热点Key缓存击穿
    RLock lock = redissonClient.getLock(SAFE_GET_DISTRIBUTED_LOCK_KEY_PREFIX + key);
    lock.lock();

    try {
        // 4. DCL双重检查:防止多线程重复查询数据库
        if (CacheUtil.isNullOrBlank(result = get(key, clazz))) {
            // 5. 执行DB查询 + 自动回填Redis缓存
            if (CacheUtil.isNullOrBlank(result = loadAndSet(key, cacheLoader, timeout, timeUnit, true, bloomFilter))) {
                // 6. 数据库无数据,执行自定义兜底回调
                Optional.ofNullable(cacheGetIfAbsent).ifPresent(each -> each.execute(key));
            }
        }
    } finally {
        // 7. 强制释放分布式锁,避免死锁
        lock.unlock();
    }
    return result;
}

五、分层逐段源码解析

1. 方法入参设计(高扩展性)

  • key / clazz:基础缓存标识与泛型类型绑定;
  • cacheLoader:函数式接口,由业务层传入数据库查询逻辑,实现方法通用;
  • timeout / timeUnit:动态设置缓存过期时间,缓解缓存雪崩;
  • bloomFilter:全局布隆过滤器,拦截不存在的非法 Key;
  • 两个自定义函数式回调:适配不同业务的过滤、空值兜底需求。

2. 一级缓存查询(高性能基石)

T result = get(key, clazz);

3. 三层熔断拦截(杜绝缓存穿透)

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;
}
  • 缓存有值:直接返回;
  • 自定义过滤器:过滤无效业务 Key;
  • 布隆过滤器:若 Key 从未存入,直接拦截,拒绝查询 DB。

4. 分布式锁(解决缓存击穿)

RLock lock = redissonClient.getLock(SAFE_GET_DISTRIBUTED_LOCK_KEY_PREFIX + key);
lock.lock();

热点 Key 过期瞬间,海量并发请求同时失效,通过Redisson 分布式锁保证:同一个 Key,同一时间仅一个线程访问数据库,避免 DB 连接打满。

5. DCL 双重检查(避免重复查库)

if (CacheUtil.isNullOrBlank(result = get(key, clazz)))

抢到锁后二次查询缓存:上一个线程可能已经完成 DB 查询 + 缓存回填,当前线程无需重复查库,节省数据库资源。

6. 数据加载与缓存回填

loadAndSet(key, cacheLoader, timeout, timeUnit, true, bloomFilter)
  • 执行业务层传入的 cacheLoader 逻辑查询数据库;
  • 查询完成后自动写入 Redis,并设置过期时间;
  • 同步维护布隆过滤器数据,保证后续穿透拦截生效。

7. 空值兜底 & 锁释放

  • 数据库无数据时,执行自定义回调(日志记录、默认值填充等);
  • finally 强制释放锁,无论业务异常与否,杜绝死锁问题。

六、解决的三大缓存问题总结

问题 解决方案 核心实现
缓存穿透 布隆过滤器 + 自定义 Key 过滤 不存在的 Key 直接拦截,不查库
缓存击穿 Redisson 分布式锁 + 双重检查 热点 Key 失效,单线程查库回填
缓存雪崩 动态过期时间 + 缓存分层 避免批量 Key 同时过期,分散 DB 压力

七、项目落地使用示例

// 查询车次详情 安全缓存调用
TrainDTO trainDTO = safeGet(
    "train:info:" + trainId,
    TrainDTO.class,
    // 缓存未命中,查询数据库
    () -> trainMapper.selectTrainInfo(trainId),
    30,
    TimeUnit.MINUTES,
    bloomFilter,
    null,
    key -> log.warn("当前Key无数据库数据:{}", key)
);

八、适用场景

  1. 高并发查询业务:车次、站点、商品、用户信息查询;
  2. 热点数据缓存:秒杀商品、节假日热门线路;
  3. 数据一致性要求高、禁止频繁打库的核心业务;
  4. 所有需要统一规范缓存使用的微服务项目。

九、总结

safeGet 是一套开箱即用、高可用、高并发的缓存标准化方案,摒弃了传统简易缓存写法的各种漏洞,通过「缓存查询→请求拦截→分布式锁→双重检查→自动回填」五层架构,完美解决缓存三大经典问题。代码基于泛型 + 函数式接口设计,通用性极强,可直接复用在电商、出行、金融等各类高并发项目中,也是面试中缓存架构设计的高频标准答案。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐