Cloudflare 存储怎么选:KV、D1、R2、Durable Objects 的一致性与边界
Cloudflare 的存储产品不少,新手最常见的困惑是:这些东西看起来都能"存数据",到底该用哪个?本文的核心观点是:别按"存什么"选,按"需要什么一致性、怎么访问"选。选错的代价通常不是性能,而是正确性。
本文涉及的限额与行为以 2026-10 官方文档为准,会变化。除特别标明「本地实测」外,均来自官方文档、未实测;本地模拟(wrangler dev)无法复现跨地区传播延迟、读副本延迟与计费。
先问三个问题
把存储选型压缩成三个问题:
- 数据是对象/二进制,还是结构化记录,还是需要协调的状态?
- 读写之间需要多强的一致性?读到几十秒前的旧值能不能接受?
- 写入的热点在哪里?是读多写少,还是同一个 key 被高频改写?
朴素做法是"所有东西先塞进 KV,因为 API 最简单"。KV 确实最简单,但它的模型是为读多写少、可容忍旧值设计的,用它做计数器或库存扣减,行为会和直觉不符。
四个产品的模型
KV:全球缓存式的最终一致
Workers KV 是最终一致的键值存储。官方文档说明,变更在其他位置可见可能需要「60 秒或更久」(是量级,不是上界),因为各处缓存副本要等超时;文档还提醒,同一位置通常能立即看到自己的写入,但不保证,不要依赖。另外"不存在"这个结果也会被缓存,新建的 key 在别处也可能延迟出现。
它不适合原子操作,也没有事务(官方原话是不适合需要原子操作或单次事务读写的场景)。官方限额页给出:同一 key 每秒 1 次写入,value 最大 25 MiB,key 最大 512 字节(来自官方文档,未实测)。详见 KV 一文。
D1:SQLite 语义的关系型数据库
D1 是托管的 SQLite,有 SQL、索引、约束和迁移。单库上限 Paid 10 GB(官方说明不可提高)、Free 500 MB,每个数据库是单线程的,吞吐取决于查询耗时。写入走主库;开启读副本后,读可以由就近副本服务,用 Sessions API 获得顺序一致性。详见 D1 一文。
R2:强一致的对象存储
R2 是 S3 兼容的对象存储。官方一致性页说明:上传、元数据更新、删除和 list 都是立即生效、对所有客户端可见的强一致;例外是 API key 的权限变更(最终一致,最长约一分钟),以及自定义域名前面的缓存,缓存可能让删除或覆盖的对象继续被旧内容服务,甚至缓存 404。单对象最大约 5 TiB,同一个对象名每秒 1 次写入,超出返回 429。详见 R2 一文。
Durable Objects:单点协调 + 事务性存储
Durable Objects 把"一个 ID 对应一个单线程实例"作为基本模型,每个实例带自己的存储,官方描述为强一致、事务性。它不是"另一个数据库",而是把状态和计算放在同一个地方,用单线程换来无需加锁的正确性。每个对象有约每秒 1000 请求的软上限,单对象 SQLite 存储上限为 10 GB(Paid;Free 的单对象上限官方页面自相矛盾,见 Durable Objects 模型,以官方为准)。详见 Durable Objects 模型。
决策树
是大文件 / 二进制 / 需要 S3 兼容?
└─ 是 → R2
是需要按对象 ID 串行化的状态(房间、计数器、限流、锁)?
└─ 是 → Durable Objects
需要关系查询、JOIN、约束、事务式批量写?
└─ 是 → D1
读多写少、能容忍约一分钟(可能更久)旧值的配置/缓存?
└─ 是 → KV顺序是有意的:先排除"必须强一致或必须协调"的需求,KV 放在最后,因为它是在放宽保证换取读性能。
常见误区
- 用 KV 做计数器或库存。 read-modify-write 没有原子性(本地实测:20 个并发的
get加一再put,最终值是 1),且存在传播延迟。需要正确计数用 DO 或 D1。 - 用 KV 存大量会频繁改写的单个 key。 同一 key 每秒 1 次写入是文档给出的限额,超出会收到 429;本地模拟不会执行这条限制,所以本地测试通过不代表线上不会被拒。
- 把 R2 当数据库。 它没有查询能力;索引和元数据放 D1,文件本体放 R2,是常见的组合。
- 一个全局 DO 扛所有请求。 单线程意味着单点吞吐上限。按业务实体分片(一个用户、一个房间一个 ID),而不是一个"总管"。
- D1 当作无限扩展的库。 Paid 10 GB 的单库上限不可提高,数据模型天然按租户/用户拆库才是出路;拆不动时该考虑别的方案(见 D1 一文最后一节)。
- 忽略缓存层对 R2 一致性的影响。 直连 Worker 绑定和 S3 API 读的是桶本身,不受缓存影响;走自定义域名则可能读到旧内容。
一致性强弱的实际含义
"最终一致"和"强一致"不是形容词,而是对你代码的约束。强一致的存储(R2、DO、D1 主库)允许你写出"写完立刻读"的直线逻辑;最终一致的 KV 则要求你的代码对旧值免疫:读到旧值不会造成错误,只会造成短暂的陈旧。判断方法很简单:如果读到一分钟甚至更久之前的值会让用户看到错误金额、重复扣减或权限误判,这份数据就不该放在 KV。反过来,功能开关、站点配置、渲染好的页面片段,晚一分钟甚至更久生效通常无伤大雅,用 KV 换来的是全球就近读取的低延迟。
还有一个常被忽略的维度是写入热点。KV 同一 key 每秒一次写入、R2 同一对象名每秒一次写入,这些都是官方限额(来自官方文档,未实测),意味着它们不适合当作高频更新的共享状态;高频更新的共享状态天然属于单点串行的 Durable Objects。
组合比单选常见
真实系统通常是组合:D1 存业务记录,R2 存附件,KV 存配置和渲染缓存,DO 负责实时协调或限流。组合的原则是每一份数据只有一个权威来源,其他位置都是可重建的派生缓存。例如 KV 里缓存的页面可以丢,D1 里的订单不能丢。这条原则能避免绝大多数一致性事故。
迁移与演进
选型不是一次性决定。典型路径是:早期用 D1 加 R2 覆盖大部分需求,出现读热点时在前面加 KV 或 Cache 做派生缓存,出现需要强协调的实时功能时再引入 Durable Objects。由于每份数据只有一个权威来源,增加缓存层不会改变正确性,只改变新鲜度,这让演进成本很低。反方向则要小心:把原本放在 KV 的权威数据迁到 D1 或 DO,需要同时处理写路径与旧值清理,所以一开始就别让 KV 成为权威来源。
小结
- 先按一致性和访问模式选,再按数据形状选。
- KV 是放宽保证的缓存,R2 是强一致的对象,D1 是关系库,DO 是按 ID 串行的状态机。
- 不确定时选更强一致的那个,等到性能真的成为问题再向 KV 缓存方向让步。