eShopOnWeb性能优化指南:用装饰器模式为商品查询加上1分钟内存缓存
【免费下载链接】eShopOnWebSample ASP.NET Core 8.0 reference application, now community supported: https://github.com/NimblePros/eShopOnWeb项目地址: https://gitcode.com/gh_mirrors/es/eShopOnWeb
eShopOnWeb 是 Microsoft 官方的 ASP.NET Core 8.0 参考应用(社区版),它演示了一个完整的电商系统:Web 商城、API 服务和 Blazor 后台管理。对于新手来说,这个项目里最值得学习的性能优化点之一,就是如何用装饰器模式(Decorator Pattern)为商品查询添加内存缓存——在不改动原有业务代码的前提下,让重复的商品列表查询在 1 分钟内直接命中缓存,不再反复调用 API。
项目背景:为什么商品查询需要缓存?
eShopOnWeb 的商品数据流是这样的:
- 用户浏览商城首页 → Web 应用查询商品列表、品牌、类型
- 每次查询都要访问数据库或 API,产生网络开销和数据库压力
- 商品、品牌、类型这类数据变化频率低、读取频率高,是典型的缓存好场景
项目的解决思路很克制:不修改原始服务类,而是"包一层"带缓存逻辑的新类,通过依赖注入把新类交给页面使用。这就是装饰器模式的经典用法。
一图看懂装饰器模式
装饰器模式的核心思想:给已有对象动态叠加新功能,而不改动它本身。
请求 → [带缓存的服务] → 命中缓存?── 是 → 直接返回 │ 否 ↓ [原始 HTTP 服务] → 调用 API → 结果写入缓存 → 返回它的优势:
- ✅ 原始服务代码零改动,职责保持单一
- ✅ 缓存逻辑可以独立测试、独立替换
- ✅ 通过依赖注入(DI)注册,切换有无缓存只需改一行注册代码
项目中的 3 种缓存方案对比
eShopOnWeb 在不同模块用了三种不同的缓存手段,放在一起对比,能帮你理解各自的适用场景:
| 方案 | 所在模块 | 缓存位置 | 过期策略 | 缓存键 |
|---|---|---|---|---|
| 装饰器 + 内存缓存 | Web 商城(CachedCatalogViewModelService) | 服务器内存(IMemoryCache) | 30 秒滑动过期 | brands、types、items-{页}-{每页}-{品牌}-{类型} |
| 装饰器 + 本地存储 | BlazorAdmin 后台(CachedCatalogItemServiceDecorator) | 浏览器 LocalStorage | 1 分钟绝对过期 | 固定键items |
| 泛型装饰器 + 本地存储 | BlazorAdmin 后台(品牌/类型下拉数据) | 浏览器 LocalStorage | 1 分钟绝对过期 | 数据类名(如CatalogBrand) |
可以看到,同一个"商品查询",在 Web 端缓存服务器内存,在 Blazor 后台则缓存在浏览器端——因为 Blazor 页面是前端直接调 API 的,把数据留在浏览器里能省掉整个网络往返。
Web 端:IMemoryCache 装饰器的关键设计
Web 端的商品查询装饰器位于 CachedCatalogViewModelService.cs,它包裹了原始的 CatalogViewModelService.cs,对品牌、类型、分页商品列表三类查询统一加缓存。
其中最有学习价值的是缓存键的设计,实现在 CacheHelpers.cs:
| 缓存对象 | 键生成规则 | 说明 |
|---|---|---|
| 商品列表 | items-{pageIndex}-{pageSize}-{brandId}-{typeId} | 每个"页 + 筛选条件"组合都有独立缓存 |
| 品牌列表 | brands | 固定键 |
| 类型列表 | types | 固定键 |
为什么商品列表的键要包含页码和筛选条件?因为用户筛选"品牌 = Nike + 第 2 页"和"未筛选 + 第 1 页"看到的商品完全不同,如果共用一个键,就会把错误的商品数据返回给页面。默认过期时长为30 秒滑动过期(DefaultCacheDuration)——只要持续有人访问,缓存就一直续命;没人访问 30 秒后自动清除。
BlazorAdmin 端:装饰器 + 浏览器本地存储
后台管理端的装饰器位于 CachedCatalogItemServiceDecorator.cs,思路和 Web 端一致,但缓存介质换成了浏览器的 LocalStorage。
它的请求流程分三步:
- 读缓存:以固定键
items从本地存储取出缓存条目 - 判过期:缓存条目创建时间 + 1 分钟 > 当前时间?命中则直接返回;否则删除旧缓存
- 回源:委托给原始的 CatalogItemService.cs 调 API,拿到结果后连同创建时间一起写回本地存储
这里有一个精巧的小类 CacheEntry.cs:它把"缓存数据 + 创建时间"打包成一个整体,让过期判断有了依据。
写操作后的缓存刷新(最容易遗漏的细节)
只缓存读取、不处理写入,会导致页面展示"陈旧数据"。装饰器对Create/Edit/Delete三个写方法的处理方式是:
- 先委托原始服务完成真实写操作
- 再调用
RefreshLocalStorageList():删除旧缓存 → 重新拉取最新列表 → 写入新缓存
这样"新增商品后刷新页面"这类操作永远不会看到过期数据,而普通浏览操作则享受缓存加速。
品牌、类型这两个下拉数据源用的是一个泛型装饰器CachedCatalogLookupDataServiceDecorator.cs,用类型名自动做缓存键,一份代码同时服务两种数据。
依赖注入注册:整个方案的"开关"
两个装饰器最终在 ServicesConfiguration.cs 中完成注册,规则很简单:
- 接口
ICatalogItemService→ 绑定到装饰器类(页面拿到的就是带缓存的版本) - 具体类
CatalogItemService→ 按自身类型单独注册(供装饰器内部构造注入使用)
也就是说,装饰器通过构造函数持有原始服务的实例,接口调用时走装饰器,装饰器内部再决定是否真正调原始服务。想临时关掉缓存?把接口的绑定换回具体类即可,业务代码一行都不用动。
关键源码导航
| 文件 | 作用 |
|---|---|
| src/Web/Services/CachedCatalogViewModelService.cs | Web 端商品查询的内存缓存装饰器 |
| src/Web/Extensions/CacheHelpers.cs | 缓存键生成规则与 30 秒过期配置 |
| src/BlazorAdmin/Services/CachedCatalogItemServiceDecorator.cs | Blazor 后台商品列表的本地存储缓存装饰器 |
| src/BlazorAdmin/Services/CacheEntry.cs | 缓存条目包装类(数据 + 创建时间) |
| src/BlazorAdmin/ServicesConfiguration.cs | 装饰器与原始服务的 DI 注册开关 |
常见问题(FAQ)
Q1:为什么叫"装饰器模式"而不是直接改原服务加缓存?保持单一职责。原始服务只负责"取数据",装饰器只负责"缓存"。将来想换 Redis、想加分布式锁,都只需新增一个装饰器类,不动老代码。
Q2:1 分钟 / 30 秒的过期时间是怎么定的?两者取向不同:Web 端用滑动过期(有人访问就续命),适合高热度查询;Blazor 端用绝对过期(创建后 1 分钟必失效),保证后台编辑数据后很快能看到最新状态。
Q3:写操作为什么必须刷新缓存?缓存放的是快照。如果新增商品后不清缓存,列表页会继续展示旧数据,用户会以为"操作没生效"。装饰器里Create、Edit、Delete后统一调用刷新方法,正是为了消除这个体验断层。
小结
eShopOnWeb 用一套组合拳演示了装饰器模式在性能优化中的标准姿势:
- 装饰器包原始服务,接口不变、代码不改
- 读操作走缓存 + 短过期,写操作强制刷新
- 缓存键包含全部筛选条件,避免脏数据串页
- DI 注册处即"缓存开关"
把这套模式迁移到你自己的项目里,商品类查询的重复请求量可以大幅减少,而实现成本不过百来行代码。
【免费下载链接】eShopOnWebSample ASP.NET Core 8.0 reference application, now community supported: https://github.com/NimblePros/eShopOnWeb项目地址: https://gitcode.com/gh_mirrors/es/eShopOnWeb
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考