Hono 生产实战 03:缓存、ETag、KV 和 Cache API
Hono 生产实战 03:缓存、ETag、KV 和 Cache API
缓存不是为了显得高级。
缓存是为了减少重复计算、减少数据库压力、降低延迟。
但缓存也最容易制造线上怪问题:
1 2 3 4
| 用户改了数据,页面还是旧的 接口明明报错,结果缓存了错误响应 登录用户看到了别人的数据 缓存 key 没设计好,命中率很低
|
Hono 项目做缓存,重点不是“加一个 cache middleware”,而是先分清哪些数据可以缓存,缓存在哪里,什么时候失效。
这篇文章讲 Hono API 里常见的 HTTP 缓存、ETag、Cloudflare Cache API、KV 缓存策略。
一、先分清四种缓存
Hono API 常见缓存有四层:
1 2 3 4
| 浏览器缓存 CDN / Cloudflare Cache KV / Redis 这类应用缓存 数据库查询结果缓存
|
每一层适合的东西不同。
浏览器缓存适合:
CDN 缓存适合:
1 2 3
| 公开 GET 响应 匿名访问内容 图片、文章、商品详情
|
KV 缓存适合:
不要缓存:
1 2 3 4 5
| 用户私密数据 带 Authorization 的响应 订单支付状态 后台操作结果 一次性 token
|
缓存第一原则:不确定能不能缓存,就先不要缓存。
二、Cache-Control
最基础的是 HTTP header。
公开内容:
1 2 3 4 5 6 7 8 9
| app.get('/api/public/config', (c) => { c.header('Cache-Control', 'public, max-age=300, stale-while-revalidate=60')
return c.json({ data: { siteName: 'wxk1991', }, }) })
|
私有内容:
1 2 3 4 5 6 7
| app.get('/api/private/me', auth(), (c) => { c.header('Cache-Control', 'no-store')
return c.json({ data: c.get('user'), }) })
|
我的习惯是:
1 2 3 4
| 公开 GET 可以考虑 public 登录态接口默认 no-store 写操作响应默认不缓存 不确定就 no-store
|
很多隐私泄漏不是数据库被攻破,而是缓存策略写错。
三、ETag
ETag 适合内容不常变化,但客户端会频繁请求的场景。
流程:
1 2 3 4
| 服务端返回 ETag 浏览器下次请求带 If-None-Match 服务端判断没变化,返回 304 浏览器复用本地缓存
|
Hono 示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| async function sha256(input: string) { const data = new TextEncoder().encode(input) const hash = await crypto.subtle.digest('SHA-256', data) return [...new Uint8Array(hash)] .map((b) => b.toString(16).padStart(2, '0')) .join('') }
app.get('/api/articles/:id', async (c) => { const article = await getArticle(c.req.param('id')) const body = JSON.stringify({ data: article }) const etag = `"${await sha256(body)}"`
if (c.req.header('If-None-Match') === etag) { return c.body(null, 304) }
c.header('ETag', etag) c.header('Cache-Control', 'public, max-age=60')
return c.body(body, 200, { 'Content-Type': 'application/json', }) })
|
ETag 的价值不是让第一次请求更快,而是让重复请求少传数据。
四、Cloudflare Cache API
如果 Hono 跑在 Cloudflare Workers,可以用 Cache API 缓存公开 GET 响应。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| app.get('/api/public/posts/:id', async (c) => { const cache = caches.default const cacheKey = new Request(c.req.url, c.req.raw)
const cached = await cache.match(cacheKey) if (cached) { return cached }
const post = await getPublicPost(c.req.param('id'))
const response = c.json({ data: post, })
response.headers.set('Cache-Control', 'public, max-age=300')
await cache.put(cacheKey, response.clone())
return response })
|
只缓存公开 GET。
不要缓存带 Authorization 的响应。
如果请求有用户身份:
1 2 3 4
| const auth = c.req.header('Authorization') if (auth) { c.header('Cache-Control', 'no-store') }
|
就不要放进 Cloudflare Cache。
五、KV 缓存
KV 适合缓存配置和热点数据。
比如缓存站点配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| type Bindings = { KV: KVNamespace }
app.get('/api/public/site-config', async (c) => { const cacheKey = 'site-config:v1' const cached = await c.env.KV.get(cacheKey, 'json')
if (cached) { return c.json({ data: cached, cache: 'hit', }) }
const config = await loadSiteConfigFromDb()
await c.env.KV.put(cacheKey, JSON.stringify(config), { expirationTtl: 300, })
return c.json({ data: config, cache: 'miss', }) })
|
KV 的特点是:
1 2 3 4
| 读很方便 适合多读少写 不是强一致数据库 写后立刻读到最新值不要强依赖 KV
|
所以 KV 适合缓存,不适合作为强一致业务状态。
六、缓存 key 设计
缓存 key 要包含会影响结果的因素。
比如文章详情:
列表查询:
1
| post:list:page={page}:tag={tag}:sort={sort}
|
多租户:
1
| tenant:{tenantId}:post:list:page={page}
|
如果接口响应和用户身份有关,缓存 key 必须包含用户维度,或者干脆不缓存。
错误示例:
正确一点:
但用户 profile 这种私密数据通常不建议走公共缓存。
七、缓存失效
缓存最大的问题不是设置,而是失效。
常见方式:
1 2 3 4
| TTL 到期 写操作后主动删除 版本号 key 短缓存 + stale-while-revalidate
|
比如文章更新后:
1 2
| await updatePost(id, input) await c.env.KV.delete(`post:detail:${id}`)
|
列表缓存更麻烦。
因为一篇文章更新可能影响:
简单项目可以用短 TTL。
复杂项目需要事件驱动清理,或者干脆少缓存列表。
八、推荐策略
我对 Hono API 的缓存策略通常是:
1 2 3 4 5 6
| 私有接口:no-store 公开详情页:ETag + CDN cache 公开配置:KV + TTL 公开列表:短 TTL 高成本第三方 API:KV cache 写操作:不缓存,并清理相关 key
|
不要为了缓存而缓存。
先找出真正慢、真正高频、真正可缓存的接口。
九、最后的建议
缓存是性能工具,也是复杂度来源。
Hono 项目里,我建议按这个顺序加:
1 2 3 4 5
| 先写正确接口 再加 Cache-Control 然后加 ETag 再考虑 Cloudflare Cache API 最后才用 KV / Redis 缓存业务数据
|
每加一层缓存,都要问:
1 2 3 4 5
| 缓存什么? 缓存多久? 谁能访问? 什么时候失效? 出错时怎么回源?
|
答不出来,就先别加。
参考资料