首页
/ Deno KV 存储引擎解析:ext/kv 的 Storage 后端、KV Connect 协议与 op 层实现

Deno KV 存储引擎解析:ext/kv 的 Storage 后端、KV Connect 协议与 op 层实现

2026-09-04 18:43:41作者:冯爽妲Honey

本文以 Deno 仓库中的 ext/kv/README.md 为骨架,系统讲解 Deno KV(deno_kv crate,自述为 "Implementation of the Deno database API")的实现原理:它以 Database trait 为可插拔存储接口,内置 SQLite 本地后端与实现 KV Connect 协议的远程后端,并通过一组 op_kv_* op 把后端能力暴露给 JS 层的 Deno.openKv API。读完本文,你将理解 KV 的三种打开路径(默认本地库、kv.sqlite3 文件、:memory:/远程 URL)各自落在哪段 Rust 代码上,以及各类配额限制、游标编码、原子写校验是如何在运行时被强制执行的。

一、crate 定位与文档骨架

ext/kv/README.md 明确了三件事,本文按同一脉络展开:

  1. deno_kv 是 Deno 的键值存储 crate。README 指出 Deno KV 的用户手册在 Deno 官方文档(Deploy KV Manual)中,本 crate 是其运行时实现;
  2. 可插拔的存储接口(Storage Backends):README 列出了两种官方后端——SQLite(本地开发默认,实现位于独立仓库 denokvdenokv_sqlite crate)与 Remote(对接实现 KV Connect 协议的远程服务,例如 Deno Deploy),并说明"额外后端可通过实现 Database trait 添加";
  3. KV Connect 协议:它定义了 Deno CLI 与远程 KV 数据库的通信方式,协议规范与 protobuf 定义(proto/kv-connect.mdproto/schema/datapath.proto)位于独立仓库 denokvproto 目录下。

从源码结构看,这三点在 ext/kv/Cargo.toml 中一一对应:crate 直接依赖 denokv_proto(协议/类型定义)、denokv_sqlite(SQLite 后端)、denokv_remote(KV Connect 客户端),以及 rusqlite 用于 SQLite 连接管理。

二、可插拔后端:DatabaseHandler 与按前缀路由

"通过实现 Database trait 添加新后端"这句话在源码中的落点是 ext/kv/interface.rs

#[async_trait(?Send)]
pub trait DatabaseHandler {
  type DB: Database + 'static;

  async fn open(
    &self,
    state: Rc<RefCell<OpState>>,
    path: Option<String>,
  ) -> Result<Self::DB, JsErrorBox>;
}

每个后端只需提供一个 DatabaseHandler 实现(type DBdenokv_proto::Database 的具体实现)。ext/kv/dynamic.rs 进一步把它抹平成对象安全的 DynamicDbHandler / RcDynamicDb 动态分发层(DynamicDb trait 暴露 dyn_snapshot_readdyn_atomic_writedyn_dequeue_next_messagedyn_watchdyn_close 五个能力),使 op 层不必关心底层是 SQLite 还是远程服务。

真正体现"可插拔"的是 MultiBackendDbHandlerext/kv/dynamic.rs):

  • MultiBackendDbHandler::remote_or_sqlite(...) 注册了两组前缀:["https://", "http://"] 映射到 RemoteDbHandler,空前缀 [""] 兜底映射到 SqliteDbHandler。因此 Deno.openKv("https://...") 走远程,Deno.openKv("my.sqlite3") 或无参调用走本地 SQLite;
  • open 前还会处理环境变量:DENO_KV_DEFAULT_PATH(无参时作为默认库路径)、DENO_KV_PATH_PREFIX(给库路径加前缀,便于多租户隔离);
  • DENO_KV_REQUIRES_DISTRIBUTED_DATABASE 面向 Deno Deploy 场景:值为 error 时,若路径不是 http(s):// 分布式的库,openKv 直接报错提示"未附加 KV 数据库";值为 warn 时仅打印一次警告并回退到内存库。从这段逻辑可以推断:Deno Deploy 在运行时会借此防止应用"看似成功、实则落到了本地内存"的静默降级;
  • 没有任何前缀匹配成功时返回 TypeError: No backend supports the given path

三、SQLite 后端:路径解析、:memory:、WAL 与跨进程 watch

ext/kv/sqlite.rs 中的 SqliteDbHandler 是本地默认后端,核心逻辑包括:

路径校验(validate_path

  • 未传路径返回 None,交由后端决定落盘位置;
  • 路径为 ":memory:" 时显式打开内存库;
  • 空字符串报 TypeError: Filename cannot be empty
  • : 开头的文件名被拒绝(提示需加 ./ 前缀),这是为了与 SQLite URI 语法隔离;
  • 普通文件路径会经过权限系统 permissions.check_open(..., OpenAccessKind::ReadWriteNoFollow, Some("Deno.openKv")),即 KV 的本地读写受 --allow-read/--allow-write 体系约束,且不允许跟随符号链接。

存储模式与默认库路径

spawn_blocking 中根据环境变量 DENO_KV_DB_MODEdisk/空串为磁盘模式,memory 为内存模式,未知值告警并回退磁盘)决定模式;落盘时:

  • 有显式路径 → rusqlite::Connection::open_with_flags(&path, flags)(并先 canonicalize 用于 watch 通知去重);
  • 无路径但 handler 配置了 default_storage_dir → 在该目录创建并打开 kv.sqlite3。这解释了本地"不传参的 Deno.openKv() 数据存哪了"——default_storage_dir 指向 Deno 缓存目录下的存储目录,库文件名固定为 kv.sqlite3
  • 其余情况全部 open_in_memory

WAL 与 watch 通知

每个连接建立后执行 PRAGMA journal_mode = wal,随后以 SqliteConfig { batch_timeout: None, num_workers: 1 } 构造 denokv_sqlite::Sqlite。值得注意的细节是 SQLITE_NOTIFIERS_MAPext/kv/sqlite.rs 顶部):一个以规范化路径为键的全局 OnceLock<Mutex<HashMap<_, SqliteNotifier>>>,让同一进程内打开同一个文件的多个 Kv 实例共享同一通知器——这正是本地 db.watch() 能跨实例看到变更的原因;内存库则各自使用独立的 SqliteNotifier::default(),互不感知。

此外 versionstamp_rng_seed 允许测试注入随机种子,使本地库生成的 versionstamp 可复现(见 SqliteDbHandler::new)。

四、Remote 后端:KV Connect 协议与打开时校验

ext/kv/remote.rs 实现了 README 中"Remote - backed by a remote service that implements the KV Connect protocol"这一后端,RemoteDbHandler::open 的流程是理解 KV Connect 在客户端侧行为的关键:

  1. 参数与权限:远程库必须提供 URL(缺失报 Missing database url,无法解析报 Invalid database url);随后依次做 check_env("DENO_KV_ACCESS_TOKEN")check_net_url,即需要 --allow-env=DENO_KV_ACCESS_TOKEN 和对应域名的网络权限;
  2. 访问令牌:从环境变量 DENO_KV_ACCESS_TOKEN 读取,缺失时报错并提示在 Deno 控制台获取令牌(源码注释直接写明该 env var 名);
  3. HTTP 客户端:通过 deno_fetch::create_http_client 构造一个强制 HTTP/2http1: false, http2: true)的客户端,支持自定义 root CA、代理、客户端证书,并将其包装为 RemoteTransportFetchClient,仅 POST)与 RemoteResponse(支持 bytes()/stream()/text())两种 trait 实现,交给 denokv_remote::Remote
  4. 打开时校验(fail fast)validate_metadata_endpoint 会立刻向数据库 URL 发送一次 metadata 交换请求(body 为 MetadataExchangeRequest { supported_versions }),约束包括:
    • METADATA_VALIDATION_TIMEOUT = 30s,超时即 openKv 报"timed out connecting to the metadata endpoint";
    • 仅接受 200 OK(与 denokv_remote 保持一致,避免 204 等 2xx 落到难以理解的解析错误);
    • 先单独解析响应里的 version 字段,SUPPORTED_PROTOCOL_VERSIONS: [u64; 3] = [1, 2, 3] 之外的版本报"unsupported KV Connect metadata version",然后再解析完整的 DatabaseMetadata
    • 源码中的 TODO 注释(对应上游 issue #22248)说明:该校验会让一次成功的 openKv 实际 POST 两次 metadata 端点(此处一次、Remote::new 的 refresher 一次),且协议版本常量目前是从 denokv_remote 复制的,未来存在漂移风险。

这段代码同时回答了 README 中"KV Connect 规范与 protobuf 定义在 denokv 仓库 proto 目录"的客户端侧意义:Deno CLI 与任何实现了该协议的服务(如 Deno Deploy)都以 metadata 交换握手、以统一的版本集合协商兼容性。

五、op 层与 JS 层:Deno.openKv 的完整调用链

ext/kv/lib.rs 顶部用 deno_core::extension! 注册了整个扩展,state 中注入 KvConfigRc<dyn DynamicDbHandler>,并声明 8 个 op 与懒加载 JS:

deno_core::extension!(deno_kv,
  deps = [ deno_web ],
  ops = [
    op_kv_database_open,
    op_kv_snapshot_read,
    op_kv_atomic_write,
    op_kv_encode_cursor,
    op_kv_dequeue_next_message,
    op_kv_finish_dequeued_message,
    op_kv_watch,
    op_kv_watch_next,
  ],
  lazy_loaded_js = [ "01_db.ts" ],
  options = {
    handler: Box<dyn DynamicDbHandler>,
    config: KvConfig,
  },
  ...
);

UNSTABLE_FEATURE_NAME = "kv"op_kv_database_open 第一步即 feature_checker.check_or_exit("kv", "Deno.openKv")。也就是说,KV API 当前是不稳定特性,运行时必须显式启用(--unstable-kv 或配置中 unstable: ["kv"]),否则 Deno.openKv 直接退出——使用本 API 的适用前提就写在这里。

打开数据库op_kv_database_open(path: Option<String>) 取出 DynamicDbHandler 调用 dyn_open,成功则把 DatabaseResource { db, cancel_handle } 放进资源表返回 rid。资源关闭时调用 db.close() 并取消相关任务——Kv 实例被 GC 时底层连接随之释放。

键与值的编解码:键在 Rust 侧表示为 Vec<AnyValue>KeyPart 支持 bool/float/Int(BigInt)/String/Bytes),经 denokv_proto::encode_key/decode_key 序列化为字节后传给后端;返回值(V8 序列化、字节、U64 三种,见 FromV8Value/ToV8Value)与 versionstamp(hex 字符串,check 时按 20 字节校验)同样在此完成 V8 ↔ proto 的转换。

op_kv_snapshot_read 接收一组 (prefix, start, end, limit, reverse, cursor) 六元组,先做配额检查(max_read_rangestotal limit ≤ max_read_entries、读键大小),再由 RawSelector::from_tuple 归一化选择器:

  • (prefix, -, -) → 前缀扫描;
  • (prefix, start, -) 要求 start 以 prefix 开头且更长,否则 StartKeyNotInKeyspace(prefix, -, end) 同理校验 EndKeyNotInKeyspace
  • (-, start, end) 要求 start <= end(-, start, -) 展开为精确键区间 start..start||0x00
  • 其余组合报 InvalidRange

游标分页由 encode_cursor/decode_selector_and_cursor 完成:游标是"边界键去掉选择器公共前缀后的 Base64URL 串";带游标续读时正向读起点为 prefix+cursor+0x00(避免重复读到边界键),反向读终点为 prefix+cursor,并有 CursorOutOfBounds 越界防护。op_kv_encode_cursor 则把这套编码暴露给 JS 层手工构造游标。

原子写op_kv_atomic_write 一次接收 checks、mutations、enqueues 三类请求并整体执行:

  • 配额:checks ≤ max_checksmutations + enqueues ≤ max_mutations
  • 版本校验:check 的 versionstamp 必须是 20 字节 hex(InvalidVersionstamp);
  • 变异类型:set/delete/sum/min/max/setSuffixVersionstampedKey,"带值却给了 delete"或"delete 却给了值"都会报 InvalidMutationWithValue/WithoutValue
  • expireIn(毫秒)在当前时间戳上做 checked 加法换算为绝对过期时间,溢出报 InvalidExpireAt 而不是让进程 panic(源码注释明确这是防溢出加固);
  • 体积校验:单个键不超过 max_write_key_size_bytes,单个值(或 enqueue payload)不超过 max_value_size_bytes,总 payload 不超过 max_total_mutation_size_bytes、总键长不超过 max_total_key_size_bytes;U64 值按固定 8 字节计;
  • 成功后返回提交产生的 versionstamp(hex),即 KvAtomicWriteResult.versionstamp

队列与 watchop_kv_dequeue_next_message 取出队首消息并注册 QueueMessageResourcehandle_rid),op_kv_finish_dequeued_message(handle_rid, success) 决定确认还是重试(finish 失败仅打 debug 日志,因为消息反正会被重试)。op_kv_watch 把键编码后交给后端 db.watch(keys) 得到 WatchStream,注册为 DatabaseWatcherResourceop_kv_watch_next 逐批拉取,输出区分 Changed(entry?)(entry 为 null 表示键被删除)与 Unchanged(心跳)。max_watched_keys 在此强制执行。

JS 层约束ext/kv/01_db.ts 在调用 op 之前还有一道校验,例如队列 delay 不得为负、不得大于 30 天(maxQueueDelay = 30 * 24 * 60 * 60 * 1000)、expireIn 必须是非负整数(注释说明这是防止 NaN/Infinity 进入 native 层时溢出 panic)、backoffSchedule 最多 5 段且每段不超过 1 小时。openKv(path) 最终就是 op_kv_database_open 后包成 Kv 对象。

六、配额配置:KvConfig 与默认值

ext/kv/config.rsKvConfigBuilder::build 给出了所有限制的默认值,这是嵌入者(如 Deno CLI 自身)调整 KV 行为的主要入口:

配置项 含义 默认值
max_write_key_size_bytes 写入键(编码后)大小上限 2048
max_read_key_size_bytes 读取键大小上限 写键上限 + 1(注释:范围选择器可能带 0x00/0xff 后缀)
max_value_size_bytes 单值 / enqueue payload 上限 65536(64 KiB)
max_read_ranges 单次快照读的范围数 10
max_read_entries 单次快照读的总 limit 之和 1000
max_checks 单次原子写的 check 数 100
max_mutations 单次原子写的 mutation+enqueue 总数 1000
max_watched_keys 单个 watch 的键数 10
max_total_mutation_size_bytes 单次原子写总 payload 800 * 1024
max_total_key_size_bytes 单次原子写总键长 80 * 1024

超限会分别抛出 TooManyRangesTooManyEntriesTooManyChecksTooManyMutationsKeyTooLargeToRead/WriteValueTooLargeTotalMutationTooLargeTotalKeyTooLarge 等带具体上限值的 JS TypeError(定义在 ext/kv/lib.rsKvErrorKind),方便上层按错误类型与限额做客户端节流。

七、如何阅读与扩展这一模块

  • ext/kv/README.md 出发,先读 ext/kv/lib.rsextension! 声明,即可按 8 个 op 名逐一对照 ext/kv/01_db.tsKv 类的方法实现,建立 API → op → trait 分发的完整链路;
  • 想理解本地存储行为(:memory:DENO_KV_DB_MODEkv.sqlite3、watch 通知器),聚焦 ext/kv/sqlite.rs;想理解远程接入与协议版本协商(DENO_KV_ACCESS_TOKEN、30 秒超时、版本 1/2/3),聚焦 ext/kv/remote.rsvalidate_metadata_endpoint
  • 想接入手写后端:实现 ext/kv/interface.rsDatabaseHandlertype DB: denokv_proto::Database),再经 MultiBackendDbHandler 按前缀注册即可被 Deno.openKv 透明使用;
  • 配额与默认值的调整点集中在 ext/kv/config.rs,所有 max_* 均可通过 builder 覆盖。

需要说明的适用前提:Deno.openKv 依赖不稳定特性开关(--unstable-kv 或等价配置);远程库依赖 DENO_KV_ACCESS_TOKEN 环境变量与网络/环境权限;本地 SQLite 路径受文件权限约束且不支持以 : 开头的文件名。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
504
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384