Hono 生产实战 03:缓存、ETag、KV 和 Cache API
wxk1991 Lv5

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 这类应用缓存
数据库查询结果缓存

每一层适合的东西不同。

浏览器缓存适合:

1
2
3
静态资源
公开接口
不常变的配置

CDN 缓存适合:

1
2
3
公开 GET 响应
匿名访问内容
图片、文章、商品详情

KV 缓存适合:

1
2
3
4
配置
热点数据
计算结果
第三方 API 响应

不要缓存:

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:detail:{postId}

列表查询:

1
post:list:page={page}:tag={tag}:sort={sort}

多租户:

1
tenant:{tenantId}:post:list:page={page}

如果接口响应和用户身份有关,缓存 key 必须包含用户维度,或者干脆不缓存。

错误示例:

1
user:profile

正确一点:

1
user:profile:{userId}

但用户 profile 这种私密数据通常不建议走公共缓存。


七、缓存失效

缓存最大的问题不是设置,而是失效。

常见方式:

1
2
3
4
TTL 到期
写操作后主动删除
版本号 key
短缓存 + stale-while-revalidate

比如文章更新后:

1
2
await updatePost(id, input)
await c.env.KV.delete(`post:detail:${id}`)

列表缓存更麻烦。

因为一篇文章更新可能影响:

1
2
3
4
5
首页列表
分类列表
标签列表
搜索结果
详情页

简单项目可以用短 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
缓存什么?
缓存多久?
谁能访问?
什么时候失效?
出错时怎么回源?

答不出来,就先别加。


参考资料