跳到主要内容

键空间

Deno KV 目前处于测试阶段

Deno 的 KV 及其相关的云基元 API,如队列和定时任务,目前仍处于实验性阶段,可能会发生变化。尽管我们尽力确保数据持久性,但特别是在 Deno 更新时,仍然存在数据丢失的可能性。

在启动使用 KV 的 Deno 程序时,需要添加--unstable标志,如下所示:

deno run -A --unstable my_kv_code.ts

Deno KV 是一个键值存储。键空间是键+值+版本戳对的平坦命名空间。键是键部分的序列,允许建模分层数据。值是任意的 JavaScript 对象。版本戳表示值何时被插入/修改。

Deno KV 中的键是键部分的序列,可以是字符串、数字、布尔、Uint8Array 或 bigint。

使用一系列部分而不是单个字符串消除了分隔符注入攻击的可能性,因为没有可见的分隔符。

键注入攻击发生在攻击者通过向键编码方案中使用的分隔符中注入用户可控变量来操纵键值存储的结构,导致意外行为或未经授权的访问。例如,考虑使用斜杠(/)作为分隔符的键值存储,具有类似 "user/alice/settings" 和 "user/bob/settings" 的键。攻击者可以创建一个新用户,名为 "alice/settings/hacked",以形成键 "user/alice/settings/hacked/settings",注入分隔符并操纵键的结构。在 Deno KV 中,注入将导致键 ["user", "alice/settings/hacked", "settings"],这是无害的。

在键部分之间,不可见的分隔符用于分隔部分。这些分隔符从不可见,但确保一个部分不能与另一个部分混淆。例如,键部分 ["abc", "def"]["ab", "cdef"]["abc", "", "def"] 都是不同的键。

键区分大小写,按其部分的字典顺序排序。第一部分最重要,最后部分最不重要。部分的顺序由部分的类型和值的类型和值来确定。

键部分排序

键部分按其类型的字典顺序排序,而在给定类型内,它们按值的大小排序。类型的排序如下:

  1. Uint8Array
  2. string
  3. number
  4. bigint
  5. boolean

在给定类型内的排序是:

  • Uint8Array:数组的字节顺序
  • string:字符串的 UTF-8 编码的字节顺序
  • number:-Infinity < -1.0 < -0.5 < -0.0 < 0.0 < 0.5 < 1.0 < Infinity < NaN
  • bigint:数学顺序,最大的负数首先,最大的正数最后
  • boolean:false < true

这意味着部分 1.0(数字)在部分 2.0(也是数字)之前排序,但大于部分 0n(bigint),因为 1.0 是数字而 0n 是 bigint,类型排序优先于类型内的值排序。

键示例

["users", 42, "profile"]; // 用户 ID 42 的个人资料
["posts", "2023-04-23", "comments"]; // 2023-04-23 所有帖子的评论
["products", "electronics", "smartphones", "apple"]; // 电子类别中的苹果智能手机
["orders", 1001, "shipping", "tracking"]; // 订单 ID 1001 的跟踪信息
["files", new Uint8Array([1, 2, 3]), "metadata"]; // 具有 Uint8Array 标识符的文件的元数据
["projects", "openai", "tasks", 5]; // OpenAI 项目中 ID 5 的任务
["events", "2023-03-31", "location", "san_francisco"]; // 2023-03-31 旧金山的事件
["invoices", 2023, "Q1", "summary"]; // 2023 年 Q1 发票摘要
["teams", "engineering", "members", 1n]; // 工程团队中 ID 1n 的成员

通用唯一字典序可排序标识符(ULIDs)

键部分排序允许包含时间戳和 ID 部分的键按时间顺序列出。通常,可以使用以下方式生成键:Date.now()crypto.randomUUID()

async function setUser(user) {
await kv.set(["users", Date.now(), crypto.randomUUID()], user);
}

连续多次运行,这会产生以下键:

["users", 1691377037923, "8c72fa25-40ad-42ce-80b0-44f79bc7a09e"]; // 第一个用户
["users", 1691377037924, "8063f20c-8c2e-425e-a5ab-d61e7a717765"]; // 第二个用户
["users", 1691377037925, "35310cea-58ba-4101-b09a-86232bf230b2"]; // 第三个用户

然而,在某些情况下,将时间戳和 ID 表示为单个键部分可能更为直观。您可以使用通用唯一字典序可排序标识符(ULID)来实现这一点。这种类型的标识符编码了一个 UTC 时间戳,按字典顺序排序,并默认情况下具有密码学随机性:

import { ulid } from "https://deno.land/x/ulid/mod.ts";

const kv = await Deno.openKv();

async function setUser(user) {
await kv.set(["users", ulid()], user);
}
["users", "01H76YTWK3YBV020S6MP69TBEQ"]; // 第一个用户
["users", "

01H76YTWK4V82VFET9YTYDQ0NY"]; // 第二个用户
["users", "01H76YTWK5DM1G9TFR0Y5SCZQV"]; // 第三个用户

此外,您可以使用工厂函数逐渐增加地生成 ULID:

import { monotonicFactory } from "https://deno.land/x/ulid/mod.ts";

const ulid = monotonicFactory();

async function setUser(user) {
await kv.set(["users", ulid()], user);
}
// 通过将最不显著的随机位递增1来获得相同时间戳的严格排序
["users", "01H76YTWK3YBV020S6MP69TBEQ"]; // 第一个用户
["users", "01H76YTWK3YBV020S6MP69TBER"]; // 第二个用户
["users", "01H76YTWK3YBV020S6MP69TBES"]; // 第三个用户

Deno KV 中的值可以是与结构化克隆算法兼容的任意 JavaScript 值,包括:

  • undefined
  • null
  • boolean
  • number
  • string
  • bigint
  • Uint8Array
  • Array
  • Object
  • Map
  • Set
  • Date
  • RegExp

对象和数组可以包含上述任何类型,包括其他对象和数组。MapSet 也可以包含上述任何类型,包括其他 MapSet

支持值内的循环引用。

不支持具有非原始原型的对象(例如类实例或 Web API 对象)。函数和符号也不能被序列化。

Deno.KvU64 类型

除了结构化可序列化值之外,还支持特殊值 Deno.KvU64 作为值。这个对象表示一个 64 位无符号整数,表示为一个 bigint。它可以与 KV 操作的 summinmax 一起使用。它不能存储在对象或数组中,必须存储为顶级值。

它可以使用 Deno.KvU64 构造函数创建:

const u64 = new Deno.KvU64(42n);

值示例

undefined;
null;
true;
false;
42;
-42.5;
42n;
"hello";
new Uint8Array([1, 2, 3]);
[1, 2, 3];
{ a: 1, b: 2, c: 3 };
new Map([["a", 1], ["b", 2], ["c", 3]]);
new Set([1, 2, 3]);
new Date("2023-04-23");
/abc/;

// 支持循环引用
const a = {};
const b = { a };
a.b = b;

// Deno.KvU64 受支持
new Deno.KvU64(42n);

版本戳

Deno KV 键空间中的所有数据都具有版本。每次插入或修改值时,都会为其分配版本戳。版本戳是单调递增的、非顺序的、12 字节值,表示值被修改的时间。版本戳不代表实际时间,而代表值被修改的顺序。

由于版本戳是单调递增的,它们可以用来确定给定值是否比另一个值更新或更旧。这可以通过比较两个值的版本戳来完成。如果版本戳 A 大于版本戳 B,则值 A 比值 B 更新。

versionstampA > versionstampB;
"000002fa526aaccb0000" > "000002fa526aacc90000"; // true

由单个事务修改的所有数据都被分配相同的版本戳。这意味着如果在同一原子操作中执行两个 set 操作,那么新值的版本戳将相同。

版本戳用于实现乐观并发控制。原子操作可以包含检查,以确保它们正在操作的数据的版本戳与操作传递给操作的版本戳匹配。如果数据的版本戳与操作传递给操作的版本戳不同,那么事务将失败,操作将不会应用。