HATEOAS 架构下的多级缓存设计与一致性保证
面试官追问: “在 HATEOAS 架构中,资源被高度打散(例如独立获取文章详情和点赞数),如果频繁请求会不会导致性能问题?你们的前后端多级缓存是如何设计并保证一致性的?”
核心防御思路:
在高度解耦的 RESTful / HATEOAS 架构中,网络请求确实会变多,但 HTTP 协议本身就是为缓存而生的。我们的思路是**“基于 HTTP 语义的客户端智能缓存” + “基于超媒体头(Header)的条件与失效声明” + “后端的事件驱动缓存”**,实现三级防御。
- 第一级:前端 SDK 的智能内存缓存 (请求去重与依赖图谱)
在前端的@hateoas-ts/resourceSDK 中,我们实现了一套基于 URI 的对象级状态缓存(StateCache),而不是单纯的 Axios 拦截器缓存:
- 请求去重(De-duplication):SDK 内部维护了一个
activeRefreshMap,极短时间内的相同 GET 请求(例如并发渲染多个相同的嵌套子组件)会被拦截并合并,实际只会向服务器发出一个网络请求。 - 短效与长效策略:对于静态字典使用
ForeverCache,对于动态业务数据使用ShortCache(例如默认 30s TTL),确保 UI 的流畅度。 - 关联依赖失效 (
inv-by):HATEOAS 允许定义超媒体连接来描述资源的逆向依赖。如果前端请求了A,其_links中包含了rel="inv-by", href="B",SDK 会在本地建立一棵依赖树(cacheDependencies)。当目标B的缓存被清除时,SDK 会沿着依赖树自动将A标记为stale,并触发 React 组件的精确重渲染。
- 第二级:HTTP 语义下的精确缓存更新与失效 (由后端声明驱动)
当用户执行一个非安全操作(如 POST、PUT、PATCH、DELETE)时,本地缓存必然过期。传统的做法是前端再手动fetchDetails(),而在我们的架构中,缓存更新完全交由后端的 HTTP 响应头来声明:
Content-Location零延迟更新:当执行PUT /orders/1成功后,后端响应除了 200/204 之外,如果带上新数据的 payload 以及Content-Location: /orders/1,前端 SDK 中的cacheMiddleware会自动拦截,并直接更新本地缓存中/orders/1的状态,实现“零延迟”的 UI 刷新,无需发起第二次查询。rel="invalidates"精准爆破:比如提交了针对某个订单的评论(POST /comments),这也会影响订单本身的统计数据。后端在 201 响应中,会附带Link: </orders/1>; rel="invalidates"。前端拦截到后,精确地清除本地对/orders/1的缓存,强制下一次访问时去服务端拉取最新数据。
- 第三级:后端基于 ETag 和 DDD 领域事件的条件请求响应
即便缓存过期导致前端发起了真实的网络请求,我们也确保后端的开销极小:
- HTTP 条件请求 (304 Not Modified):后端在返回资源时,会生成
ETag和Last-Modified头。前端 SDK 在缓存过期但数据还在(或者刷新时),会带上If-None-Match。Java 后端拦截器判断若数据哈希未变,直接返回 304,节省网络带宽和序列化开销。 - 领域事件驱动的 Redis 缓存:在 Java 后端,我们严格遵循 DDD 架构。当订单聚合根发生变化并持久化时,通过 Spring 的 ApplicationEvent 机制发布领域事件。消费者监听到事件后,驱逐(Evict)该资源在 Redis 中的缓存。这样即便没有命中 304,也能在服务端走 Redis 内存缓存,避免打穿到 DB。
总结:
在这套设计中,前端开发者不需要写任何一行代码来手动管理状态的刷新和失效。依靠超媒体语义(Content-Location, Link: invalidates, inv-by)和 HTTP 规范,让后端集中统筹业务状态和关系,前端自动化维系缓存图谱,最终达到了性能和开发体验的双赢。