很多人第一次接触 Cloudflare 存储方案时会困惑:KV 和 D1 都能存数据,我该用哪个?
答案很简单:它们根本不是一回事。
KV 是键值存储,D1 是关系型数据库。就像问"Redis 和 PostgreSQL 选哪个"一样——这取决于你要解决什么问题。
Workers KV 是 Cloudflare 提供的全球分布式键值存储。你可以把它理解为一个超大的、自动同步到全球的 Map<string, string>。
核心特征:
API 长这样:
// 写入
await env.MY_KV.put(
'user:123',
JSON.stringify({ name: 'Chiway', role: 'admin' }),
)
// 读取
const user = await env.MY_KV.get('user:123', 'json')
// 带过期时间写入(秒)
await env.MY_KV.put('session:abc', token, { expirationTtl: 3600 })
// 列出所有 key
const keys = await env.MY_KV.list({ prefix: 'user:' })
// 删除
await env.MY_KV.delete('user:123')就这些。没有 SELECT * FROM users WHERE role = 'admin',没有 JOIN,没有事务。
在决定用 KV 之前,你需要知道这些硬约束:
| 指标 | 免费版 | 付费版 |
|---|---|---|
| 日读取次数 | 10 万 | 无限(月 1000 万免费) |
| 日写入次数 | 1,000 | 无限(月 100 万免费) |
| 单 key 最大写入频率 | 1 次/秒 | 1 次/秒 |
| 单账户存储 | 1 GB | 无限 |
| 单 key 大小 | 512 字节 | 512 字节 |
| 单 value 大小 | 25 MiB | 25 MiB |
| 单 key 元数据 | 1024 字节 | 1024 字节 |
几个关键点:
付费版超出免费额度后:
| 操作 | 价格 |
|---|---|
| 读取 | $0.50 / 百万次 |
| 写入 | $5.00 / 百万次 |
| 删除 | $5.00 / 百万次 |
| 列表查询 | $5.00 / 百万次 |
| 存储 | $0.50 / GB·月 |
注意:写入比读取贵 10 倍。这不是巧合,而是在告诉你 KV 的设计定位——读多写少。
现在来做个正式对比:
| 维度 | KV | D1 |
|---|---|---|
| 数据模型 | Key-Value | 关系型(SQLite) |
| 查询能力 | 只有 get/put/list/delete | 完整 SQL |
| 一致性 | 最终一致(60s 内) | 可选强一致(Sessions API) |
| 读取延迟 | 极低(边缘缓存) | 较低(需要查主库或副本) |
| 写入延迟 | 较低 | 较低 |
| 适合的数据规模 | 大量小数据 | 结构化数据集 |
| 单条数据上限 | 25 MiB | 无明确单行限制 |
| 总容量上限 | 无限(付费) | 10 GB/库(付费) |
| 复杂查询 | 不支持 | 支持 |
| 事务 | 不支持 | 支持 |
用一句话概括:
// 存储
await env.CONFIG.put(
'feature:dark-mode',
JSON.stringify({ enabled: true, rollout: 0.5 }),
)
// 读取
const feature = await env.CONFIG.get('feature:dark-mode', 'json')
if (feature?.enabled && Math.random() < feature.rollout) {
// 启用暗色模式
}特征开关读取频繁、更新很少,完美契合 KV 的特性。
// 创建 session
const sessionId = crypto.randomUUID()
await env.SESSIONS.put(
`session:${sessionId}`,
JSON.stringify({ userId: 123 }),
{
expirationTtl: 86400, // 24 小时过期
},
)
// 验证 session
const session = await env.SESSIONS.get(`session:${sessionId}`, 'json')
if (!session) {
return new Response('Unauthorized', { status: 401 })
}Session 天然适合 KV:按 ID 精确查找、有过期时间、不需要复杂查询。
async function getWeather(city: string, env: Env) {
const cacheKey = `weather:${city}`
// 先查缓存
const cached = await env.CACHE.get(cacheKey, 'json')
if (cached) return cached
// 缓存未命中,请求源 API
const response = await fetch(`https://api.weather.com/v1/${city}`)
const data = await response.json()
// 写入缓存,5 分钟过期
await env.CACHE.put(cacheKey, JSON.stringify(data), { expirationTtl: 300 })
return data
}外部 API 响应缓存是 KV 的经典场景——读多写少、按 key 访问、允许短暂过期。
// 存储用户偏好
await env.PREFS.put(
`user:${userId}:prefs`,
JSON.stringify({
theme: 'dark',
language: 'zh-CN',
timezone: 'Asia/Shanghai',
}),
)
// 读取
const prefs = await env.PREFS.get(`user:${userId}:prefs`, 'json')用户偏好很少变化,但每次请求都要读,KV 很合适。
async function rateLimit(ip: string, env: Env): Promise<boolean> {
const key = `ratelimit:${ip}`
const current = parseInt((await env.LIMITS.get(key)) || '0')
if (current >= 100) {
return false // 超限
}
// 注意:这里有竞态条件,高并发下计数可能不准
// 需要精确限流请用 Durable Objects
await env.LIMITS.put(key, String(current + 1), { expirationTtl: 60 })
return true
}警告:KV 没有原子递增操作,高并发下会有竞态条件。精确限流场景建议用 Durable Objects。
-- 这种查询 KV 做不了
SELECT * FROM posts WHERE author_id = 123 AND status = 'published' ORDER BY created_at DESC LIMIT 10;只要你需要"按条件筛选",就应该考虑 D1。
SELECT posts.title, users.name
FROM posts
JOIN users ON posts.author_id = users.id
WHERE posts.created_at > date('now', '-7 days');多表关联是关系型数据库的核心能力,KV 完全不支持。
SELECT author_id, COUNT(*) as post_count
FROM posts
GROUP BY author_id
HAVING post_count > 10;统计、分组、聚合——这些都是 SQL 的领域。
await env.DB.batch([
env.DB.prepare('UPDATE accounts SET balance = balance - ? WHERE id = ?').bind(
100,
fromId,
),
env.DB.prepare('UPDATE accounts SET balance = balance + ? WHERE id = ?').bind(
100,
toId,
),
])D1 支持批量操作的原子性,KV 不支持事务。
如果你的数据有明确的 schema、多个字段、字段间有约束关系,D1 的表结构比 KV 的扁平 key-value 更合适。
实际项目中,KV 和 D1 经常一起用:
async function getPost(slug: string, env: Env) {
// 1. 先查 KV 缓存
const cacheKey = `post:${slug}`
const cached = await env.CACHE.get(cacheKey, 'json')
if (cached) {
return cached
}
// 2. 缓存未命中,查 D1
const post = await env.DB.prepare(
`SELECT posts.*, users.name as author_name
FROM posts
JOIN users ON posts.author_id = users.id
WHERE posts.slug = ?`,
)
.bind(slug)
.first()
if (post) {
// 3. 写入缓存,10 分钟过期
await env.CACHE.put(cacheKey, JSON.stringify(post), { expirationTtl: 600 })
}
return post
}这个模式很常见:
当数据更新时,记得清理缓存:
async function updatePost(id: number, data: any, env: Env) {
// 更新 D1
await env.DB.prepare('UPDATE posts SET title = ?, content = ? WHERE id = ?')
.bind(data.title, data.content, id)
.run()
// 清理相关缓存
const post = await env.DB.prepare('SELECT slug FROM posts WHERE id = ?')
.bind(id)
.first()
if (post) {
await env.CACHE.delete(`post:${post.slug}`)
}
}快速判断:
| 你的需求 | 推荐方案 |
|---|---|
| 存用户 session | KV |
| 存用户资料 | D1 |
| 缓存 API 响应 | KV |
| 博客文章系统 | D1(可加 KV 缓存) |
| 特征开关 / 配置 | KV |
| 电商订单 | D1 |
| 限流计数(精确) | Durable Objects |
| 限流计数(粗略) | KV |
| 全局计数器 | Durable Objects |
不行。KV 没有查询能力,没有索引,没有事务。如果你发现自己在用 KV 模拟这些功能(比如手动维护多个索引 key),说明你应该用 D1。
不完全对。KV 的读取确实更快(边缘缓存),但如果你的访问模式需要查询,用 KV 反而会更慢——因为你可能需要多次读取才能拼出结果。
要看场景。Session、缓存、配置这些数据用 KV 完全没问题。"最终一致"的影响是:写入后 60 秒内,其他地区可能读到旧数据。对于大多数场景,这完全可以接受。
10 GB 的结构化数据其实很多。一篇博客文章平均 10 KB,10 GB 能存 100 万篇。如果你真的需要更大,Cloudflare 的建议是拆库(per-tenant),而不是单库无限扩。
| 场景 | 选择 | 原因 |
|---|---|---|
| 全局配置、特征开关 | KV | 读多写少,按 key 访问 |
| Session 存储 | KV | 精确查找 + 过期时间 |
| API 响应缓存 | KV | 读多写少,允许短暂过期 |
| 用户数据、内容系统 | D1 | 需要结构化查询 |
| 订单、交易记录 | D1 | 需要事务和关联 |
| 热点数据加速 | KV + D1 | D1 存储,KV 缓存 |
| 精确计数器 | Durable Objects | 需要强一致性 |
一句话:KV 是缓存,D1 是数据库。用对地方,它们都很强。