缓存穿透、击穿、雪崩完全指南:从一次"加了 Redis,数据库还是被打挂"看懂缓存防护

2022 年我做一个电商的商品详情接口。这个接口 QPS 很高首页搜索推荐到处都在调它。第一版我直接查数据库压测一上量数据库 CPU 就飙红。我马上想到加缓存查商品时先查 Redis 有就直接返回没有再查数据库查完写回 Redis。这套旁路缓存加上去效果立竿见影数据库 CPU 应声降下来。我当时心里很踏实有缓存挡着数据库稳了。可上线之后数据库还是三番五次地被打挂每次现象还不一样。第一次有人在疯狂查一批根本不存在的商品 ID 这些请求全部绕过了缓存直怼数据库。第二次某个大促爆款商品它的缓存恰好过期的那一秒成千上万个请求在同一瞬间涌向数据库。第三次某天凌晨一大批缓存几乎同时过期数据库瞬间被压垮。我盯着这三次各不相同的事故想了很久才彻底想明白第一版错在一个根本的认知上我以为加了缓存数据库就被保护了。可这句话只对了一半缓存只在它被命中的时候保护数据库一旦没命中无论是因为数据压根不存在还是 key 恰好过期还是一大批 key 集体过期流量就会穿过这层缓存原封不动地砸到数据库上。这三次事故正对应着缓存最经典的三个漏点缓存穿透缓存击穿缓存雪崩。本文从头梳理为什么加了缓存数据库还会被打挂缓存穿透是什么怎么防布隆过滤器怎么用缓存击穿怎么用互斥锁和逻辑过期挡住缓存雪崩怎么靠随机过期化解以及缓存一致性缓存预热这些把缓存真正做对要避开的坑。

2022 年我做一个电商的商品详情接口。这个接口QPS 很高,首页、搜索、推荐,到处都在调它。第一版我直接查数据库,压测一上量,数据库 CPU 就飙红。我马上想到加缓存:查商品时,先查 Redis,Redis 有就直接返回,没有再查数据库、查完写回 Redis。这套"旁路缓存"加上去,效果立竿见影——数据库 CPU 应声降下来,接口快得飞起。我当时心里很踏实:"有缓存挡着,数据库稳了。"可上线之后,数据库还是三番五次地被打挂,每次现象还不一样。第一次:监控显示有人在疯狂查一批根本不存在的商品 ID,这些请求全部绕过了缓存、直怼数据库。第二次:某个大促爆款商品,它的缓存恰好过期的那一秒,成千上万个请求在同一瞬间涌向数据库。第三次最壮观:某天凌晨,一大批缓存几乎同时过期,数据库瞬间被压垮。我盯着这三次各不相同的事故想了很久才彻底想明白,第一版错在一个根本的认知上:我以为"加了缓存,数据库就被保护了"。可这句话只对了一半。缓存,只在它被"命中"的时候保护数据库;而它一旦没命中——无论是因为数据压根不存在、还是 key恰好过期、还是一大批 key 集体过期——流量就会穿过这层缓存,原封不动地砸到数据库上。缓存不是一堵密不透风的墙,它是一张有可能漏的网。这三次事故,正对应着缓存最经典的三个"漏点":缓存穿透、缓存击穿、缓存雪崩。我以为加缓存就是"查之前先看一眼 Redis",结果真做下来,坑一个接一个。这篇文章就把它梳理一遍:为什么加了缓存数据库还会被打挂、缓存穿透是什么怎么防、布隆过滤器怎么用、缓存击穿怎么用互斥锁和逻辑过期挡住、缓存雪崩怎么靠随机过期化解,以及缓存一致性、缓存预热这些把缓存真正做对要避开的坑。

问题背景

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

现象:一个高 QPS 的商品详情接口,加了 Redis 旁路缓存后数据库压力骤降。但上线后数据库仍被三次打挂:一次是有人狂查不存在的商品 ID,请求全部绕过缓存;一次是热点商品缓存过期的瞬间,海量请求同时压向数据库;一次是大批 key 几乎同时过期,数据库瞬间崩溃。

我当时的错误认知:"加了缓存,所有读请求就都被缓存挡住了,数据库自然就安全了。"

真相:缓存只在命中时保护数据库。一旦没命中,流量会直接穿到数据库。没命中有三种典型成因,对应三种事故:数据本就不存在(缓存穿透)、单个热点 key 过期(缓存击穿)、大量 key 同时过期(缓存雪崩)。把缓存做对,不是"加上 Redis"就完事,而是要逐一堵上这三个漏点。

要把缓存防护做对,需要几块认知:

  • 为什么"加了缓存"数据库还会被打挂——缓存只在命中时有效;
  • 缓存穿透:查不存在的数据,怎么用空值缓存和布隆过滤器防;
  • 缓存击穿:热点 key 过期瞬间,怎么用互斥锁、逻辑过期挡住;
  • 缓存雪崩:大量 key 同时失效,怎么用随机过期时间打散;
  • 缓存一致性、缓存预热、降级这些工程坑怎么处理。

一、为什么"加了缓存"数据库还会被打挂

先把这件最根本的事钉死:缓存只在"命中"那条路径上保护数据库;只要请求"没命中"缓存,它就会原样落到数据库上。缓存挡住的是"重复读到的热数据",挡不住"没命中"本身。

下面这段代码,就是我那个"加了还会被打挂"的第一版——它是教科书式的旁路缓存(Cache-Aside),本身没写错:

import json
import redis

r = redis.Redis(host="localhost", port=6379)


def get_product_naive(pid: int) -> dict | None:
    # 旁路缓存:先查 Redis,没有再查库,查完写回 Redis。
    key = f"product:{pid}"
    cached = r.get(key)
    if cached is not None:
        return json.loads(cached)           # 缓存命中,直接返回
    # 缓存没命中 —— 落到数据库
    row = db_query_product(pid)
    if row is not None:
        r.set(key, json.dumps(row), ex=3600)  # 写回缓存,存 1 小时
    return row
    # 这段代码没写错,但它只在"命中"时保护数据库。
    # 一旦大量请求集中在"没命中"上,它们会一起砸向 db_query_product。

这段代码逻辑完全正确,在正常流量下也工作得很好——同一个热门商品被反复查,第一次落库,之后全走缓存。它的问题不在代码本身,而在一个错误的安全感:它默认"没命中"是偶尔、零星发生的小概率事件。可现实里,"没命中"会成片地、集中地爆发。我们把"没命中"拆开看,会发现它有三种截然不同的成因:

  • 缓存穿透:查的数据本来就不存在。数据库里没有,缓存里永远也不会有,每次查都必然落库。
  • 缓存击穿:某个热点 key 到点过期了。在它失效的那一瞬间,原本全走缓存的海量请求,同时没命中、同时落库。
  • 缓存雪崩:大量 key 在几乎同一时刻过期。一大片热数据的保护同时消失,数据库被瞬间压垮。

这三种,是三个不同的洞,得分别来堵。先看第一个,也是最容易被恶意利用的——缓存穿透。

二、缓存穿透:查一个永远不存在的东西

缓存穿透,指的是请求一个数据库里根本不存在的数据。它的致命之处在于:对于一个不存在的 ID,db_query_product 返回 None,于是上面那段代码不会写回缓存(因为 row is None)。结果——下一次查同一个不存在的 ID,还是没命中,还是落库。这个 ID永远命中不了缓存。如果有人恶意拿着一堆随机的、不存在的 ID 疯狂请求,等于缓存形同虚设,每一发都直接命中数据库

解法的第一招,朴素而有效:既然查不到,那就把"查不到"这件事本身也缓存起来。给不存在的 ID,在缓存里存一个特殊的空值标记

NULL_FLAG = "__NULL__"          # 特殊标记:代表"这个 ID 查过了,不存在"


def get_with_null_cache(pid: int) -> dict | None:
    """防穿透:把"查无此数据"也缓存起来,挡住对不存在 ID 的重复查询。"""
    key = f"product:{pid}"
    cached = r.get(key)
    if cached is not None:
        if cached == NULL_FLAG.encode():
            return None                  # 命中空值标记:直接返回,不落库
        return json.loads(cached)
    row = db_query_product(pid)
    if row is None:
        # 关键:数据不存在,也往缓存里写一个空值标记,
        # 但 TTL 设短一些(比如 60 秒)—— 万一这个 ID 之后真有了数据。
        r.set(key, NULL_FLAG, ex=60)
        return None
    r.set(key, json.dumps(row), ex=3600)
    return row

空值缓存把"不存在"也变成了一次命中,挡住了对同一个不存在 ID 的重复查询。但它有个软肋:如果攻击者每次用的都是全新的、不重样的随机 ID,那每个新 ID 第一次仍会落库一次,而且会在 Redis 里堆出海量的空值 key。要从根上堵住"大量不重样的不存在 ID",需要一件更专门的武器。

三、布隆过滤器:在查库之前就拦下不存在的 ID

对付"海量不重样的不存在 ID",需要一个能在查库之前就快速回答"这个 ID 有没有可能存在"的东西。这就是布隆过滤器(Bloom Filter)

它的特点要记准:布隆过滤器说一个 ID "不存在",那就一定不存在;它说"可能存在",那就可能存在(有极小概率误判)。换句话说,它没有漏报、只有极少误报。这个特性正好够用:我们把所有真实存在的商品 ID 预先灌进布隆过滤器;请求来时先问它——它说"不存在",就百分百能放心地直接拒绝,连缓存和数据库都不用碰

import hashlib


class BloomFilter:
    """极简布隆过滤器:用一个大 bit 数组 + 多个哈希函数。"""

    def __init__(self, size: int = 1 << 20, hash_count: int = 4):
        self.size = size
        self.hash_count = hash_count
        self.bits = bytearray(size // 8 + 1)

    def _hashes(self, value: str):
        for i in range(self.hash_count):
            h = hashlib.md5(f"{value}:{i}".encode()).hexdigest()
            yield int(h, 16) % self.size      # 每个哈希函数定位一个 bit

    def add(self, value: str):
        for pos in self._hashes(value):
            self.bits[pos // 8] |= (1 << (pos % 8))   # 把对应 bit 置 1

    def might_contain(self, value: str) -> bool:
        # 只要有一个 bit 是 0,就【一定】不存在;全是 1 才【可能】存在
        return all(self.bits[pos // 8] & (1 << (pos % 8))
                   for pos in self._hashes(value))

把布隆过滤器架在最前面,商品查询的第一道关就成了"问布隆":

# 服务启动时,把数据库里所有真实存在的商品 ID 灌进布隆过滤器
product_bloom = BloomFilter()
for pid in db_all_product_ids():
    product_bloom.add(str(pid))


def get_with_bloom(pid: int) -> dict | None:
    """防穿透终极版:查缓存查库之前,先问布隆过滤器。"""
    if not product_bloom.might_contain(str(pid)):
        # 布隆说"不存在" —— 那就一定不存在,直接拒绝,根本不碰 db
        return None
    # 布隆说"可能存在",才走正常的缓存 + 数据库流程
    return get_with_null_cache(pid)

布隆过滤器把"不存在的 ID"挡在了数据库门外。要记得:商品新增时,要同步把新 ID add 进布隆过滤器,否则新商品会被误判成不存在。穿透这个洞堵上了,接下来看第二个——比穿透更突然的击穿。

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

缓存击穿,和穿透完全是两回事。击穿的数据是真实存在的,而且通常还是个超级热点(比如大促爆款)——平时它稳稳地待在缓存里,海量请求全靠它命中。问题出在它过期的那一刹那:在缓存被删掉、新值还没写回的那个极短的时间窗里,本来全走缓存的成千上万个并发请求,会同时没命中、同时冲进数据库,去查同一行数据。数据库被这同一瞬间的尖峰瞬间打爆

解法的核心思路是:既然这成千上万个请求要查的是同一行数据,那就没必要让它们去查。只放一个请求去查库重建缓存,其余的稍等一下、等那一个把缓存建好,直接用就行。实现这个"只放一个"的,是一把互斥锁:

import time


def get_with_lock(pid: int) -> dict | None:
    """防击穿:热点 key 失效时,只放一个请求去重建缓存。"""
    key = f"product:{pid}"
    cached = r.get(key)
    if cached is not None:
        return json.loads(cached)
    # 没命中 —— 抢锁,只有抢到锁的那一个请求能去查库
    lock_key = f"lock:{key}"
    got_lock = r.set(lock_key, "1", nx=True, ex=10)   # SET NX 抢锁
    if not got_lock:
        # 没抢到锁:说明已有别人在重建缓存,稍等一下再读缓存
        time.sleep(0.05)
        return get_with_lock(pid)            # 重试:这次大概率能命中了
    try:
        row = db_query_product(pid)          # 抢到锁的,才查库
        if row is not None:
            r.set(key, json.dumps(row), ex=3600)
        return row
    finally:
        r.delete(lock_key)                   # 重建完,务必释放锁

互斥锁把"万箭齐发"收敛成了"一个请求查库、其余等结果"。它的代价是:没抢到锁的请求会多等那么几十毫秒。如果连这点延迟都不能忍,还有另一种思路——逻辑过期:缓存永不真正过期,只在 value 里存一个"逻辑过期时间";请求读到一个逻辑上已过期的值,照样先把这个旧值返回(不阻塞),同时另起一个后台任务去悄悄重建。

def get_with_logical_expire(pid: int) -> dict | None:
    """防击穿之逻辑过期:key 不设真实 TTL,过期了也先返回旧值,
    后台异步重建 —— 谁都不用等。"""
    key = f"product:{pid}"
    cached = r.get(key)
    if cached is None:
        return None                          # 约定:热点数据已提前预热进缓存
    data = json.loads(cached)
    if data["expire_at"] > time.time():
        return data["value"]                 # 逻辑上没过期,直接用
    # 逻辑上过期了:先把旧值返回(不阻塞用户),再触发后台重建
    if r.set(f"lock:{key}", "1", nx=True, ex=10):
        submit_async(rebuild_cache, pid)     # 异步重建,不阻塞当前请求
    return data["value"]                     # 当前请求拿的是"略旧"的数据

互斥锁和逻辑过期,是两种取舍:互斥锁保证数据新,代价是少数请求要等;逻辑过期保证不阻塞,代价是短时间内会读到旧值。按你的业务对"新"和"快"哪个更敏感来选。击穿是单个热点 key 的事;如果一大批 key 同时出事,那就是更大的灾难——雪崩。

五、缓存雪崩:大量 key 在同一时刻集体失效

缓存雪崩,是击穿的"群体版"。它的成因往往很不起眼:你在某次批量预热缓存时,把一大批商品在同一个循环里写进了 Redis,而且统一设了 ex=3600。于是这批 key,会在一小时后的同一秒,集体过期。那一秒,数据库要同时承受这一大批数据的重建请求,瞬间被压垮——这就是雪崩。

雪崩的根因,是 key 的过期时间太"整齐"了。解法也就顺理成章:给过期时间加一个随机扰动,把"集体过期"打散成"分散过期"。

import random


def set_with_jitter(key: str, value, base_ttl: int = 3600):
    """防雪崩:给 TTL 加一个随机扰动,避免大量 key 同时过期。"""
    # 在 base_ttl 基础上,随机上浮 0 ~ 600 秒。
    # 这样原本会"同一秒集体过期"的 key,被打散到 10 分钟的区间里。
    ttl = base_ttl + random.randint(0, 600)
    r.set(key, json.dumps(value), ex=ttl)


def warm_up_products(pids: list[int]):
    """缓存预热:服务上线前,把热点商品提前灌进缓存。
    注意每个 key 的 TTL 都带独立随机扰动,不会同时过期。"""
    for pid in pids:
        row = db_query_product(pid)
        if row is not None:
            set_with_jitter(f"product:{pid}", row)   # 每个 TTL 都不同

一行 random.randint,就把"一根尖峰"摊平成了"一段平缓的小坡"。除了随机 TTL,雪崩的防护通常还叠加两道保险:一是给数据库访问加限流和熔断——万一真有大批请求漏下来,也只放一部分进数据库,其余快速失败,绝不让数据库被彻底压垮;二是多级缓存,在 Redis 之外再加一层本地缓存,Redis 整个挂掉时还有本地缓存兜一下。三个洞——穿透、击穿、雪崩——到这里都堵上了。但缓存还有一类更安静的坑,不出事故,却会让你的数据悄悄出错

六、工程坑:缓存一致性、预热与降级

三个"被打挂"的洞堵上了,但缓存还有几个不出事故、却会让数据出错的工程坑。坑 1:数据更新时,缓存和数据库的一致性。当一个商品被修改,数据库和缓存两份数据就要同步。一个常见的错误是"先删缓存,再更新数据库":在你删了缓存、还没更新完数据库的空档里,另一个读请求进来,没命中缓存,从数据库读到了旧值,又把这个旧值写回了缓存——于是缓存里长期是错的。更稳妥的顺序是先更新数据库,再删除缓存:

def update_product(pid: int, new_data: dict):
    """更新商品:先改数据库,再删缓存(不是改缓存,是删)。"""
    db_update_product(pid, new_data)         # 1. 先把数据库改掉
    # 2. 再删缓存 —— 注意是【删除】不是【更新】。
    #    删掉它,下一次读自然会从新的数据库重建,简单又不易错。
    r.delete(f"product:{pid}")
    # 为什么是"删"不是"改":若两个更新并发,各自改缓存可能
    # 把缓存改成"后改的数据库 + 先改的缓存"这种错乱组合。
    # 而"删除"没有这个问题 —— 下一次读以数据库为准重建。

这里有两个要点。一是删缓存,而不是更新缓存:并发更新下,两个请求各自去缓存,可能改出错乱的组合;而是幂等的,删掉后下一次读以数据库为准重建,简单可靠。二是顺序先库后缓存。即便如此,理论上仍有极小的不一致窗口,要求强一致的话还需更复杂的方案(如订阅数据库 binlog 来删缓存),但对绝大多数业务,"先更新数据库、再删缓存"已经足够

坑 2:缓存预热不能漏。服务刚上线或 Redis刚重启时,缓存是空的。如果这时直接放全量流量进来,所有请求第一次全部没命中、全部落库——这本身就是一场人造的雪崩。所以上线前要用第五节的 warm_up_products热点数据预先灌好坑 3:Redis 挂了要能降级。缓存是性能优化,不该是系统命脉。如果你的代码在 Redis 连不上时直接抛异常,那 Redis 一挂、整个服务就跟着挂——缓存反而成了新的单点故障。正确的做法是降级:Redis 不可用时,捕获异常跳过缓存直接查数据库(同时配合限流,别把数据库压垮),让服务带病也能转坑 4:缓存的 key 要有规范。product:123 这样带业务前缀的命名,能避免不同业务的 key 互相撞车,排查问题时也一眼能认。下面这张图,把一次带完整防护的缓存查询串起来:

关键概念速查

概念 / 手段 说明
缓存只在命中时有效 没命中的请求会原样落到数据库,缓存挡不住没命中本身
缓存穿透 查数据库里本就不存在的数据,缓存永远不命中,每次都落库
空值缓存 把查无此数据也缓存成空标记,TTL 设短,挡住对同一不存在 ID 的重复查
布隆过滤器 说不存在就一定不存在,在查缓存查库前拦下海量不重样的不存在 ID
缓存击穿 单个热点 key 过期瞬间,海量请求同时没命中、同时落库查同一行
互斥锁重建 热点 key 失效时只放一个请求查库重建,其余请求稍等后读缓存
逻辑过期 key 不设真实 TTL,过期也先返回旧值,后台异步重建,谁都不用等
缓存雪崩 大量 key 在同一时刻集体过期,数据库瞬间承受大批重建被压垮
随机过期时间 给 TTL 加随机扰动,把集体过期打散成分散过期,削平尖峰
先更库再删缓存 更新时先改数据库再删缓存,删而不是改,下次读以数据库为准重建

避坑清单

  1. 别以为加了缓存数据库就安全,缓存只在命中时有效,没命中的请求会原样砸到数据库。
  2. 缓存穿透是查不存在的数据,这种 ID 永远不会被写回缓存,每次都落库,容易被恶意刷。
  3. 防穿透先用空值缓存,把查无此数据也缓存成空标记,但 TTL 要设短防止数据后来真有了。
  4. 对付海量不重样的不存在 ID 要用布隆过滤器,它说不存在就一定不存在,可在查库前拦下。
  5. 商品新增时要同步把新 ID 加进布隆过滤器,否则新商品会被误判成不存在而查不到。
  6. 缓存击穿是单个热点 key 过期瞬间海量请求同时落库,要用互斥锁只放一个请求去重建。
  7. 不能忍受互斥锁那点等待时,可用逻辑过期,过期也先返回旧值后台异步重建,代价是数据略旧。
  8. 缓存雪崩是大量 key 同时过期,根因是 TTL 太整齐,要给过期时间加随机扰动打散。
  9. 更新数据时要先更新数据库再删缓存,且是删不是改,并发改缓存可能改出错乱组合。
  10. 服务上线前要预热热点缓存,Redis 挂了要能降级直查数据库,别让缓存变成新的单点故障。

总结

回头看那三次"加了缓存、数据库还是被打挂"的事故,以及我后来在缓存防护上接连踩的坑,最该记住的不是某一段加锁代码,而是我动手前那个想当然的判断——"加了缓存,数据库就被保护了"。这句话错在它把缓存,想象成了一堵密不透风的墙:似乎只要墙立起来,所有流量就都被挡在墙外了。可缓存根本不是一堵墙,它是一张——一张大部分时候很管用、但天然就带着洞的网。它能牢牢兜住那些被反复读到的热数据,但它兜不住三种东西:它兜不住本来就不存在的数据(穿透),因为不存在的东西从来就没进过这张网;它兜不住热点 key 过期的那一刻(击穿),因为网上那一个最关键的结恰好断开了;它兜不住大批 key 同时过期(雪崩),因为半张网在同一瞬间整片消失了。缓存防护这件事想清楚的,正是这个——你不能只满足于"把网架起来",你得盯着这张网会从哪儿漏,然后一个洞一个洞地去补。

所以做缓存,真正的工程量不在"查之前先看一眼 Redis"那个一目了然的主干上。那个旁路缓存的主干,十几行代码就写完了,而且怎么测都对。真正的工程量,在于你有没有把那三个洞认出来、并分别堵上:面对"查不存在的数据",你有空值缓存布隆过滤器吗?面对"热点 key 失效的瞬间",你有互斥锁逻辑过期吗?面对"大批 key 集体过期",你的 TTL 带随机扰动吗?——而且这三个洞,形态完全不同:穿透是"数据不存在"的问题,击穿是"时间点集中"的问题,雪崩是"数量规模"的问题。它们名字相近、却不能用同一招对付。这篇文章的几节,其实就是顺着这三个洞展开的:先想清楚缓存为什么会漏,再逐一看穿透、击穿、雪崩各自怎么堵,最后是一致性、预热、降级这几个把缓存真正做对的工程细节。

你会发现,缓存防护的思路,和现实里给一个热门窗口分流人潮完全相通。一个景区的热门售票窗口,平时排队的人就是被"缓存"住的常规客流。可它也会遇到三种麻烦:有人拿着假票、根本不该来这个窗口反复纠缠(这是穿透,得在入口先验票拦下);窗口的售票员临时离岗那几分钟,憋了一长队人同时涌上来(这是击穿,得留一个人先顶上、别让队伍炸开);更糟的是所有窗口恰好同时换班(这是雪崩,所以换班时间得错开)。应对的智慧是相通的:不该进来的,在最外层就拦掉;关键节点失效时,别让所有人一拥而上;别让大批保护在同一刻一起消失。缓存的穿透、击穿、雪崩三道防线,本质上就是把这套朴素的分流智慧,翻译成了代码。

最后想说,缓存做没做扎实,差距永远不会在开发期暴露——开发时你点几下页面,缓存命中率高得很,有没有防穿透、TTL 有没有加随机,功能跑起来一模一样。它只在真实的、有恶意流量、有大促热点、有海量并发的生产环境里才显形。那时候它会用最难堪的方式给你结账:做不好,你会像我一样,在某个深夜被告警叫醒——数据库 CPU 100%,接口大面积超时,你登上去一查,要么是有人拿着不存在的 ID 在疯狂刷你,要么是一个爆款的缓存过期了把库击穿,要么是一整批缓存同时到期掀起雪崩。而做了,它会安安静静地、不被任何人察觉地,把恶意的不存在请求挡在布隆过滤器外,把热点过期的瞬间收敛成一个请求重建,把本会集体过期的 key 均匀打散到一段时间里——数据库那头的负载稳稳的、纹丝不动,大促流量再汹涌,它也波澜不惊。所以别等数据库被打挂的告警找上门,在你写下第一行"先查一下缓存"的代码时就该想清楚:有人查一个不存在的东西,我的缓存挡得住吗?一个热点缓存过期的那一瞬,会发生什么?我的一批缓存,会在同一秒一起过期吗?这几个问题都有了答案,你的缓存才不只是开发库里那个让接口变快的小优化,而是一道无论流量怎么冲、怎么刷,都能稳稳替数据库扛住的可靠防线。

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

RAG 完全指南:从一次"把整个知识库塞进 prompt、模型却答得驴唇不对马嘴"看懂检索增强生成

2026-5-21 21:15:14

技术教程

大模型流式输出完全指南:从一次"用户盯着空白屏幕等了十几秒以为卡死"看懂 SSE 流式响应

2026-5-21 21:54:55

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