Smartstore 输出缓存:Donut Hole 缓存策略深度剖析
【免费下载链接】SmartstoreA modular, scalable and ultra-fast open-source all-in-one eCommerce platform built on ASP.NET Core 10项目地址: https://gitcode.com/GitHub_Trending/smar/Smartstore
Smartstore是一款基于 ASP.NET Core 的开源一体化电子商务平台,而它的**输出缓存(Output Cache)**正是平台"超快"性能的核心秘密之一。本文将带你完整剖析 Smartstore 独特的Donut Hole(甜甜圈洞)缓存策略:静态外圈如何整体缓存、动态内层如何实时替换、缓存失效又靠什么精准触发——帮助新手和普通用户快速理解这套机制背后的性能原理。
什么是 Donut Hole 缓存策略?
想象一个电商页面:顶部导航、Banner、商品网格、页脚几乎天天不变,但"购物车数量""登录用户名"却时刻在变。传统做法要么整页不缓存(慢),要么整页缓存(数据不新鲜)。
Smartstore 的Donut Hole Caching(甜甜圈洞缓存)给出了优雅的折中方案:
- 外圈(Page):页面的非动态外层——HTML、CSS、JavaScript 及大部分静态内容,整体缓存在服务端内存、数据库或 Redis 中;
- 内圈(Component):即"甜甜圈的洞",嵌入在静态外层中的动态区块(如搜索框、个性化推荐),每次请求实时生成;
- 请求到达时,服务器先取缓存的外圈,再把新鲜的内圈"填"进洞里,拼出最终页面返回给用户。
这套策略既拿到了整页缓存的巨大性能收益,又保证了动态内容永远新鲜。缓存命中后无需重新执行控制器与视图渲染,响应时间可大幅降低、服务器负载显著减少,这正是 Smartstore 号称"ultra-fast"的关键所在。
📖 官方文档中对这一概念有完整说明:output-cache.md
哪些页面可以缓存?——路由级"白名单"机制
Smartstore 的输出缓存采用**显式声明(opt-in)**方式:没有任何配置时,什么都不缓存。只有被明确列出的路由才会被视为缓存候选。
缓存路由的两种形态
| 元素 | 路由格式 | 示例 |
|---|---|---|
| 整页(Full Page) | [{模块}/]控制器短名/动作 | Catalog/Category、BlogModule/Blog/List |
| 视图组件(View Component) | vc:[{模块}/]组件短名 | vc:SearchBox |
模块开发者只需在模块内提供一个internal的 CacheableRoutes 类,实现 ICacheableRouteProvider 接口并返回路由字符串列表即可,无需任何 DI 注册:
internal sealed class CacheableRoutes : ICacheableRouteProvider { public int Order => 0; public IEnumerable<string> GetCacheableRoutes() { yield return "Catalog/Category"; yield return "vc:SearchBox"; } }缓存键:为什么同一页面会有多份缓存?
同一个页面在不同环境下内容不同,因此 Smartstore 会用多个环境要素共同生成缓存条目键(cache key),每种组合各存一份:
- 页面路径与查询字符串
- 当前语言、货币
- 当前商店、主题
- 全部客户角色
- 应用版本号
多店铺、多语言、多主题的站群场景下,这正是缓存不出"串味"问题的保障。
动态"甜甜圈洞"是如何被实时替换的?
这是整个策略最精妙的部分,官方称之为Substitution(替换):
- 首次请求:页面正常渲染,但所有不匹配任何"组件路由"的视图组件(即动态洞)不会被写进缓存内容,而是被替换为一段JSON 元数据(包含组件信息与它的
Invoke方法参数); - 后续请求:服务器直接从缓存取出"带洞的外圈",解析每个洞里的 JSON 元数据,现场调用对应组件的
Invoke方法,把新鲜 HTML 填回洞里,拼出最终页面。
⚠️ 这里有两条重要约束,新手务必牢记:
- 组件
Invoke方法的参数不能依赖父页面或外层视图模型(二次请求时模型已不可用),且所有参数必须可序列化为 JSON; - 绝不要把涉及用户个人信息的组件设为可缓存——否则 A 用户的购物车可能展示给 B 用户。
另外,后台管理页面永远不会被缓存,只有前台页面在缓存范围内。
缓存失效:实体标签让脏数据"无处可藏"
输出缓存最大的难题是:商品 A 在后台被改价或删除后,任何以任意形式展示过商品 A 的页面都必须立即失效。Smartstore 用一套"实体标签 + 观察者"体系自动化了这件事。
显示声明(Announce)与缓存标签
核心服务是 IDisplayControl。控制器只需在准备视图模型时调用一次Announce(product),系统就会为实体生成缓存标签(tag)。内置的标签规则已在 DisplayControl.cs 中预置:
- 商品 →
p5、分类 →c12、品牌 →m3、主题(新闻)→t8、菜单 →mnu1…… - 连带关系也被覆盖:修改商品的图片、规格、捆绑子项、阶梯价,同样会打上所属商品的标签
p5。
缓存条目生成时,这些标签随条目一起被缓存。此后任何实体被编辑或删除,所有携带该标签的缓存条目都会被精准清除——改价商品 A,全站涉及 A 的页面瞬间刷新,无需手动清缓存。
对于自定义实体(如博客文章),可通过DisplayControl.RegisterHandlerFor注册标签生成规则,例如为BlogPost生成b5标签。
失效观察者:复杂场景的"总开关"
IOutputCacheInvalidationObserver 是单例服务,允许模块注册两类观察者:
- 实体观察者:监控实体变更(如"博客文章的可见性属性被修改时,按路由批量失效所有博客列表页");
- 设置观察者:监控系统设置键(如"任何
CatalogSettings.*变更时,失效相关目录页面",支持通配前缀)。
失效粒度由粗到细都有覆盖:按路由失效(InvalidateByRouteAsync)、按标签失效(InvalidateByTagAsync)、按前缀失效(InvalidateByPrefixAsync),完整接口见 IOutputCacheProvider。
逃生舱:标记请求为"不可缓存"
即使路由匹配成功,只要页面包含不宜缓存的内容,调用 IDisplayControl 的MarkRequestAsUncacheable()即可让缓存提供方放弃拦截本次请求——私密的、个性化的页面永远安全。
上手清单:模块开发者的 5 条黄金法则
- ✅只声明确实可缓存的路由——输出缓存是 opt-in,少即是多;
- ✅用户相关组件永远不缓存,防止个人数据"串用户";
- ✅在准备视图模型时 Announce 所有被显示的实体,让标签系统自动接管失效;
- ✅为自定义实体注册标签处理器(
DisplayControl.RegisterHandlerFor),并处理好子实体到父实体的标签映射; - ✅在启动类的
BuildPipeline中注册失效观察者,覆盖设置变更这类全局影响。
核心源码导读
想进一步深入,建议按此顺序阅读:
- 策略概念与路由规范:dev-docs/framework/platform/output-cache.md
- 可缓存路由定义:ICacheableRouteProvider.cs
- 显示控制与标签体系:DisplayControl.cs、IDisplayControl.cs
- 失效观察者与缓存提供程序:IOutputCacheInvalidationObserver.cs、IOutputCacheProvider.cs
小结
Smartstore 的输出缓存并非简单"存页面",而是一套静态外圈整体缓存 + 动态内圈实时替换 + 实体标签精准失效三位一体的工程体系。Donut Hole 策略让商城首页在秒杀级流量下依然轻快如飞,而标签与观察者机制又确保商品一改,全站缓存即刻保鲜——这正是它兼顾"快"与"新"的答案。
【免费下载链接】SmartstoreA modular, scalable and ultra-fast open-source all-in-one eCommerce platform built on ASP.NET Core 10项目地址: https://gitcode.com/GitHub_Trending/smar/Smartstore
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考