1. 项目背景

1.1 事故回放

周三上午 10 点,运营按计划推送了一条"爆款机械键盘 5 折"的 Push 通知。瞬间,商品详情接口 QPS 从 2000 飙到 25000。10 分钟后,告警群开始刷屏——数据库 CPU 从 30% 打到 95%,连接池耗尽,商品详情接口大面积超时。DBA 紧急加了只读副本,但数据库仍然喘不过气。

事后复盘发现三个并发问题同时触发:

  1. 缓存穿透:有人在 Push 发出的同时用脚本批量请求 product/-1、product/-2、product/-99999——这些商品 ID 在数据库中根本不存在,缓存也无数据,每次请求都直接砸到数据库做无用查询。

  2. 缓存击穿:机械键盘的缓存刚好在 Push 前 30 秒自然过期。Push 一到,所有请求同时发现缓存 miss,几千个请求同时回源数据库查同一件商品。

  3. 缓存雪崩:运营在前一天晚上批量预热了 5000 件活动商品缓存,全设了相同的 600 秒 TTL。活动开始后第 10 分钟,5000 个 key 几乎同时过期,数据库瞬间承受了 5000 个不同的查询。

这三个问题就是缓存系统中著名的"三兄弟"。它们名字相似但成因不同,需要不同的治理方案。

1.2 三兄弟一句话定义

问题 一句话 核心特征
缓存穿透 请求的数据缓存里没有,数据库里也没有 每次请求都穿透缓存打数据库
缓存击穿 热点数据缓存刚好过期,大量请求同时回源 单个 key 的瞬时并发问题
缓存雪崩 大量 key 同时失效或缓存层不可用 批量 key 的系统可用性问题

1.3 本章目标

围绕商品详情接口做三层治理:

  • 穿透治理:参数校验 → 空值缓存 → Bloom Filter(三层防线)。
  • 击穿治理:互斥锁重建 → 逻辑过期 → 热点永不失效。
  • 雪崩治理:TTL 随机化 → 多级缓存 → 限流降级。

2. 项目设计

2.1 第一幕:穿透——有人拿假饭票排队

小胖:"用户查不存在的商品,返回空不就行了?为什么会把数据库打挂?"

小白:"因为缓存里没有 product:99999999 的 key,每次请求都穿透缓存到数据库——数据库也查不到,返回空,但这个过程走了完整的 SQL 执行路径。攻击者只要不停换不存在的 ID,这个空查询就永远被缓存加速不了。"

大师:"穿透的本质是'请求绕过了缓存层直接打到数据库'。我们设三道防线:"

穿透三层防线:

  请求到达
    │
    ▼
  ┌─────────────────┐
  │ 第1层: 参数校验   │ ← ID 格式/范围不合法的直接拒绝(0 成本)
  └────────┬────────┘
    │ 通过
    ▼
  ┌─────────────────┐
  │ 第2层: Bloom     │ ← 判断 ID "一定不存在" → 直接返回空(极小内存)
  │       Filter     │    判断 ID "可能存在"  → 继续
  └────────┬────────┘
    │ 可能存在
    ▼
  ┌─────────────────┐
  │ 第3层: 空值缓存   │ ← 数据库返回 null → 写入短 TTL 的空标记
  └────────┬────────┘    下次请求命中空标记 → 不查数据库
    │
    ▼
  查数据库(最后防线)

技术映射:穿透治理是逐层拦截——越靠前的防线成本越低,拦截越多。参数校验几乎零成本,Bloom Filter 占用极小内存,空值缓存是最后的兜底。

2.2 第二幕:击穿——全校抢一份红烧肉

小胖:"热点商品是真实存在的,缓存过期了再查一次数据库不就行了?"

小白:"问题不在'查一次',而在'几千个请求同时查'。热点商品缓存过期的瞬间,所有请求都发现 miss,同时去查数据库。数据库瞬间承受几千个完全相同的查询——这就是击穿。"

大师:"击穿治理的核心是控制回源并发——让只有一个请求去查数据库,其他请求要么等结果、要么返回旧数据。"

击穿的两种解法:

方案 A: 互斥锁重建(等新数据)
  ┌──────────┐     获取锁     ┌──────────┐
  │ 请求1-999 │─── 失败 ───▶ │ sleep+重试 │──▶ 读到新缓存
  └──────────┘               └──────────┘
  ┌──────────┐     获取锁     ┌──────────┐
  │ 请求1000  │─── 成功 ───▶ │ 查DB+写缓存 │
  └──────────┘               └──────────┘
  优点: 数据最新
  缺点: 获取锁失败的请求会等待

方案 B: 逻辑过期(先返回旧数据)
  缓存 value = {data: ..., expireAt: 1777550000}
  → 读缓存时判断 expireAt:
    - 未过期 → 直接返回 data
    - 已过期 → 返回旧 data + 异步线程去刷新缓存
  优点: 永不等C待
  缺点: 短暂返回旧数据

技术映射:互斥锁适合"宁可慢一点,也要新数据"的场景(如价格);逻辑过期适合"宁可看旧数据,也不能卡"的场景(如推荐列表)。

2.3 第三幕:雪崩——食堂窗口同时关门

小胖:"雪崩听起来像一大片商品同时没缓存,是不是把 TTL 改长就行?"

大师:"改长只能推迟。雪崩的成因不只有'TTL 集中过期'——Redis 重启、主从切换、网络分区、内存淘汰,都可能导致大量缓存同时不可用。应对雪崩要从四个维度设计:"

雪崩治理四维度:

  1. TTL 维度: 随机抖动,分散过期时间
     基础 600s + random(0, 120) = 分散在 600~720s

  2. 多级缓存维度: 本地缓存(Caffeine) + Redis + 数据库
     Redis 挂了还有本地缓存兜底

  3. 限流降级维度: 
     - 缓存未命中时限制回源并发数(信号量)
     - Redis 不可用时返回兜底数据(静态 JSON)
     - 非核心数据直接降级隐藏

  4. 高可用维度: 主从+Sentinel 减少 Redis 不可用概率
     (第 23-24 章展开)

小白追问:"雪崩和击穿有什么本质区别?"

大师:"击穿是一个热点 key 的问题——你只需要保护那一个 key。雪崩是系统层面的问题——大量 key 同时失效或缓存层整体不可用。击穿用锁解决,雪崩用架构韧性解决。"


3. 项目实战

3.1 环境准备

docker exec -it redis-lab redis-cli -a redis123

3.2 实战一:穿透三件套——参数校验+空值缓存+Bloom Filter

步骤 1:参数校验(第一层防线)

public Product getProduct(String productId) {
    // 第1层: 参数校验——非法 ID 直接拒绝
    if (productId == null || !productId.matches("\\d{1,10}")) {
        return null;  // 不走 Redis,不走数据库
    }
    long id = Long.parseLong(productId);
    if (id <= 0 || id > 99999999) {
        return null;
    }
    // ... 继续缓存逻辑
}

步骤 2:空值缓存(第三层防线——先实践最简单的)

# 查一个不存在的商品,写入空标记
127.0.0.1:6379> SET mall:cache:product:99999:null "__NULL__" EX 60
OK

# 60 秒内再次查询 → 命中空标记 → 不查数据库
127.0.0.1:6379> GET mall:cache:product:99999:null
"__NULL__"
public Product getProduct(String productId) {
    // ... 参数校验 ...

    String cacheKey = "mall:cache:product:" + productId;
    String nullKey = cacheKey + ":null";

    // 1. 先查缓存
    String cached = redis.get(cacheKey);
    if (cached != null) {
        return "__NULL__".equals(cached) ? null : deserialize(cached);
    }

    // 2. 空值缓存检测(防穿透)
    if (Boolean.TRUE.equals(redis.hasKey(nullKey))) {
        return null;  // 短期内已经确认不存在,不查数据库
    }

    // 3. 回源数据库
    Product product = database.findProduct(productId);
    if (product != null) {
        redis.set(cacheKey, serialize(product), Duration.ofSeconds(600 + random.nextInt(120)));
    } else {
        redis.set(nullKey, "__NULL__", Duration.ofSeconds(60));  // 空值短 TTL
    }
    return product;
}

步骤 3:Bloom Filter(第二层防线——高性能拦截)

# 如果安装了 RedisBloom 模块:
127.0.0.1:6379> BF.RESERVE mall:bf:product 0.01 1000000
OK
127.0.0.1:6379> BF.ADD mall:bf:product 1001
(integer) 1
127.0.0.1:6379> BF.EXISTS mall:bf:product 1001
(integer) 1     # 可能存在
127.0.0.1:6379> BF.EXISTS mall:bf:product 99999
(integer) 0     # 一定不存在!

Bloom Filter 的特点:如果返回"不存在"——100% 确定(不会有假阴性);如果返回"可能存在"——可能有 1% 误判率(假阳性)。这意味着 Bloom Filter 只能拦截一定不存在的 ID,不能替代精确判断。

// Bloom Filter 集成
public Product getProductWithBloom(String productId) {
    // 第2层: Bloom Filter——判断"一定不存在"
    if (!bloomFilter.mightExist("mall:bf:product", productId)) {
        return null;  // 一定不存在,直接返回
    }
    // "可能存在" → 继续走缓存+数据库路径
    return getProduct(productId);
}

没有 RedisBloom 模块时的替代方案(学习环境):

# 用 Set 维护所有有效商品 ID(内存成本高,学习可用)
127.0.0.1:6379> SADD mall:product:valid_ids 1001 1002 1003
127.0.0.1:6379> SISMEMBER mall:product:valid_ids 1001
(integer) 1
127.0.0.1:6379> SISMEMBER mall:product:valid_ids 99999
(integer) 0     # 不存在,拒绝

3.3 实战二:击穿治理——互斥锁重建

# 模拟热点商品缓存过期后的互斥锁

# 请求 A:尝试获取重建锁
127.0.0.1:6379> SET mall:lock:rebuild:product:1001 "req-A-001" NX EX 5
OK

# 请求 B:同时尝试获取(失败)
127.0.0.1:6379> SET mall:lock:rebuild:product:1001 "req-B-001" NX EX 5
(nil)

# 请求 A 重建完成后释放锁(Lua 校验 value)
127.0.0.1:6379> EVAL "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end" 1 mall:lock:rebuild:product:1001 "req-A-001"
(integer) 1
public class HotKeyProtector {
    private static final int LOCK_TTL = 5;
    private static final int RETRY_MS = 50;
    private static final int MAX_RETRY = 3;

    public Product getWithMutex(String productId) {
        String cacheKey = "mall:cache:product:" + productId;

        // 1. 先尝试读缓存
        String cached = redis.get(cacheKey);
        if (cached != null) return deserialize(cached);

        // 2. 未命中 → 互斥锁控制回源并发
        String lockKey = "mall:lock:rebuild:product:" + productId;
        String requestId = UUID.randomUUID().toString();

        for (int i = 0; i < MAX_RETRY; i++) {
            Boolean locked = redis.setIfAbsent(lockKey, requestId, Duration.ofSeconds(LOCK_TTL));
            if (Boolean.TRUE.equals(locked)) {
                // 获得锁 → 查库重建
                try {
                    Product product = database.findProduct(productId);
                    if (product != null) {
                        redis.set(cacheKey, serialize(product), Duration.ofSeconds(600 + random.nextInt(120)));
                    }
                    return product;
                } finally {
                    releaseLock(lockKey, requestId);
                }
            }
            // 没拿到锁 → 等一会儿重试读缓存
            safeSleep(RETRY_MS);
            cached = redis.get(cacheKey);
            if (cached != null) return deserialize(cached);
        }

        // 3. 重试耗尽 → 直接查库(降级策略)
        log.warn("互斥锁重建失败,降级直接查库: {}", productId);
        return database.findProduct(productId);
    }
}

3.4 实战三:击穿治理——逻辑过期

# 逻辑过期:value 中带 expireAt 字段,Redis key 不设短 TTL
127.0.0.1:6379> SET mall:hot:product:1001 '{"data":{"id":1001,"name":"机械键盘","price":299},"expireAt":1777550000}'
OK

# 读取时判断 expireAt:
#  - 未过期 → 直接返回 data
#  - 已过期 → 返回旧 data + 异步刷新
public class LogicalExpireHandler {

    @Data
    public static class CacheWrapper<T> {
        private T data;
        private long expireAt;  // 逻辑过期时间戳(毫秒)
    }

    private final ScheduledExecutorService refreshPool = Executors.newScheduledThreadPool(2);

    public Product getWithLogicalExpire(String productId) {
        String key = "mall:hot:product:" + productId;
        String cached = redis.get(key);

        if (cached == null) {
            // 首次加载(冷启动)
            return rebuildCache(key, productId);
        }

        CacheWrapper<Product> wrapper = deserializeWrapper(cached);

        // 判断是否逻辑过期
        if (wrapper.getExpireAt() > System.currentTimeMillis()) {
            return wrapper.getData();  // 未过期,直接返回
        }

        // 已过期 → 先返回旧数据,异步刷新
        refreshPool.submit(() -> rebuildCache(key, productId));
        return wrapper.getData();  // 返回旧数据(可能稍微过时)
    }

    private Product rebuildCache(String key, String productId) {
        String lockKey = "mall:lock:rebuild:product:" + productId;
        String requestId = UUID.randomUUID().toString();

        Boolean locked = redis.setIfAbsent(lockKey, requestId, Duration.ofSeconds(5));
        if (!Boolean.TRUE.equals(locked)) {
            // 已有其他线程在刷新 → 直接读缓存
            String cached = redis.get(key);
            if (cached != null) return deserializeWrapper(cached).getData();
            return database.findProduct(productId);
        }

        try {
            Product product = database.findProduct(productId);
            if (product != null) {
                CacheWrapper<Product> wrapper = new CacheWrapper<>();
                wrapper.setData(product);
                wrapper.setExpireAt(System.currentTimeMillis() + 600_000);  // 10 分钟后逻辑过期
                redis.set(key, serializeWrapper(wrapper));
            }
            return product;
        } finally {
            releaseLock(lockKey, requestId);
        }
    }
}

3.5 实战四:雪崩治理——TTL 抖动+本地缓存+降级

TTL 随机抖动:

// 每次写入缓存时,TTL = 基础值 + 随机偏移
int baseTtl = 600;
int jitter = ThreadLocalRandom.current().nextInt(120);
redis.set(key, value, Duration.ofSeconds(baseTtl + jitter));

多级缓存兜底:

// L1: Caffeine 本地缓存(30s TTL, 1000 条)
private final Cache<String, Product> localCache = Caffeine.newBuilder()
    .maximumSize(1000)
    .expireAfterWrite(30, TimeUnit.SECONDS)
    .build();

public Product getWithFallback(String productId) {
    String key = "mall:cache:product:" + productId;

    try {
        // 1. L1 本地缓存
        Product cached = localCache.getIfPresent(key);
        if (cached != null) return cached;

        // 2. L2 Redis
        String redisJson = redis.get(key);
        if (redisJson != null) {
            Product p = deserialize(redisJson);
            localCache.put(key, p);
            return p;
        }

        // 3. 数据库
        Product p = database.findProduct(productId);
        if (p != null) {
            redis.set(key, serialize(p), Duration.ofSeconds(600 + random.nextInt(120)));
            localCache.put(key, p);
        }
        return p;

    } catch (Exception e) {
        // Redis 不可用 → 降级: 本地缓存兜底
        Product local = localCache.getIfPresent(key);
        if (local != null) {
            log.warn("Redis 不可用,降级到本地缓存: {}", productId);
            return local;
        }
        // 再降级: 返回静态兜底数据
        log.error("所有缓存层不可用,返回兜底数据: {}", productId);
        return Product.FALLBACK;
    }
}

3.6 实战五:事故复盘模板

每次缓存事故后必须完成以下复盘:

## 事故复盘: [日期] [事故简述]

### 1. 影响范围
- 影响接口: [商品详情/下单/...]
- 影响时长: [N 分钟]
- P95 延迟变化: [5ms → 900ms]
- 数据库 QPS 变化: [2000 → 15000]

### 2. 根因分析
- [ ] 缓存穿透? (哪些 ID 在恶意请求?)
- [ ] 缓存击穿? (哪个热点 key 过期?)
- [ ] 缓存雪崩? (哪些 key 同时过期 / Redis 是否重启?)
- [ ] 其他? (网络/配置/代码变更)

### 3. 已采取措施
- [加空值缓存 / 加互斥锁 / TTL 加随机抖动 / ...]

### 4. 待改进项
- [ ] Bloom Filter 覆盖不全
- [ ] 本地缓存未启用
- [ ] 限流降级策略缺失
- [ ] 监控告警阈值不合理

3.7 实战六:布隆过滤器的维护与更新

Bloom Filter 最大的痛点——当新商品入库时,需要更新 Bloom Filter。如果重建代价太大,就需要考虑增量写入:

# 新商品上线 → 加入 Bloom Filter
BF.ADD mall:bf:product 1004

# 定期全量重建(低峰期执行)
# 从数据库加载所有有效商品 ID,重建 Bloom Filter
# 这样做能修正 Bloom Filter 中的假阳性积累
@Component
public class BloomFilterMaintainer {

    // 商品上线时增量写入
    public void onProductCreated(String productId) {
        bloomFilter.add("mall:bf:product", productId);
    }

    // 每日凌晨全量重建(修正假阳性积累)
    @Scheduled(cron = "0 0 3 * * *")
    public void rebuildBloomFilter() {
        log.info("开始重建 Bloom Filter...");
        // 1. 创建新的 Bloom Filter(临时名)
        bloomFilter.reserve("mall:bf:product:new", 0.01, 2000000);

        // 2. 分批从数据库加载所有有效商品 ID
        int offset = 0;
        int batchSize = 10000;
        List<String> ids;
        do {
            ids = database.getValidProductIds(offset, batchSize);
            bloomFilter.multiAdd("mall:bf:product:new", ids);
            offset += batchSize;
        } while (!ids.isEmpty());

        // 3. 原子替换
        redis.rename("mall:bf:product:new", "mall:bf:product");
        log.info("Bloom Filter 重建完成");
    }
}

3.8 实战七:限流降级——信号量保护数据库

当缓存全部 miss 时,不能用"无限制回源"来应对——这会压垮数据库。需要信号量限制并发回源数:

@Component
public class DatabaseProtector {

    private final Semaphore dbSemaphore = new Semaphore(100);  // 最多 100 并发回源

    public Product getProductWithProtection(String productId) {
        String cacheKey = "mall:cache:product:" + productId;

        // 1. 查缓存
        String cached = redis.get(cacheKey);
        if (cached != null) return deserialize(cached);

        // 2. 尝试获取回源许可
        if (!dbSemaphore.tryAcquire(100, TimeUnit.MILLISECONDS)) {
            // 获取失败 → 降级
            log.warn("回源并发已满,降级处理: {}", productId);
            metricsCollector.record("cache.db.fallback");
            return Product.FALLBACK;  // 返回兜底数据
        }

        try {
            // 双重检查:可能其他线程已经重建了缓存
            cached = redis.get(cacheKey);
            if (cached != null) return deserialize(cached);

            return rebuildCache(cacheKey, productId);
        } finally {
            dbSemaphore.release();
        }
    }
}

4.3 生产事故案例库

时间 公司 事故简述 根因 教训
2021 某电商 大促缓存预热后 10 分钟数据库被打爆 5000 个 key 相同 TTL 同时过期(雪崩) TTL 永远加随机抖动
2022 某社交 恶意脚本请求不存在的用户 ID,数据库 CPU 100% 无空值缓存(穿透) 空值缓存+Bloom Filter
2023 某金融 热点理财产品缓存过期,全部请求打到主库 无互斥锁/逻辑过期(击穿) 热点 key 用逻辑过期
2023 某出行 Redis 网络闪断 3 秒后恢复,所有业务缓存全 miss 无本地缓存兜底(雪崩) 多级缓存降级
2024 某视频 推荐流缓存批量过期,数据库被打挂 30 分钟 相同 TTL + 无降级策略 TTL 抖动+限流熔断

4.4 缓存治理成熟度模型

Level 1: 无缓存
  → 全量查数据库,无 Redis

Level 2: 基础缓存
  → 用了 Cache Aside,但无穿透/击穿/雪崩防护

Level 3: 初级防护
  → 空值缓存 + TTL 抖动 + 简单互斥锁

Level 4: 系统化防护(本章目标)
  → Bloom Filter + 逻辑过期 + 多级缓存 + 限流降级

Level 5: 自适应防护
  → 动态 TTL(根据访问频率调整)+ 热点自动发现 + 预热策略自动化

5. 本章实战环境脚本

#!/bin/bash
# 缓存治理实验脚本

# 1. 模拟穿透:批量请求不存在的商品
echo "=== 模拟穿透攻击 ==="
for i in $(seq 1 1000); do
  redis-cli -a redis123 GET "mall:cache:product:$i" > /dev/null 2>&1
done
echo "穿透攻击完成,检查数据库 QPS..."

# 2. 模拟击穿:删除热点 key 后并发请求
echo "=== 模拟击穿 ==="
redis-cli -a redis123 DEL mall:cache:product:8888
for i in $(seq 1 100); do
  redis-cli -a redis123 GET mall:cache:product:8888 > /dev/null 2>&1 &
done
wait
echo "击穿模拟完成,检查互斥锁..."

# 3. 模拟雪崩:批量删除
echo "=== 模拟雪崩 ==="
redis-cli -a redis123 --scan --pattern "mall:cache:product:*" | head -500 | \
  xargs -I {} redis-cli -a redis123 DEL {}
echo "雪崩模拟完成,检查命中率骤降..."

6. 本章核心代码速查

场景 核心代码 关键参数
空值缓存 SET key:null "__NULL__" EX 60 TTL 30-120s
互斥锁 SET lock:rebuild NX EX 5 TTL 略大于查询耗时
逻辑过期 {data:{...}, expireAt:timestamp} key 本身不设短 TTL
TTL 抖动 int ttl = 600 + random.nextInt(120) 最大抖动不超过基础值 20%
Bloom Filter BF.RESERVE bf 0.01 1000000 误判率 1%,100 万容量
信号量限流 new Semaphore(100) 最大并发回源 100

5.1 日常巡检清单

缓存系统的健康需要定期检查:

每日检查:
  □ 缓存命中率 > 80%?(INFO stats keyspace_hits/misses)
  □ 数据库 QPS 是否正常?(对比接入缓存前后的基线)
  □ 慢查询数量?(SLOWLOG GET 10)

每周检查:
  □ 空值缓存 key 数量是否异常?
  □ Bloom Filter 的误判是否导致穿透回源?
  □ 互斥锁等待时间 P99 是否在预期内?

每月检查:
  □ 热点 key 列表是否需要更新?
  □ TTL 策略是否需要调整?
  □ 降级策略是否需要演练?

4. 项目总结

4.1 三兄弟治理速查

问题 核心解法 Redis 命令 适用条件
穿透 参数校验 无(应用层) 所有场景
穿透 空值缓存 SET key:null "__NULL__" EX 60 数据库返回 null
穿透 Bloom Filter BF.EXISTS 高流量+低误判容忍
击穿 互斥锁 SET lock NX EX 5 + Lua 释放 可接受短暂等待
击穿 逻辑过期 value 中存 expireAt 可接受短暂旧数据
雪崩 TTL 抖动 EX 600+random(0,120) 所有批量写入
雪崩 多级缓存 Caffeine + Redis 热点极高数据
雪崩 限流降级 应用层信号量/熔断 Redis 不可用时

4.2 思考题

  1. 设计题:如果商品详情接口 QPS 达到 10 万,同时 Bloom Filter 误判率设为 0.1%,所需内存是多少?如果 Bloom Filter 的误判导致一个不存在的商品 ID 穿透到数据库,后续的防御机制是什么?

  2. 场景题:逻辑过期返回旧数据,在什么业务场景下是不可接受的?请举出至少 3 个例子,并说明为什么。

(答案提示将在下一章末尾给出。)


上一章思考题答案提示(第16章):

第1题(1000 万日活优化):最可能出问题的模块:① 排行榜 ZSet——单个 key 承载全站排名,成为热 key → 按小时/地区拆分 shard;② DAU Bitmap——单 key 大小膨胀到 12.5MB → 用户 ID 映射为连续内部编号;③ Set 社交关系——大 V 的粉丝 Set 过大 → 只在 Redis 存前 1000 粉丝,其余放数据库。

第2题(退货库存回补):Lua 脚本增加一个逻辑——如果操作是"退款"类型,则执行 INCRBY stockKey quantity(回补)。需要新增参数标识操作类型(购买/退款),并在业务层验证退款请求的合法性(是否真的买过、是否已退过)。

延伸阅读与资源

Redis 8 实战精讲:从 CRUD 到源码,构建高可用缓存系统
Java 工程师进阶:从 JVM 生产排障到OpenJDK原理
MySQL Server 9从入门到进阶的实战之旅
Nacos 3.x 注册配置中心实战修炼:从入门到源码扩展
Elasticsearch从入门到进阶的实战之旅
RabbitMQ从入门到进阶的实战之旅:从单机到大促高可用架构
Celery 入门到进阶之路:从异步任务到自研调度平台
LangGraph 生产级实战进阶:从零到生产级Agent工作流开发
Dify 从入门到源码:LLM 应用平台实战修炼
从零到生产级:FastAPI 异步高并发、源码与 SRE 实战
实战SQLAlchemy 2.0: 从 CRUD 到生产级架构
从零打造企业级 AI 助手:LangChain RAG、Agent 与生产实战
后端工程师 AI 转型课:Ollama 私有化大模型从入门到生产
MongoDB 实战进阶与内核修炼
NumPy 从入门到生产落地:全链路实战指南(科学计算/向量化/性能调优)
Milvus向量数据库实战修炼:从 0 到 1 精通向量检索与生产落地
Python 3实战精进:从脚本到高并发订单引擎
python入门:Rquests从菜鸟脚本到企业级SDK的网络实战圣经

posted on 2026-11-12 10:42  一天不进步,就是退步  阅读(9)  评论(0)    收藏  举报