
缓存雪崩救星来了NestJS RedisX 防击穿机制原理与实战【免费下载链接】nestjs-redisxModular Redis toolkit for NestJS with plugin architecture - caching, locks, rate limiting, circuit breaker, pub/sub, idempotency, streams, metrics tracing项目地址: https://gitcode.com/gh_mirrors/ne/nestjs-redisx缓存雪崩是高并发系统最隐蔽的杀手之一。当热点缓存在同一秒集体过期成千上万的请求会同时穿透缓存直击数据库瞬间打满连接池、拖垮下游服务。NestJS RedisX正是为解决这类问题而生的模块化 Redis 工具包它的缓存插件内置了一套完整的防击穿Anti-Stampede机制——在本机进程内合并并发请求跨实例用分布式锁协调回源让 100 个并发请求只产生 1 次数据库查询。本文带你读懂它的原理并给出可直接落地的配置实战。缓存雪崩与缓存击穿先分清两个高频故障很多新手把雪崩和击穿混为一谈但它们的成因和应对完全不同缓存击穿Cache Breakdown某个热点 Key突然过期大量请求同时打到数据库。RedisX 的防击穿stampede protection正是针对这个场景。缓存雪崩Cache Avalanche大量 Key 在同一时间过期导致数据库被集中轰炸。需要错峰过期 防击穿 预热组合拳。缓存穿透查询一个根本不存在的 Key每次都穿透缓存。需要布隆过滤或空值缓存兜底。RedisX 的防击穿机制能同时缓解前两类故障它保证同一个 Key 在任何时刻只允许一个请求回源无论这个 Key 是被热点请求击穿还是被批量过期雪崩波及。防击穿核心原理两层防护架构RedisX 的防击穿采用两层防护既解决单机内的并发合并又解决跨实例的重复回源第 1 层本地 Singleflight进程内 ├─ 合并同一进程内的并发请求 ├─ 等待者共享同一个 Promise零轮询 └─ 不产生任何 Redis 调用零额外延迟 第 2 层分布式 Redis 锁跨进程 ├─ 通过 SET NX EX 原子命令抢锁 ├─ 锁 Key 格式_stampede:{cacheKey} ├─ 用 Lua 脚本释放锁只有持有者能释放 └─ 防止多实例同时加载数据两层防护的源码集中在 packages/cache/src/stampede/ 目录核心实现是 stampede-protection.service.ts。在mode: l1-only纯内存模式下没有 Redis 锁单进程内的 singleflight 依然是完整且正确的防护。一次完整的防击穿请求旅程用getOrSet()或Cached装饰器时防击穿是默认开启的。请求进来后会发生这些事缓存未命中进入getOrSet()流程检查本地是否有同名请求正在进行——有就直接等待它共享的 Promise没有则同步注册一个新的 flight在任何异步操作之前完成注册杜绝空隙窗口尝试用SET _stampede:{key} {value} EX {ttl} NX抢分布式锁锁被别的实例持有在waitTimeout内轮询等待每 50ms 一次等锁释放后重新读缓存直接返回别的实例写好的值——全局只回源一次抢锁成功执行加载器回源拿数据在持锁状态下写回缓存用加载结果 resolve 所有本地等待者通过 Lua 脚本释放锁只有所有者才能释放杜绝误删。整个等待过程本地等待者用的是Promise.race()而非轮询跨实例等待才需要轮询锁 Key。加载完成的 flight 会同步移除因此失效后再次访问一定重新读缓存绝不会拿到失效前的旧值。防击穿核心配置项配置项默认值说明enabledtrue是否全局开启防击穿lockTimeout5000加载器最大执行时间毫秒同时用作 Redis 锁 TTLwaitTimeout10000等待者等待回源结果的最长时间毫秒fallbackload抢锁失败时的行为load继续加载 /error抛错 /null返回 null最简开启配置示例import { CachePlugin } from nestjs-redisx/cache; CacheModule.forRoot({ plugins: [ new CachePlugin({ stampede: { enabled: true, // 默认就是开启的 lockTimeout: 5000, // 回源最久 5 秒 waitTimeout: 10000, // 等待者最多等 10 秒 fallback: load, // 抢锁失败也照常加载 }, }), ], });实战用 getOrSet 一键获得防击穿能力最推荐的方式是通过 Service API 的getOrSet()防击穿开箱即用import { Injectable } from nestjs/common; import { CacheService } from nestjs-redisx/cache; Injectable() export class ProductService { constructor(private readonly cache: CacheService) {} // 防击穿默认开启100 个并发请求只有 1 次回源 async getProduct(id: string) { return this.cache.getOrSet( product:${id}, () this.db.fetchProduct(id), { ttl: 300 }, // 5 分钟缓存 ); } }如果某个 Key 不需要防击穿比如低频数据可以用skipStampede: true单独跳过。从 v1.1.0 起Cached装饰器内部也走getOrSet()因此装饰器用法同样自动获得防击穿。更完整的用法示例可以参考 service-get-or-set.usage.ts。防击穿效果对比场景无防护有防护100 个并发请求100 次数据库查询仅 1 次数据库查询数据库负载瞬时尖峰平稳回源请求响应50ms50ms等待者响应各自 50ms约 60ms共享等待进阶组合拳SWR 双保险过期不阻塞防击穿解决并发回源而Stale-While-RevalidateSWR解决过期时的可用性TTL 过期后先返回旧数据同时在后台异步刷新用户几乎无感知。两者组合后缓存过期既不阻塞请求、也不产生重复回源是电商秒杀、热点榜单场景的黄金搭档。const user await this.cache.getOrSetUser( user:123, () this.repository.findOne(123), { ttl: 300, // 5 分钟新鲜期 swr: { enabled: true, staleTime: 300 }, // 再给 5 分钟 stale 窗口 } );SWR 的实现位于 packages/cache/src/swr/ 目录的 swr-manager.service.ts它通过cachedAt / staleAt / expiresAt三个时间戳精确管理数据新鲜度且后台刷新有去重机制——同一个 Key 同一时刻只会有一个刷新任务。进阶组合拳启动预热从源头避免雪崩冷启动时缓存为空第一批请求必然全部回源。RedisX 的**缓存预热Cache Warmup**在 NestJSOnModuleInit生命周期自动执行应用开始接收流量之前就把热点数据装进缓存配合 chunk 分批 Promise.allSettled单次失败不会影响整批。new CachePlugin({ warmup: { enabled: true, concurrency: 10, // 每批最多并行 10 个 Key keys: [ { key: top-products, loader: () repo.findTopSelling(100), ttl: 3600 }, { key: config:app, loader: () loadAppConfig(), ttl: 86400 }, ], }, });预热内部同样调用getOrSet()所以防击穿和 SWR 元数据会一并生效。实现代码见 warmup.service.ts。建议只预热高频热点数据避免把全量数据塞进缓存造成资源浪费。监控与排障防击穿效果可视化防击穿是否真的生效RedisX 提供统计指标const stats await this.cache.getStats(); // { stampedePrevented: 142, ... } —— 已阻止的击穿事件数activeFlights当前正在回源的加载任务数totalWaiters所有正在等待的请求总数oldestFlight最老的回源任务耗时毫秒prevented累计阻止的击穿事件数。排查锁是否卡住可以直接在 redis-cli 里查_stampede:*前缀的 KeyKEYS _stampede:* # 查看当前所有防击穿锁 TTL _stampede:popular # 检查锁的剩余存活时间正常情况下锁会在lockTimeout后自动过期无需人工干预若发现异常残留再谨慎清理。防击穿最佳实践小结默认开启无需额外配置用getOrSet()或Cached即可获得完整防击穿热点 Key 配置合理超时lockTimeout应略大于最慢回源时间waitTimeout应大于lockTimeout组合 SWR读多写少的热点数据开启 SWR过期不阻塞组合预热启动时预装热点数据从源头规避雪崩关注stampedePrevented指标数值长期为 0说明热点可能没有真正走缓存。缓存雪崩不可怕可怕的是没有防击穿机制就上线。NestJS RedisX 把这套原本需要自己写 Redis 锁 Promise 合并的复杂逻辑封装成了插件一行配置即可拥有生产级的防护能力。完整的防击穿文档见 website/en/reference/cache/stampede.md更多缓存实践可参考 website/en/reference/cache/ 目录。【免费下载链接】nestjs-redisxModular Redis toolkit for NestJS with plugin architecture - caching, locks, rate limiting, circuit breaker, pub/sub, idempotency, streams, metrics tracing项目地址: https://gitcode.com/gh_mirrors/ne/nestjs-redisx创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考