1. 项目背景
1.1 事故回放
周三上午 10 点,运营按计划推送了一条"爆款机械键盘 5 折"的 Push 通知。瞬间,商品详情接口 QPS 从 2000 飙到 25000。10 分钟后,告警群开始刷屏——数据库 CPU 从 30% 打到 95%,连接池耗尽,商品详情接口大面积超时。DBA 紧急加了只读副本,但数据库仍然喘不过气。
事后复盘发现三个并发问题同时触发:
-
缓存穿透:有人在 Push 发出的同时用脚本批量请求
product/-1、product/-2、product/-99999——这些商品 ID 在数据库中根本不存在,缓存也无数据,每次请求都直接砸到数据库做无用查询。 -
缓存击穿:机械键盘的缓存刚好在 Push 前 30 秒自然过期。Push 一到,所有请求同时发现缓存 miss,几千个请求同时回源数据库查同一件商品。
-
缓存雪崩:运营在前一天晚上批量预热了 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 思考题
-
设计题:如果商品详情接口 QPS 达到 10 万,同时 Bloom Filter 误判率设为 0.1%,所需内存是多少?如果 Bloom Filter 的误判导致一个不存在的商品 ID 穿透到数据库,后续的防御机制是什么?
-
场景题:逻辑过期返回旧数据,在什么业务场景下是不可接受的?请举出至少 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的网络实战圣经

微信公众号: 架构师日常笔记 欢迎关注!
浙公网安备 33010602011771号