Redis 缓存三大杀手完全指南:从一次"缓存挂了数据库被打垮"看懂穿透、击穿、雪崩

2022 年我给一个高 QPS 的商品详情接口前面加了一层 Redis 缓存:先查缓存、没有再查 MySQL 回填。加完平时稳得很,直到双十一前一次压测我亲眼看着事故发生——流量一上来 MySQL 的 CPU 几秒钟冲到 100%,慢查询堆积、接口大面积超时、整条链路雪崩。复盘很久才想明白:我以为"加了缓存就等于数据库被保护",可缓存这层保护是有漏洞的。本文把缓存的三把刀一个个讲透。先看清案发现场 Cache-Aside 旁路缓存:先查缓存、未命中查库回填,三大问题都出在它身上,它只在"绝大多数请求命中缓存"这个前提下才保护数据库。缓存穿透:查数据库里根本不存在的数据,缓存永远命中不了、请求全量打库,用缓存空值挡住反复查同一个不存在 ID、用布隆过滤器彻底拦掉随机不存在 ID。缓存击穿:某个真实存在的热点 key 过期那一瞬间,成千上万并发同时未命中一起涌向数据库,用 SETNX 互斥锁只放一个线程查库重建、或用逻辑过期返旧值加异步重建,按"怕慢还是怕旧"取舍。缓存雪崩:大批 key 同一秒集体失效或 Redis 整个宕机,用过期时间叠加随机量打散集中过期、用信号量降级限流给数据库兜底。还有绕不开的缓存与数据库一致性——延迟双删能缓解但不能根除;以及大 key、热 key、缓存预热、命中率监控这些工程坑。核心一句:加了缓存不等于数据库被保护,缓存是一条有特定薄弱点的堤坝,你不逐个加固它就会在高峰期被精准击穿。

2022 年我负责一个商品详情接口,QPS 不低。为了扛住流量,我在接口前面加了一层 Redis 缓存:查商品时先看缓存,缓存没有再查 MySQL、然后回填进缓存。加完之后效果立竿见影,数据库压力一下小了很多,平时跑得稳稳当当。直到那年双十一前的一次压测,我亲眼看着事故发生:压测流量一上来,监控里 MySQL 的 CPU 几秒钟内就冲到 100%,慢查询堆积,商品接口大面积超时,整条链路雪崩。我当时第一反应是"缓存不是加了吗,怎么还把库打挂了"。盯着监控复盘了很久才想明白:我以为"加了缓存"就等于"数据库被保护了",可缓存这层保护其实是有很多漏洞的——有人拿一批根本不存在的商品 ID 来刷,缓存永远命中不了,请求全量穿透到库;一个超热的商品缓存恰好过期的那一瞬间,成百上千个并发请求同时发现缓存没了,一起涌向数据库;甚至我图省事给一大批缓存设了相同的过期时间,它们就在同一秒集体失效……这些场景下,缓存非但没挡住流量,反而像一道被瞬间击穿的堤坝。那次之后我才认真搞懂:缓存穿透、缓存击穿、缓存雪崩——这是悬在每一个缓存方案头上的三把刀。这篇文章就把它们一个个讲透:每一种是怎么发生的、为什么会把数据库打垮、分别该怎么防,以及缓存一致性这些绕不开的坑。

问题背景

先把那次事故的现象和我的误判讲清楚,后面所有的方案都是冲着纠正这个误判去的。

现象:商品接口加了 Redis 缓存,平时数据库压力很小、运行平稳;但在流量高峰或某些特定请求模式下,大量请求绕过缓存直接打到 MySQL,数据库 CPU 瞬间打满,接口大面积超时,服务雪崩。

我当时的错误认知:"在数据库前面加一层缓存,数据库就被保护起来了。绝大多数请求都会命中缓存,落到库上的能有多少?"

真相:缓存对数据库的保护,不是无条件的。它建立在一个隐含前提上——"绝大多数请求都能命中缓存"。而缓存穿透、击穿、雪崩这三种情况,恰恰都是从不同角度打破这个前提:让请求大量地、集中地无法命中缓存,从而直接砸向数据库。缓存这层堤坝有几个特定的薄弱点,不主动去加固,它就会在高峰期被精准击穿。

要把缓存方案做稳,需要几块认知:

  • 缓存最常用的 Cache-Aside 模式长什么样,三大问题为什么都出在它身上;
  • 缓存穿透:查不存在的数据,怎么用空值缓存和布隆过滤器挡住;
  • 缓存击穿:热点 key 过期瞬间的并发,怎么用互斥锁和逻辑过期挡住;
  • 缓存雪崩:大批 key 同时失效或 Redis 宕机,怎么用随机过期和降级挡住;
  • 缓存和数据库的一致性该怎么保证。

一、先看清缓存的基本模式:Cache-Aside

要理解三大问题,得先看清它们共同的"案发现场"——绝大多数业务用的缓存模式,叫 Cache-Aside(旁路缓存)

它的读流程很朴素,就三步:第一步,先查缓存,命中就直接返回;第二步,缓存没命中,就去查数据库;第三步,把从数据库查到的结果回填进缓存,并设一个过期时间,这样下次同样的查询就能命中了。下面是这个模式最标准的代码:

// Cache-Aside(旁路缓存)读流程:先查缓存,未命中再查库并回填。
public Product getProduct(Long id) {
    String key = "product:" + id;

    // 1. 先查缓存
    String cached = redis.opsForValue().get(key);
    if (cached != null) {
        return JSON.parseObject(cached, Product.class);   // 命中,直接返回
    }

    // 2. 缓存未命中,查数据库
    Product product = productMapper.selectById(id);

    // 3. 把查到的结果回填进缓存,并设一个过期时间
    if (product != null) {
        redis.opsForValue().set(key, JSON.toJSONString(product),
                30, TimeUnit.MINUTES);
    }
    return product;
}

这段代码看起来天衣无缝,平时也确实跑得很好。但请盯住它的逻辑——它只在一个前提下能保护数据库:大部分请求都走进了第 1 步那个 if 分支。一旦请求大量地从第 1 步漏下来、涌进第 2 步,这段代码就成了一个"高效地把流量转发给数据库"的管道。三大问题,本质上就是三种"让请求大量漏到第 2 步"的方式。

二、缓存穿透:查一笔永远不存在的数据

第一把刀,叫缓存穿透。它指的是:请求查询一个数据库里根本不存在的数据。

看清它为什么致命:一个不存在的 ID,查数据库返回空,于是上面那段代码的第 3 步 if (product != null) 不成立——缓存没有被回填。结果就是,下一次同样的请求来了,缓存照样未命中,又去查了一次库……对于一个不存在的 ID,缓存这层永远命中不了,每一个这样的请求都 100% 穿透到数据库。如果有人恶意拿一批随机的、不存在的 ID 来刷接口,等于直接把全部流量引到了数据库上。

第一种对策是缓存空值:既然问题是"查不到就不写缓存",那就改成——查不到,也往缓存里写一个特殊的"空值"标记。这样同一个不存在的 ID 再来,会命中这个空值标记,直接返回,不再打库。要点是给空值设一个较短的过期时间(防止它一直占着内存,也防止这个 ID 后来真的有了数据时长时间读到旧的空值)。

// 防穿透方案一:把"查不到"这件事本身也缓存起来(缓存空值)。
private static final String EMPTY = "__EMPTY__";   // 空值的特殊标记

public Product getProductSafe(Long id) {
    String key = "product:" + id;

    String cached = redis.opsForValue().get(key);
    if (cached != null) {
        // 命中的是空值标记 —— 说明这个 id 确实不存在,直接返回 null
        if (EMPTY.equals(cached)) return null;
        return JSON.parseObject(cached, Product.class);
    }

    Product product = productMapper.selectById(id);
    if (product == null) {
        // 查不到也要写缓存:写一个空值标记,过期时间设短一些
        redis.opsForValue().set(key, EMPTY, 5, TimeUnit.MINUTES);
        return null;
    }
    redis.opsForValue().set(key, JSON.toJSONString(product),
            30, TimeUnit.MINUTES);
    return product;
}

缓存空值能解决"反复查同一个不存在的 ID"。但如果攻击者每次都用不同的不存在 ID,缓存里会堆满大量空值标记,而且每个新 ID 第一次还是会穿透一次。这时需要第二种、更彻底的对策:布隆过滤器(Bloom Filter)。它是一个极省内存的数据结构,能快速回答"某个元素可能在集合里,还是一定不在集合里"。它的特性很关键:说"不存在"时一定准确,说"可能存在"时有极小概率误判。把数据库里所有存在的 ID 都灌进布隆过滤器,查询前先问它一句,凡是它说"不存在"的,直接拦掉:

// 防穿透方案二:布隆过滤器 —— 查缓存之前先问它"这个 id 可能存在吗"。
private BloomFilter<Long> bloomFilter;

@PostConstruct
public void initBloomFilter() {
    // 预计 100 万个 id,可接受的误判率 1%
    bloomFilter = BloomFilter.create(
            Funnels.longFunnel(), 1_000_000, 0.01);
    // 服务启动时,把数据库里所有"存在的 id"都灌进去
    for (Long id : productMapper.selectAllIds()) {
        bloomFilter.put(id);
    }
}

public Product getProductWithBloom(Long id) {
    // 布隆过滤器说"不存在",那就 100% 不存在 —— 直接拦掉,连缓存都不查
    if (!bloomFilter.mightContain(id)) {
        return null;
    }
    // 它说"可能存在",才走正常的查缓存、查库流程
    return getProductSafe(id);
}

三、缓存击穿:热点 key 过期的那一瞬间

第二把刀,叫缓存击穿。注意它和穿透完全不同:击穿查的数据是真实存在的,问题出在时间点上。

设想一个超级热点的商品——比如大促主推款,它的缓存每秒被读几万次。某一刻,它的缓存过期了。就在过期的那一瞬间,这几万个并发请求同时发现缓存未命中,于是它们同时涌向数据库去查同一条数据。本来缓存好好的时候数据库一个请求都不用接,过期的瞬间却要硬扛几万个完全相同的查询。一个热点 key 的过期,就能在那一瞬间击穿缓存、给数据库来一记重拳。

第一种对策是互斥锁:既然问题是"几万个请求同时去查库",那就限制成"只让一个请求去查库重建缓存,其余的请求等一下"。用 Redis 的 SETNX(set if not exist)实现一把分布式锁,谁抢到锁谁去查库,没抢到的稍等片刻再重试读缓存——那时缓存通常已经被重建好了:

// 防击穿:热点 key 过期瞬间,只放一个线程去查库重建,其余线程等待。
public Product getProductWithLock(Long id) {
    String key = "product:" + id;
    String cached = redis.opsForValue().get(key);
    if (cached != null) {
        return JSON.parseObject(cached, Product.class);
    }

    String lockKey = "lock:product:" + id;
    // 用 SETNX 抢一把分布式锁,只有抢到的线程才去查库
    Boolean locked = redis.opsForValue()
            .setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
    if (Boolean.TRUE.equals(locked)) {
        try {
            // 抢到锁:查库、回填缓存
            Product product = productMapper.selectById(id);
            redis.opsForValue().set(key, JSON.toJSONString(product),
                    30, TimeUnit.MINUTES);
            return product;
        } finally {
            redis.delete(lockKey);             // 一定要在 finally 里释放锁
        }
    } else {
        // 没抢到锁:稍等片刻再重试(此时缓存大概率已被别人重建好)
        try { Thread.sleep(50); } catch (InterruptedException ignored) {}
        return getProductWithLock(id);
    }
}

互斥锁的代价是:没抢到锁的请求要等待,在重建期间响应会变慢一点。如果连这点延迟都不能接受,可以用第二种对策——逻辑过期:缓存永不真正过期(不设 Redis 的 TTL),而是把一个"逻辑过期时间"写进 value 里。读到数据后自己判断:没到逻辑过期时间就正常返回;已经逻辑过期了,就先返回手里这份旧数据,同时异步起一个线程去重建缓存。这样任何请求都不会被阻塞,代价是逻辑过期到重建完成的这段时间里,会读到旧数据。

// 防击穿方案二:逻辑过期 —— 缓存永不真正过期,过期时间写在 value 里。
class CacheData {
    Object data;
    long expireAt;        // 逻辑过期时间戳,由我们的代码自己判断
}

public Product getByLogicalExpire(Long id) {
    String key = "product:" + id;
    CacheData cd = JSON.parseObject(
            redis.opsForValue().get(key), CacheData.class);
    if (cd == null) return null;

    // 还没到逻辑过期时间:数据新鲜,直接返回
    if (cd.expireAt > System.currentTimeMillis()) {
        return (Product) cd.data;
    }
    // 已逻辑过期:异步起线程重建缓存,但当前请求先拿旧数据返回,不阻塞
    CACHE_REBUILD_POOL.submit(() -> rebuildCache(id));
    return (Product) cd.data;                 // 旧数据先顶着
}

互斥锁和逻辑过期,本质是同一个权衡的两端:互斥锁保证数据新鲜,代价是重建期间少量请求要等待;逻辑过期保证永不阻塞,代价是重建期间会读到旧数据。选哪个,取决于你的业务更不能容忍"慢"还是"旧"。

四、缓存雪崩:大批 key 同时失效,或 Redis 整个宕机

第三把刀,叫缓存雪崩。如果说击穿是"一个热点 key 倒下",雪崩就是"一大片 key 同时倒下",甚至是整个缓存层倒下。它有两种典型成因。

成因一:大批 key 在同一时刻集中过期。这恰恰常常是开发者图省事造成的——比如系统启动时批量预热了一万个缓存,代码里全都写 set(key, value, 30, MINUTES)。结果这一万个 key 会在 30 分钟后的同一秒一起过期,那一秒涌进来的所有请求全部未命中、全部砸向数据库。成因二:Redis 实例整个挂了。Redis 宕机,缓存层瞬间彻底消失,100% 的流量直接打到数据库——这是最猛烈的雪崩。

针对成因一,对策简单却极其有效:回填缓存时,给过期时间叠加一个随机量,把原本会"齐步走"的过期时间打散到一个时间范围内,数据库的压力就被摊平了:

// 防雪崩:回填缓存时给过期时间叠加随机量,避免大批 key 同时过期。
public void cacheProduct(Long id, Product product) {
    String key = "product:" + id;
    // 基础 30 分钟,再叠加 0~10 分钟的随机量,把过期时间打散开
    long ttl = 30 * 60 + ThreadLocalRandom.current().nextInt(10 * 60);
    redis.opsForValue().set(key, JSON.toJSONString(product),
            ttl, TimeUnit.SECONDS);
}

针对成因二(Redis 宕机),随机过期就无能为力了——这时缓存层已经整个没了。这里要靠两道防线。第一道是让 Redis 本身别那么容易整个挂掉:生产环境用主从 + 哨兵或集群模式,别用单实例。第二道、也是最关键的一道,是降级和限流:你必须接受"缓存层有可能彻底失效"这个事实,并为此准备好一个兜底方案——当缓存不可用时,绝不能让海量请求毫无节制地冲向数据库,而要用一个限流器(比如信号量)把"同时查库的请求数"卡在一个数据库能扛住的水平,超出的请求直接快速失败、返回兜底结果:

// 防雪崩最后一道防线:即便缓存全线失效,也只放有限的请求去查库。
private final Semaphore dbPermits = new Semaphore(50);   // 最多 50 个并发查库

public Product getProductDegradable(Long id) {
    try {
        return getProductWithLock(id);
    } catch (Exception e) {
        // 缓存链路异常,走降级:返回兜底结果,绝不放任请求继续砸数据库
        log.warn("缓存链路异常,触发降级", e);
        return queryDbLimited(id);
    }
}

private Product queryDbLimited(Long id) {
    // 拿不到查库名额,说明数据库已接近极限 —— 直接快速失败,保住数据库
    if (!dbPermits.tryAcquire()) {
        return Product.unavailable();
    }
    try {
        return productMapper.selectById(id);
    } finally {
        dbPermits.release();
    }
}

这里的核心思想要记住:雪崩防护的终点,不是"保证缓存永不失效"——那做不到;而是承认缓存会失效,并保证它失效时数据库不会被一起拖垮。一个健康的系统,在缓存全挂的最坏情况下,应该是"变慢、部分降级",而不是"彻底雪崩"。

五、绕不开的坑:缓存与数据库的一致性

讲完三大杀手,还有一个所有缓存方案都绕不开的问题:数据更新时,缓存和数据库怎么保持一致?数据库一旦更新了,缓存里那份就成了旧数据。

处理更新有两个动作:更新数据库、清理缓存。它们的顺序很有讲究。常见的错误是"先更新数据库,再删缓存"——如果删缓存那一步失败了,缓存里就一直是旧数据。而"先删缓存,再更新数据库"也有一个经典的并发漏洞:线程 A 删了缓存、还没来得及更新数据库,线程 B 进来一看缓存没有,就去查了数据库(查到的还是旧值)并回填进了缓存;然后 A 才更新完数据库——此刻数据库是新值,缓存却被 B 填回了旧值,不一致了。

实践中一个被广泛采用的缓解方案是延迟双删:先删一次缓存,再更新数据库,然后延迟一小段时间,再删一次缓存。第二次删除,正是为了清掉"在前面那个并发窗口里被其他读请求回填进去的旧值":

// 缓存一致性:先删缓存、再更数据库,并延迟一会儿再删一次(延迟双删)。
public void updateProduct(Product product) {
    String key = "product:" + product.getId();

    // 1. 先删缓存
    redis.delete(key);
    // 2. 再更新数据库
    productMapper.updateById(product);

    // 3. 延迟一小段时间后再删一次缓存。
    //    目的:清掉"第 1 步到第 2 步之间,被其他读请求回填进去的旧值"。
    DELAY_POOL.schedule(() -> redis.delete(key),
            500, TimeUnit.MILLISECONDS);
}

这里要诚实地说一句:延迟双删也只是降低不一致的概率和持续时间,并不能 100% 消除。要真正强一致,代价会很高(比如加分布式锁、或用 binlog 订阅来更新缓存)。所以更现实的工程态度是:接受缓存存在短暂不一致,只要给每个缓存都设了过期时间,最坏情况下,过期后也会自动回到一致——关键是想清楚你的业务能不能容忍这个"短暂"。

六、工程坑:大 key、热 key、缓存预热、命中率监控

把缓存真正用稳,还有几个工程细节绕不开。

坑 1:警惕大 key。把一个巨大的对象(比如一个几 MB 的列表)整个塞进一个缓存 key,会有麻烦:读它会占用大量网络带宽,而删除或过期一个大 key 甚至可能阻塞 Redis。大对象要拆分存储,或者只缓存必要的字段。

坑 2:热 key 可能压垮单个 Redis 节点。缓存击穿讲的是热点 key 过期,但即使不过期,一个超热的 key 也会让承载它的那个 Redis 分片承受远超其他分片的压力。极端热点可以考虑在多个节点上放副本,或者在应用层再加一层本地缓存(多级缓存)来分流。

坑 3:缓存预热。系统刚启动或刚发布时,缓存是空的,这时如果直接放全量流量进来,所有请求都会未命中、一起打库——相当于一次自找的雪崩。重要的热点数据,应该在系统对外提供服务之前就主动加载进缓存,这叫预热。

坑 4:一定要监控缓存命中率。命中率是缓存健康状况的第一指标。命中率突然下跌,往往就是穿透、击穿或雪崩正在发生的前兆。没有这个监控,你只能等数据库告警了才后知后觉。

// 监控缓存命中率:命中率是判断缓存是否健康的第一指标。
public Product getProductMonitored(Long id) {
    String key = "product:" + id;
    String cached = redis.opsForValue().get(key);

    if (cached != null) {
        meterRegistry.counter("cache.hit").increment();    // 命中,计数 +1
        return JSON.parseObject(cached, Product.class);
    }
    meterRegistry.counter("cache.miss").increment();        // 未命中,计数 +1

    Product product = productMapper.selectById(id);
    if (product != null) {
        cacheProduct(id, product);
    }
    // 命中率 = hit / (hit + miss)。它一旦骤降,
    // 基本就是穿透 / 击穿 / 雪崩三者之一正在发生,应立刻告警。
    return product;
}

下面这张图把"防穿透 + 防击穿 + 防雪崩"串成一条完整的查询链路:

关键概念速查

概念 / 手段 说明
Cache-Aside 旁路缓存:先查缓存,未命中查库并回填;三大问题都出在它身上
缓存穿透 查数据库里不存在的数据,缓存永远命中不了,请求全量打库
缓存空值 查不到也写一个空值标记进缓存,挡住反复查同一不存在 key
布隆过滤器 极省内存地判断元素是否存在,说"不存在"一定准,可彻底挡穿透
缓存击穿 某个热点 key 过期瞬间,大量并发同时未命中、一起打库
互斥锁 用 SETNX 让只有一个线程去查库重建,其余等待,防击穿
逻辑过期 缓存不真过期,过期时间写进 value,过期后返旧值并异步重建
缓存雪崩 大批 key 同时过期,或 Redis 整体宕机,流量集中砸向数据库
随机过期 过期时间叠加随机量,把集中过期打散,防雪崩成因之一
降级限流 缓存失效时用信号量限制并发查库数,超出快速失败,保住数据库

避坑清单

  1. 加了缓存不等于数据库被保护;缓存的保护建立在"绝大多数请求命中缓存"这个前提上。
  2. 缓存穿透查的是不存在的数据,缓存永远 miss;查不到也要写缓存(空值标记),并设较短过期。
  3. 空值缓存挡不住"每次用不同的不存在 ID"的攻击,需用布隆过滤器在查缓存前先拦掉。
  4. 缓存击穿查的是真实存在的热点数据,问题在它过期的瞬间被大量并发同时打库。
  5. 防击穿用互斥锁(只放一个线程查库重建)或逻辑过期(返旧值 + 异步重建),按"怕慢还是怕旧"取舍。
  6. 缓存雪崩有两种成因:大批 key 同时过期、Redis 整体宕机,要分别应对。
  7. 批量设缓存时别用相同 TTL,给过期时间叠加随机量,把集中过期打散开。
  8. Redis 用主从哨兵或集群别用单实例;并接受"缓存会整体失效",用降级限流给数据库兜底。
  9. 更新数据时缓存与数据库会不一致;延迟双删能缓解但不能根除,务必给缓存设过期时间兜底。
  10. 警惕大 key 和热 key;系统上线前做缓存预热;务必监控命中率,它骤降是三大问题的前兆。

总结

回头看那次压测把数据库打垮的事故,最该记住的不是某一个具体的防护代码,而是我上线前那个想当然的判断——"加了缓存,数据库就安全了"。这个判断错在它把缓存当成了一堵无条件挡住一切的墙。可缓存从来不是墙,它是一条有特定薄弱点的堤坝:不存在的数据会从穿透处漏过去,热点 key 过期会在击穿处裂开一道口子,大批 key 同时失效会让整段堤坝一起垮掉。你不去逐个加固这些薄弱点,它平时看着固若金汤,高峰期却会从最意想不到的地方溃决。

所以用好缓存,核心是想清楚一件事:我的缓存,会在什么情况下大面积失去保护作用?这篇文章的三大杀手,其实就是这个问题的三个标准答案——查不存在的数据(穿透)、热点 key 过期的瞬间(击穿)、大批 key 同时失效或 Redis 宕机(雪崩)。把它们对应的防护一一就位:布隆过滤器和空值缓存堵住穿透,互斥锁和逻辑过期顶住击穿,随机过期和降级限流扛住雪崩,再加上对一致性的清醒认识——你的缓存方案才算是经得起高峰检验的。

你会发现,这三种防护背后是同一种工程思维:不要假设理想情况,要主动设想"最坏会怎样",并为最坏情况准备好应对。穿透,是设想"有人专门查不存在的数据";击穿,是设想"最热的那个 key 偏偏在最忙的时候过期";雪崩,是设想"整个 Redis 突然就没了"。优秀的系统设计,从来不是把正常流程写得多漂亮,而是把每一种异常情况都想到了、都接住了。缓存只是一个具体的舞台,这套"为最坏情况做准备"的思维,在限流、熔断、容灾里,处处都用得上。

最后想说,缓存的三大问题,有一个共同的残酷之处:它们在 Demo 和日常低峰期完全不会暴露。平时命中率高得很,穿透?偶尔几个不存在的请求而已;击穿?并发量根本不够触发;雪崩?key 又没多到能压垮库。它们只在真实的高并发、真实的流量高峰、真实的恶意请求面前才会瞬间引爆——而那一刻,往往就是你最不希望出事的大促当天。所以别等监控告警把你从睡梦中叫醒,在你写下第一行 redis.get 的时候就该问自己:这个 key 查不到怎么办?它过期的那一秒怎么办?整个 Redis 没了怎么办?这三个问题都有了答案,你的缓存才不只是 Demo 里那个漂亮的命中率数字,而是一道真正能在洪峰面前守住数据库的堤坝。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

RAG 完全指南:从一次"AI 答得头头是道却全是错的"看懂检索增强生成

2026-5-21 18:29:46

技术教程

LLM 流式输出完全指南:从一次"前端等了 20 秒白屏"看懂 SSE 流式响应

2026-5-21 18:41:43

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索