BFF 层缓存设计:在 CDN 和 API 之间再加一层缓存
BFF 层缓存设计在 CDN 和 API 之间再加一层缓存一、你已经做了 CDN 缓存和 API 缓存但页面 TTFB 还是 800ms一个典型的 Web 应用请求链路浏览器 → CDN缓存命中率 60%→ Nginx 反向代理 → BFFBackend For Frontend→ 下游微服务多个。CDN 缓存了静态资源微服务端也做了 Redis 缓存。但页面 TTFB首字节时间还是高——瓶颈就在 BFF。BFF 层的职责是聚合多个下游 API 的数据按照前端需要的格式拼装后返回。这个聚合操作本身就耗时等待最慢的 API 返回通常是 2-3 个 API 中最慢的那个然后做数据拼接、格式转换。如果这层不做缓存每个页面请求都要重复一次完整的聚合链路。但在 BFF 层加缓存有独特的挑战BFF 缓存的是聚合后的数据涉及多个数据源的时效性和一致性问题。BFF 不能简单地缓存 5 分钟——一个数据源更新了其他数据源没更新缓存需要做到部分失效。二、底层机制与原理剖析BFF 缓存的难点不在于怎么存而在于什么时候失效。传统的 TTL 缓藏在这一层基本不可用——下游数据源的更新频率各不相同TTL 要么设太短没效果要么设太长数据过期。为此系统采用了一套动态的请求与失效流程页面请求到达后BFF 优先检查缓存命中则直接返回响应若未命中则并行请求下游 API包括用户信息、商品列表及推荐内容。待各 API 响应并携带 ETag 或 Version 字段后BFF 聚合数据、计算缓存键并写入缓存。在失效策略上系统摒弃了被动等待过期转而监听下游 API 变更事件。一旦数据源发生变更消息队列广播事件BFF 据此执行选择性失效仅清除受影响数据源如 user:xxx 或 product:xxx关联的缓存随后再次进入缓存检查流程。核心设计是三个方面缓存键设计BFF 缓存的键不只是一个 URL path而是API 响应版本的组合 hash。下游每个 API 返回数据时可以附带一个 ETag 或 version 字段。BFF 把这些 ETag 收集起来构成缓存键。当任一 API 的 ETag 变化时缓存键变触发重新聚合。选择性失效不依赖 TTL 做失效改用事件驱动。当下游数据变更时如用户修改了昵称通过消息队列广播变更事件。BFF 对应的缓存失效接口收到事件后只失效该数据源关联的缓存。分层 TTL对不同数据源设置不同的 TTL。用户信息 30 秒商品列表 5 分钟推荐内容 1 小时。聚合后的缓存取最小值作为 expire或者允许不同数据源分别过期——用后台异步刷新。三、生产级代码实现/** * BFF 缓存层 * 核心设计 * 1. 缓存键 下游 API 版本组合 hash解决多数据源一致性 * 2. 事件驱动的选择性失效替代纯 TTL * 3. 防缓存击穿并发同一缓存键只允许一个请求回源 */ import { createHash } from crypto; import Redis from ioredis; interface CacheEntryT { data: T; createdAt: number; expiresAt: number; etags: Recordstring, string; // 下游 API 的版本标识 } interface ApiSource { name: string; ttl: number; // 缓存时间秒 fetch: () Promise{ data: any; etag: string }; } class BFFCacheManager { private redis: Redis; private pendingRequests: Mapstring, Promiseany new Map(); constructor(redisUrl: string) { this.redis new Redis(redisUrl, { maxRetriesPerRequest: 3, retryStrategy(times) { return Math.min(times * 100, 3000); }, }); } /** * 聚合请求 缓存 * * 设计决策缓存键包含 API 版本的组合 hash。 * 不仅基于请求参数还基于数据源的当前状态。 * 当任一数据源更新后 ETag 变化缓存自动失效。 */ async getOrFetchT( cacheKey: string, sources: ApiSource[], ttl: number 60, ): PromiseT { // Step 1尝试从 Redis 获取缓存 const cached await this.redis.get(bff:${cacheKey}); if (cached) { const entry: CacheEntryT JSON.parse(cached); // 检查各数据源是否过期 const allEtagsValid await this.checkEtags(entry.etags, sources); if (allEtagsValid Date.now() / 1000 entry.expiresAt) { return entry.data; } // 某个数据源过期但缓存整体还有效 - 后台异步刷新 if (Date.now() / 1000 entry.expiresAt) { this.refreshInBackground(cacheKey, sources, ttl).catch(() {}); return entry.data; // 返回旧数据不阻塞当前请求 } } // Step 2缓存未命中或已过期 - 回源 return this.fetchAndCacheT(cacheKey, sources, ttl); } /** * 回源获取 写入缓存 * * 防缓存击穿pendingRequests 保证同一 cacheKey 的并发请求 * 只有一个会真正回源其他的等待结果。 */ private async fetchAndCacheT( cacheKey: string, sources: ApiSource[], ttl: number, ): PromiseT { // 防击穿检查是否有正在进行的相同请求 const pendingKey pending:${cacheKey}; const pending this.pendingRequests.get(pendingKey); if (pending) { return pending as PromiseT; } const promise this.doFetch(sources); this.pendingRequests.set(pendingKey, promise); try { const result await promise; // 写入 Redis 缓存 const entry: CacheEntryT { data: result.data, createdAt: Date.now() / 1000, expiresAt: Date.now() / 1000 ttl, etags: result.etags, }; await this.redis.setex( bff:${cacheKey}, ttl 60, // Redis TTL 稍长于逻辑 TTL留缓冲 JSON.stringify(entry), ); return result.data; } finally { this.pendingRequests.delete(pendingKey); } } /** * 执行下游 API 调用和数据聚合 */ private async doFetch(sources: ApiSource[]) { // 并行请求所有下游 API const results await Promise.allSettled( sources.map(s s.fetch()) ); const etags: Recordstring, string {}; const aggregated: Recordstring, any {}; for (let i 0; i sources.length; i) { const result results[i]; const source sources[i]; if (result.status fulfilled) { aggregated[source.name] result.value.data; etags[source.name] result.value.etag; } else { // 单个 API 失败不阻塞整体用上次缓存的数据兜底 const fallback await this.getFallback(source.name); aggregated[source.name] fallback; etags[source.name] stale; } } return { data: aggregated as any, etags }; } /** * 检查各数据源的 ETag 是否仍然有效 */ private async checkEtags( cachedEtags: Recordstring, string, sources: ApiSource[], ): Promiseboolean { // 简化实现检查每个 source 是否有 ETag 变化 // 生产环境应通过轻量级的 HEAD 请求获取 ETag而非完整 GET for (const source of sources) { const cachedEtag cachedEtags[source.name]; if (!cachedEtag || cachedEtag stale) { return false; } } return true; } /** * 后台异步刷新缓存 */ private async refreshInBackground( cacheKey: string, sources: ApiSource[], ttl: number, ): Promisevoid { try { await this.fetchAndCache(cacheKey, sources, ttl); } catch (err) { // 后台刷新失败静默处理不阻塞主流程 console.warn(BFF cache refresh failed for ${cacheKey}:, err); } } /** * 获取降级数据 */ private async getFallback(sourceName: string): Promiseany { // 尝试从本地内存缓存读取上次成功的数据作为降级 return { _fallback: true, _source: sourceName }; } /** * 选择性缓存失效 * 当下游数据变更时仅失效关联的缓存 */ async invalidateBySource(sourceName: string): Promisevoid { // 使用 Redis SCAN 查找所有包含该 source 的缓存键 // 生产环境建议使用 Redis 的 Pub/Sub 索引表 // 而非 scanscan 在大键量下很慢 let cursor 0; const keysToDelete: string[] []; do { const [newCursor, keys] await this.redis.scan( cursor, MATCH, bff:*, COUNT, 100, ); cursor newCursor; for (const key of keys) { const cached await this.redis.get(key); if (cached) { try { const entry: CacheEntryany JSON.parse(cached); // 该缓存包含此数据源且标记非 stale - 需要失效 if (entry.etags[sourceName] entry.etags[sourceName] ! stale) { keysToDelete.push(key); } } catch { keysToDelete.push(key); // 格式异常也删除 } } } } while (cursor ! 0); if (keysToDelete.length 0) { await this.redis.del(...keysToDelete); } } }四、边界分析与架构权衡BFF 缓存的复杂性代价引入了多数据源的 ETag 管理这意味着下游 API 需要有 ETag 或版本接口支持。如果下游 API 不支持BFF 就只能退化为纯 TTL 缓存——而这种退化版本的问题前面已经说了。另一个问题是事件驱动的失效依赖消息队列的可靠性。如果缓存失效消息丢失缓存中的数据就永远不会更新。内存与 Redis 的权衡BFF 通常是 Node.js 进程如果缓存太大如缓存了 1000 页面的聚合数据Node.js 的堆内存会被撑爆。这就是为什么方案中使用了 Redis 而非本地内存。但引入 Redis 也增加了一个网络依赖——BFF 到 Redis 的延迟通常在 1-5ms。适用边界最适合有 3 个以上下游数据源、聚合逻辑复杂的 BFF 层。也适合下游数据源更新频率不同但前端要求强一致性的场景。前端是 SPA 结构、通过 BFF 做 SSR 或 BFF 聚合的场景是典型受益者。禁用场景不适合只有 1-2 个简单 API 调用的 BFF微服务网关层缓存收益不大。也不适合对实时性要求极高的场景如实时竞价、毫秒级行情缓存引入的旧数据风险高于收益。五、总结BFF 缓存的核心不是把数据存起来而是解决多数据源的一致性。ETag 组合缓存键让缓存对下游数据变更敏感、选择性失效避免一锅端的全清、防止缓存击穿保证回源可控。关键是放弃纯 TTL 思维把数据什么时候变了作为失效触发点而不是缓存到了多少秒该过期了。

相关新闻

5分钟掌握缠论分析:通达信插件让技术分析自动化

5分钟掌握缠论分析:通达信插件让技术分析自动化

5分钟掌握缠论分析:通达信插件让技术分析自动化 【免费下载链接】ChanlunX 缠中说禅炒股缠论可视化插件 项目地址: https://gitcode.com/gh_mirrors/ch/ChanlunX 还在为复杂的缠论分析头疼吗?ChanlunX缠论分析工具作为一款专业的通达信插件&#…

2026/7/19 18:37:09 阅读更多 →
力扣287-寻找重复数

力扣287-寻找重复数

287. 寻找重复数 - 力扣(LeetCode) 给定一个包含 n 1 个整数的数组 nums ,其数字都在 [1, n] 范围内(包括 1 和 n),可知至少存在一个重复的整数。 假设 nums 只有 一个重复的整数 ,返回 这个…

2026/7/19 18:37:09 阅读更多 →
哪个品牌的智慧食堂系统好用

哪个品牌的智慧食堂系统好用

究竟哪家的智慧食堂系统更适合你?在信息化快速发展的今天,智慧食堂管理系统已经成为各大企业、高校等食堂管理的必备工具。各大品牌如云勤通、阿里云、腾讯云及一些其他行业内的企业,纷纷推出了智慧食堂管理系统。这些系统各有特点&#xff0…

2026/7/19 18:37:09 阅读更多 →

最新新闻

互联网大厂常见Java面试题及答案汇总(2026持续更新)

互联网大厂常见Java面试题及答案汇总(2026持续更新)

金九银十即将来袭,又是一个跳槽的好季节,准备跳槽的同学都摩拳擦掌准备大面好几场,今天为大家准备了互联网面试必备的 1 到 5 年 Java 面试者都需要掌握的面试题,分别 JVM,并发编程,MySQL,Tomca…

2026/7/20 0:15:40 阅读更多 →
ngx_output_chain_get_buf

ngx_output_chain_get_buf

1 定义 ngx_output_chain_get_buf 函数 定义在 src/core/ngx_output_chain.cstatic ngx_int_t ngx_output_chain_get_buf(ngx_output_chain_ctx_t *ctx, off_t bsize) {size_t size;ngx_buf_t *b, *in;ngx_uint_t recycled;in ctx->in->buf;size ctx->buf…

2026/7/20 0:13:39 阅读更多 →
python数据可视化技巧的100个练习 -- 31. 类别数据的点图

python数据可视化技巧的100个练习 -- 31. 类别数据的点图

重要性★★★☆☆ 难度★★☆☆☆ 你是一家零售公司的数据分析师。你的经理要求你可视化最近产品发布的客户满意度评级分布。评级是分类的,范围从“非常不满意”到“非常满意”。创建一个点图以显示每个评级类别的频率。使用 Python 进行数据处理和可视化。在代码中生成输入…

2026/7/20 0:12:39 阅读更多 →
智能体走进物理世界,千里科技携舱驾协同成果亮相WAIC 2026

智能体走进物理世界,千里科技携舱驾协同成果亮相WAIC 2026

在2026世界人工智能大会(WAIC 2026)举办期间,千里科技董事长、阶跃星辰董事长印奇作为特邀嘉宾出席大会开幕式并在大会主论坛(上午场)发表主题演讲《当智能体进入物理世界》。在印奇看来,"智能体"…

2026/7/20 0:12:39 阅读更多 →
商汤大装置发布“技术-生态-商业”闭环布局,共启“国产AI基础设施规模化商用元年”

商汤大装置发布“技术-生态-商业”闭环布局,共启“国产AI基础设施规模化商用元年”

7月18日,在WAIC 2026商汤科技 “基座大模型架构创新与生态合作论坛”上,商汤科技联合创始人、大装置事业群总裁杨帆发表《智变共生——加速AI基础设施持续升级》主题演讲,系统呈现了商汤大装置国产AI基础设施“技术-生态-商业”闭环布局&…

2026/7/20 0:12:39 阅读更多 →
2026年具身智能领域代表性机器人产品观察:普渡一脑多形底座与实景落地解析

2026年具身智能领域代表性机器人产品观察:普渡一脑多形底座与实景落地解析

前言2026年被行业视为具身智能从"实验室炫技"走向"规模化量产"的关键拐点。据弗若斯特沙利文《全球商用服务机器人市场研究报告》,普渡科技以23%市占率位居全球商用服务机器人第一,业务覆盖85+个国家和地区,累…

2026/7/20 0:11:39 阅读更多 →

日新闻

2026 WAIC:努比亚二代“豆包手机”NaviX Ultra亮相,智能体验全面升级!

2026 WAIC:努比亚二代“豆包手机”NaviX Ultra亮相,智能体验全面升级!

7月18日智东西消息,在2026 WAIC期间,努比亚联合字节豆包打造的二代“豆包手机”努比亚NaviX Ultra首次亮相,相比一代有诸多升级。智能体手机理念中兴通讯终端事业部总裁、努比亚总裁倪飞表示,智能体手机要从人操作手机变为手机帮人…

2026/7/20 0:00:34 阅读更多 →
努比亚NaviX Ultra亮相WAIC,智能体手机能否让用户生活更简单?

努比亚NaviX Ultra亮相WAIC,智能体手机能否让用户生活更简单?

努比亚NaviX Ultra:外观与功能双升级在2026 WAIC期间,首次亮相的努比亚NaviX Ultra吸引了众多目光。它是努比亚联合字节豆包打造的二代“豆包手机”,与一代努比亚M153相比,外观设计变化较大。其机身背部搭载横向排布的大尺寸影像模…

2026/7/20 0:00:34 阅读更多 →
C# 将逗号分割的字符串转换为long,并添加到List<long>

C# 将逗号分割的字符串转换为long,并添加到List<long>

目录 方法1:使用Split和Convert.ToInt64 方法2:使用LINQ的Select和ToList 方法3:使用TryParse进行异常安全转换(推荐) 如果您喜欢此文章,请收藏、点赞、评论,谢谢,祝您快乐每一天…

2026/7/20 0:00:34 阅读更多 →

周新闻

Go语言静态资源打包方案对比与实践指南

Go语言静态资源打包方案对比与实践指南

1. 项目背景与核心需求在Go语言开发中,我们经常需要处理静态资源文件的打包问题。无论是Web应用的模板文件、前端资源,还是配置文件、证书等,都需要随程序一起分发。传统做法是将这些文件与编译后的二进制文件放在同一目录下,但这…

2026/7/19 0:00:40 阅读更多 →
Go语言实现高性能LDAP认证服务的架构与实践

Go语言实现高性能LDAP认证服务的架构与实践

1. 项目背景与核心价值LDAP(轻量级目录访问协议)作为企业级身份认证的黄金标准,已经服务了超过80%的财富500强公司。我在金融科技领域实施统一认证体系时,发现传统Java方案存在启动慢、内存占用高等痛点。而Go语言凭借其协程并发模…

2026/7/20 4:31:26 阅读更多 →
【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

更多请点击: https://intelliparadigm.com 第一章:AI面试官实战指南的核心价值与适用场景 AI面试官并非替代人类HR的“黑箱工具”,而是以可解释、可审计、可迭代的方式,赋能招聘全链路的关键基础设施。其核心价值在于将主观经验沉…

2026/7/19 0:00:40 阅读更多 →

月新闻