deno.com
本页内容

键空间

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

跳转到标题

Deno KV 中的键是键部分的序列,可以是 stringnumberbooleanUint8Arraybigint

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

当攻击者通过将键编码方案中使用的分隔符注入用户控制的变量来操纵键值存储的结构时,就会发生键注入攻击,从而导致意外行为或未经授权的访问。例如,考虑一个使用斜杠 (/) 作为分隔符的键值存储,其键可能像 "users/alice/settings" 和 "users/bob/settings"。攻击者可以创建一个名为 "alice/settings/hacked" 的新用户,以形成键 "users/alice/settings/hacked/settings",从而注入分隔符并操纵键结构。在 Deno KV 中,注入将导致键 ["users", "alice/settings/hacked", "settings"],这不会造成危害。

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

键区分大小写,并按其部分进行字典序排序。第一部分是最重要的,最后一部分是最不重要的。部分的顺序由部分的类型和值共同决定。

键部分排序 跳转到标题

键部分按其类型进行字典序排序,在给定类型内部,则按其值进行排序。类型的排序如下

  1. Uint8Array
  2. 字符串
  3. number
  4. bigint
  5. 布尔值

在给定类型内部,排序为

  • 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"]; // User with ID 42's profile
["posts", "2023-04-23", "comments"]; // Comments for all posts on 2023-04-23
["products", "electronics", "smartphones", "apple"]; // Apple smartphones in the electronics category
["orders", 1001, "shipping", "tracking"]; // Tracking information for order ID 1001
["files", new Uint8Array([1, 2, 3]), "metadata"]; // Metadata for a file with Uint8Array identifier
["projects", "openai", "tasks", 5]; // Task with ID 5 in the OpenAI project
["events", "2023-03-31", "location", "san_francisco"]; // Events in San Francisco on 2023-03-31
["invoices", 2023, "Q1", "summary"]; // Summary of Q1 invoices for 2023
["teams", "engineering", "members", 1n]; // Member with ID 1n in the engineering team

通用唯一字典序可排序标识符 (ULID) 跳转到标题

键部分排序允许由时间戳和 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"]; // First user
["users", 1691377037924, "8063f20c-8c2e-425e-a5ab-d61e7a717765"]; // Second user
["users", 1691377037925, "35310cea-58ba-4101-b09a-86232bf230b2"]; // Third user

然而,在某些情况下,将时间戳和 ID 表示在单个键部分中可能更直接。为此,您可以使用通用唯一字典序可排序标识符 (ULID)。这种类型的标识符编码了 UTC 时间戳,按字典序可排序,并且默认是加密随机的。

import { ulid } from "jsr:@std/ulid";

const kv = await Deno.openKv();

async function setUser(user) {
  await kv.set(["users", ulid()], user);
}
["users", "01H76YTWK3YBV020S6MP69TBEQ"]; // First user
["users", "01H76YTWK4V82VFET9YTYDQ0NY"]; // Second user
["users", "01H76YTWK5DM1G9TFR0Y5SCZQV"]; // Third user

此外,您可以使用 monotonicUlid 函数单调递增地生成 ULID。

import { monotonicUlid } from "jsr:@std/ulid";

async function setUser(user) {
  await kv.set(["users", monotonicUlid()], user);
}
// Strict ordering for the same timestamp by incrementing the least-significant random bit by 1
["users", "01H76YTWK3YBV020S6MP69TBEQ"]; // First user
["users", "01H76YTWK3YBV020S6MP69TBER"]; // Second user
["users", "01H76YTWK3YBV020S6MP69TBES"]; // Third user

跳转到标题

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

  • undefined
  • null
  • 布尔值
  • number
  • 字符串
  • bigint
  • Uint8Array
  • Array
  • Object
  • Map
  • Set
  • Date
  • RegExp

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

支持值中的循环引用。

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

Deno.KvU64 类型 跳转到标题

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

它可以使用 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/;

// Circular references are supported
const a = {};
const b = { a };
a.b = b;

// Deno.KvU64 is supported
new Deno.KvU64(42n);

版本戳 跳转到标题

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

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

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

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

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

您找到所需内容了吗?

隐私政策