KernelRanch
返回文章列表

KV 存储入门:命名空间、读写与最终一致的实测延迟

2026-11-10 #Cloudflare

读完这一篇,你能自己建一个 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 k1473ms冷写,落到中心存储
get k16ms同机房刚刚写过,命中缓存
get missing-key207ms未命中,负结果也要走一趟
第二次 put k2465ms与第一次接近
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 读写 APIhttps://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;Worker cf59-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 翻页。