KV 存储入门:命名空间、读写与最终一致的实测延迟
读完这一篇,你能自己建一个 KV 命名空间、在 Worker 里通过 binding 读写它、用 CLI 查看里面的 key,并且知道哪些场景不该用它。下面的延迟数字来自 2026-11-10 在本机账号上真实创建的一个临时命名空间(cf59-kv-demo),验证完 Worker 和命名空间都已删除——其中"删除后 list 还有旧 key"这一条,是这次实测里最值得记住的现象。
1. KV 是什么形状的东西
KV 是全局键值存储,一个命名空间里放一堆 key -> value,value 可以是字符串、JSON 或二进制(ArrayBuffer)。它没有表结构、没有查询语言、没有事务,只有四种动作:put、get、delete、list。
写入会落到中心存储,之后被缓存到各地边缘。这意味着写完之后在别的机房读,可能还是旧值。官方文档对这个行为的说法是:变更通常立刻在写入发生的那个网络位置可见,其他位置最多需要 60 秒或更久。这一篇的第 5 节会用实测数据说明它到底有多"最终"。
如果你的需求是强一致、原子计数、或者同一 key 每秒写几十次,KV 不合适,往上走是 Durable Objects,见 62-workers-durable-objects.md。如果是文件类对象(图片、压缩包),看 60-workers-r2-image-hosting.md。
2. 建命名空间
wrangler kv namespace create cf59-kv-demo
真实输出:
🌀 Creating namespace with title "cf59-kv-demo"
✨ Success!
To access your new KV Namespace in your Worker, add the following snippet to your configuration file:
[[kv_namespaces]]
binding = "cf59_kv_demo"
id = "cb0502164c1a406a94d4052cd20d68e6"
拿到 id 之后写进 wrangler.toml。binding 名可以自己改,我用的是大写形式:
name = "cf59-kv-demo"
main = "src/index.js"
compatibility_date = "2026-10-08"
[[kv_namespaces]]
binding = "CF59_KV"
id = "cb0502164c1a406a94d4052cd20d68e6"
部署时 wrangler 会把这个绑定打印出来,一眼能确认挂对了:
Your Worker has access to the following bindings:
Binding Resource
env.CF59_KV (cb0502164c1a406a94d4052cd20d68e6) KV Namespace
本地开发不需要 id 也能跑,wrangler dev 会用本地模拟的 KV;只有要摸到远端数据时才需要真实 id。命名空间数量与读写额度见 56-free-tier-limits-cheatsheet.md。
3. 在 Worker 里读写
env 上就是绑定名,方法都是异步的:
export default {
async fetch(request, env) {
const url = new URL(request.url);
const kv = env.CF59_KV;
const k = url.searchParams.get("k") || "";
const v = url.searchParams.get("v") || "";
if (url.pathname === "/put") {
const t0 = Date.now();
await kv.put(k, v, { expirationTtl: 3600 });
return Response.json({ ok: true, key: k, ms: Date.now() - t0 });
}
if (url.pathname === "/get") {
const t0 = Date.now();
const val = await kv.get(k);
return Response.json({ key: k, value: val, ms: Date.now() - t0 });
}
return new Response("Not Found", { status: 404 });
}
};
expirationTtl 单位是秒,这里是 1 小时。也可以用 expiration 传绝对时间戳(秒)。不设就永不过期——写日志、存临时验证码这类场景记得设。
get 的第二个参数决定返回类型:
const s = await kv.get("k1"); // string 或 null
const o = await kv.get("obj:1", "json"); // 解析后的对象
const b = await kv.get("bin", "arrayBuffer");
const t = await kv.get("k1", "stream");
实测 get("obj:1", "json") 返回 {"type":"object","obj":{"a":1,"b":"x"}},说明运行时帮你 JSON.parse 了。key 不存在时返回 null,不抛异常——这一点和很多 KV 客户端不同,判断要用 === null 而不是 try/catch。
需要元数据和缓存状态时用 getWithMetadata:
const { value, metadata, cacheStatus } = await kv.getWithMetadata("k1");
// 实测:{"value":"hello","metadata":null,"cacheStatus":null}
metadata 是 put 时第三个参数里带的,cacheStatus 在普通读里是 null,只有 cacheTtl 生效时才有意义。
4. 列 key
const res = await kv.list({ prefix: "k", limit: 10 });
// {"keys":[{"name":"k1","ttl":1794296820},{"name":"k2","ttl":1794296823}],
// "list_complete":true,"cursor":null}
ttl 字段是过期时间的 unix 秒。实测 1794296820 对应 2026-11-10 07:47:00Z,而写入发生在 06:47 左右,正好是 expirationTtl: 3600 的结果。没有设过期的 key 里不会有 ttl 字段。
list 一次最多返回 1000 个 key,结果多了要给 cursor 翻页。这是 KV 唯一能做"查询"的手段,只能按前缀过滤,不能按 value 内容查。
5. 延迟与最终一致:实测
八个请求挨个打,记录 Worker 内部计时:
| 操作 | 耗时 | 说明 |
|---|---|---|
第一次 put k1 | 473ms | 冷写,落到中心存储 |
get k1 | 6ms | 同机房刚刚写过,命中缓存 |
get missing-key | 207ms | 未命中,负结果也要走一趟 |
第二次 put k2 | 465ms | 与第一次接近 |
get k2(删除后) | 5ms | 返回 null |
写比读慢两个数量级:473ms 对 6ms。这个差距在写多读少的场景无所谓,在"每次请求都写一下"的场景会直接拖垮 P95。命中缓存之后的读是 6ms,比查一次 D1 或 KV 之外的存储都快。
真正有意思的是删除后的 list。del?k=k2 返回 {"deleted":"k2"},紧接着 get k2 立刻返回 null,但同一个时刻的 list 里 k2 还在:
{"keys":[{"name":"k1","ttl":1794296820},{"name":"k2","ttl":1794296823}], ...}
等 45 秒再查,k2 已经从列表里消失了:
{"keys":[{"name":"k1","ttl":1794296820}],"list_complete":true,"cursor":null}
这就是"最终一致"的实际手感:单 key 的读在写入位置附近很快正确,而 list 这种索引类操作滞后更明显。用它做过期清理、后台扫 key 的定时任务时,扫到已经删掉的 key 属于正常现象,代码里要容忍。定时任务的写法见 63-workers-cron-triggers.md。
6. 用 CLI 看数据
不想为了看一眼值就写接口,CLI 更直接:
wrangler kv key list --namespace-id=<id> --remote
wrangler kv key get k1 --namespace-id=<id> --remote
实测 key list 返回 JSON 数组,带 expiration 的 key 和不带的混在一起;key get 直接输出 hello。--remote 表示操作线上数据,不加则落在本地模拟存储里——这两个存储是两套,本地写的线上看不到,容易误判为"数据丢了"。
命名空间删除是不可逆的,命令会要求确认:
About to delete remote KV namespace id: "cb0502164c1a406a94d4052cd20d68e6".
This action is irreversible and will permanently delete all data in the KV namespace.
非交互环境下 wrangler 会自动填 yes,脚本里删错一次就没了,命令里的 id 要核对两遍。
7. 常见问题
问:get 返回 null 和返回 "null" 怎么区分? 答:key 不存在时是 null(typeof 为 object);存进去的字符串 "null" 返回的是字符串。如果存的是 JSON,用 get(k, "json") 之后不存在同样是 null,所以判断"有没有"最好靠 list 或者写入时带元数据。
问:本地 wrangler dev 写的 key 为什么线上读不到? 答:默认是本地模拟存储,落在 .wrangler/state 下。要操作远端得加 --remote(CLI)或改 dev 的远端模式。这一点在 65-wrangler-dev-debug.md 里还会遇到。
问:value 能放多大? 答:单个 value 上限 25 MiB,key 名 512 字节。放图片、视频不适合 KV,那是 R2 的活。
小结
KV 的用法就四个方法加一个绑定:put 写入、get 读取、delete 删除、list 按前缀枚举。env 上的绑定名直接就是入口,本地和线上是两套存储,别混。
选型上记住两组数字:写 400ms 级、读到缓存 6ms 级,所以它天生适合"读很多、写很少"——配置、白名单、静态资源、落地页文案。反过来,凡是需要"写完立刻在别处读到"或者"同一 key 高频写"的,KV 会给你最终一致的惊喜,该换 Durable Objects。数据规模和额度的边界在 56-free-tier-limits-cheatsheet.md,继续往下学存储的话,关系型数据从 61-workers-d1-database.md 开始。
数据来源与检索时间
检索时间 2026-11-10。全部延迟、返回体、CLI 输出均为本机对真实命名空间与已部署 Worker 的执行结果。
| 来源 | URL | 对应数据 |
|---|---|---|
| KV 工作原理 | https://developers.cloudflare.com/kv/concepts/how-kv-works/ | 中心存储 + 边缘缓存模型、写入后其他位置最多 60 秒可见、不适合原子操作与高频写、推荐场景清单 |
| KV 读写 API | https://developers.cloudflare.com/kv/api/ | put / get / delete / list 的方法签名与 expirationTtl 参数(未抓取) |
| Wrangler KV 命令 | https://developers.cloudflare.com/kv/reference/kv-commands/ | kv namespace create/delete、kv key list/get 的参数(未抓取) |
核验说明
- 已部署验证并删除:
wrangler kv namespace create cf59-kv-demo创建命名空间cb0502164c1a406a94d4052cd20d68e6;Workercf59-kv-demo部署到cf59-kv-demo.hyysn.workers.dev(上传 1.55 KiB / gzip 0.62 KiB,启动 1ms)。验证后wrangler delete cf59-kv-demo --force与wrangler kv namespace delete均返回成功,kv namespace list已查不到该命名空间。 - 第 5 节的五组延迟、
getWithMetadata返回、list的ttl与两次 list 对比、key list/key get输出,全部来自上述真实环境的请求与命令。 - "60 秒可见"这一条引自 KV 工作原理页面的正文,本次只观察到 45 秒后 list 索引消失,没有做跨机房的多点对比,因此不能给出更精确的传播时延。
- 账号里已有的其他命名空间(
ATTACK_KV_*、TOKENS等)全程未改动,kv namespace list的完整输出只用于确认自己的命名空间已删除。 - 未做:没有测 25 MiB value 的实际写入、没有测
cacheTtl参数对读延迟的影响、没有测 1000 个 key 以上的cursor翻页。