Deno KV是一个基于键值的数据库,从Deno 1.32.0开始作为不稳定的API内置到Deno运行时中。Deno Deploy 现在整合了 Deno KV(目前作为仅限邀请的测试版),并在全球范围内分发 KV 数据。这意味着,当 Web 应用程序置于 Deploy 上时,现在每个服务器实例附近都有一个数据库。Deno KV还提供ACID事务来维护数据一致性。

本文将通过简单易懂的示例涵盖Deno KV的所有方面。由于有关Deno KV的信息在Deno文档中的多个位置,因此请将这篇文章视为KV的一站式指南。

请注意,已经创建了十几个工作代码示例来支持此博客文章。它们可以在此存储库文件夹中找到。您还将在本文的相关部分找到链接的每个示例。

指标

Deno KV术语中的索引是数据存储的单位。在关系数据库 (RDBMS) 术语中,它们可以松散地视为表,但它们更像是 SQL 索引,因为它们按键值排序以便快速查找。

KV 主索引是使用每条记录的唯一键(通常是 UUID)存储记录的索引。我们将在接下来的几节中详细讨论它们。二级索引存储附加信息或用于排序。我们稍后会讨论它们。

键、值和版本戳

Deno KV 具有明确定义的键、值和表示值版本的版本戳。

KV 键

如上所述,Deno KV是一个键值数据库。在最简单的形式中,数据库记录的数据是持久保存的,并使用键找到。在Deno KV中,密钥是一个元组,一个具有固定长度的数组。元组的每个成员都称为一个部分。所有部分都链接在一起形成一种复合键。关键部件可以是 stringnumber boolean Uint8Array 、 或 bigint 的类型。

以下是用于识别唯一记录的 Deno KV 密钥的几个示例:

// User key
const userKey = ["user", <userid>]; // Used for storing user objects
// Address key for a particular User
const addressKey ["address", <userId>]; // Used for storing a user's address

如上所述,当使用唯一键来保存值时,生成的索引称为主索引。Web 平台提供 crypto.getRandomUUID() 获取唯一 ID。

索引是关键部分的有序序列,因此与 [1, "user"] 不同 ["user", 1]

在主索引中使用时,第一个关键部分通常是标识要持久保存的模型集合的字符串常量, "user" "address" 或者在示例中。用于将记录添加到索引时,关键部分将组合成复合键。

索引的初始关键部分可以扩展为多个部分。例如,如果我们有一个具有角色的用户,我们可能会有第二部分表示一个角色,例如:

const userAdminKey = ["user", "admin", <userId>];
const userCustomerKey = ["user", "customer", <userId>];
const userGuestKey = ["user", "guest", <userId>];

如果模型上有角色字段,则前面的键可以简化为如下所示(假设每个用户都有一个角色):

const userRoleKey = ["user_by_role", <userRole>, <userId>];

使用这些键创建的索引称为二级索引。它们需要持久化在具有主索引的原子事务中,以使数据在索引之间保持一致(请参阅下面的二级索引讨论)。二级索引应具有描述其功能的第一部分名称。在这种情况下,键用于查找用户,第一部分是 "user_by_role"

KV 值

为了使 Deno KV 值持久化,值必须是与 JavaScript 的结构化克隆算法兼容的结构化、可序列化的 JavaScript 类型。

基本上,它是可以作为 except SharedArrayBuffer 的第一个 structuredClone() 参数传递的任何内容。这是一个 SharedArrayBuffer 例外,因为 是无法跨 KV 远程隔离传递的共享内存数据结构。

有关不符合结构化克隆算法的内容的更多详细信息,请参阅有关 MDN 上的讨论。

尽管有这些限制,但最常见的JavaScript类型包括 undefined ,,, null boolean number string Array Map Uint8Array Object 作为KV值工作(完整列表)。

Deno 手册指出,不支持将具有非基元原型的对象(如类实例或 Web API 对象)作为 KV 值。

以下是如何使用键和值持久化到 Deno KV 的示例(该方法 set() 将在后面讨论):

// persist an object with id, name and role fields
kv.set(["user", "a12345"], {id: "a12345", name: "Craig", role: "admin"});
// persist an environmental variable value
kv.set(["env", "IS_PROD"], true);
// persist a hit count by page and userId
kv.set(["hits", "account.tsx", "a12345"], 254);

KV 版本戳

每次将新值保存到 Deno KV 时,都会根据保存记录时的时间戳自动为其提供一个 12 字节的版本值。KV 称之为 versionstamp .更新值时,将创建一个新 versionstamp 值。

新的 versionstamp 将始终比前一个修改“大”,因此记录的第二次修改将始终大于布尔比较中的第一次修改(版本戳2>版本戳1)。如果要跟踪和显示记录的版本历史记录,这一点很重要。

确保 versionstamp 事务以原子方式完成,从而确保不同索引中的附属关系记录是一致的。可以检查 Deno KV 中的原子操作,以确保数据在上次获取 ( get() ) 和持久化新数据之间保持一致。此检查是使用记录完成 versionstamp 的(有关详细信息,请参见下文)。

克鲁德操作

KV中主要的CRUD(创建,读取,更新和删除)操作被定义为 Deno.Kv 类上的方法: set() (创建和更新), get() (读取)和 delete() (删除)。所有这些操作都是异步的,因此调用前面需要加上 await 关键字。

💡 演示 CRUD 操作的工作代码可以在此博客附属的存储库中找到。

Deno KV 上的所有 CRUD 操作都从连接到 KV 数据库开始,只需简单地调用 Deno.openKv()

// Open a KV connection returning a Deno.Kv instance
const kv: Deno.Kv = await Deno.openKv();

我们将在下面的示例中使用 kv KV 连接对象( Deno.Kv 实例)。

克鲁德数据

最好创建一个 TypeScript 类型或接口来表示数据模型。例如,用户模型将如下所示:

interface User {
  id: string;
  name: string;
  email: string;
  phone?: string;
}

向模型添加关系,例如地址和电话号码,将使用 AddressPhone 接口。它们都有一个 userId 字段,允许查找特定用户的地址和电话号码。为简单起见,我们将在此处重点介绍 User 模型。

Deno User KV CRUD 操作将保留通常来自用户填写的 HTML 注册表单的数据。在本例中,我们将手动创建一个具有唯一 ID 的用户:

const userId = "1";
const user = {
  id: userId,
  name: "John Doe",
  email: "john@doe.com",
  phone: "2071231234"
}

用户 id 可以像我在这里所做的那样进行硬编码,但最好将其生成为唯一值。内置 crypto 于 Web 平台中的对象是创建唯一 id 的简单方法:

const userId = crypto.randomUUID();

创建 ( set()

让我们连接到 KV 数据存储并插入 user 需要两个参数的数据,一个 Deno.KvKey 和一个值,其类型是方法的 TypeScript 泛型参数(在此 User 示例中):

const result = await kv.set<User>(["user", userId], user);
if (result.ok === false) {
  throw new Error(`There was a problem persisting user ${user.name}`)
}

在 SQL 数据库术语中,数据表将称为“用户”,用户 ID 将作为主键。

set() 调用的返回值为a Promise<Deno.KvCommitResult>KvCommitResult 对象包含布尔 ok 字段和字段 versionstamp 。如果调用失败 okset() 则设置为 false 。将是 versionstamp 持久记录的版本戳。

读取 ( get()

该方法 get() 用于从Deno KV获取单个记录。例如:

// Assumes kv is a Deno.Kv object
const foundRecord: Deno.KvEntryMaybe<User> = await kv.get<User>(["user", userId]);
// Get user value
const user = foundRecord.value;

调用 返回 get() 包含 Deno.KvEntryMaybe keyvalue versionstamp 属性的对象(的 KvEntryMaybe 可能部分表示结果的值和版本戳可能为 null)。 key 将始终是您在 get() 呼叫中使用的密钥。和 value timestamp 值是在与 关联的 key KV 存储中找到的值。

下面是从 返回 get() 的对象的示例:

// ids obtained from a call to crypto.getRandomUUID()
{"key":["users","4f18bbe6-1e0a-483f-9b89-556be297191c"],
"value":{"id":"4f18bbe6-1e0a-483f-9b89-556be297191c","name":"John Doe","email":"john@doe.com","phone":"2071239876","age":35},
"versionstamp":"0000000000001d960000"}

💡 请注意,调用的值位于 get() 调用结果的属性中 value 。这可能会使您陷入困境,因为您可能希望 get() 调用仅返回值。另请注意,返回值包装在 中 Promise ,因此请确保在调用前面加上 await .

如果使用不在 KV 存储中的 进行调用 get() 则会返回一个同时具有 valueversionstamp 等于 null key 的对象。

set() 选项

该方法 get() 具有一个名为 的 options 可选第二个参数。参数包含一个 options 字段 consistency 。它有两个值 "eventual""strong" (有关详细信息,请参阅下面的讨论)。

在本地运行 Deno KV 时,该 consistency 值无关紧要,因为 KV 存储是本地的。当使用Deno KV的应用程序在Deno Deploy的云中运行时,这是相关的,因为KV存储实例是远程分发的(见下文)。

更新 ( set()

更新 KV 数据也将使用与插入相同的 set() 方法:

user.phone = "5182349876"
// Assumes kv is a Deno.Kv object
const result = await kv.set<User>(["user", userId], user);
if (result.ok === false) {
  throw new Error(`There was a problem persisting user ${user.name}`)
}

更新的参数和返回值与创建/插入调用 set() 相同。

删除 ( delete()

使用需要键 ( Deno.KvKey ) 参数的方法删除 delete() 记录。

// Assumes kv is a Deno.Kv object
await kv.delete(["user", userId]);

delete 方法返回解析为 undefined 值的 a Promise<void>

建议在事务中完成突变方法 set()delete() ,该事务将返回带有 ok 属性的结果,以指示事务是成功 ( ok: true ) 还是失败 ( ok: false )。有关更多详细信息,请参阅下面的交易部分。

读取多条记录 ( list()getMany()

读取多条记录涉及在 上使用 list() Deno.KvgetMany() 、 上的两种方法。

使用 KV list() 读取列表

该方法 list() 获取多条记录并生成异步迭代器 ( Deno.KvListIterator )。循环使用这种迭代器的最简单方法是使用带循环的 for-of await 循环。下面是一个简单的示例:

// The iterator is returned from a call to list().
// 'await' used with 'for' because the iterator is async
for await (const row of iterator) {
  const user = row.value;
  console.log(user.name); // do something with the user object
}

💡 深入研究 list() 该方法的工作代码可以在此博客附属的存储库中找到。存储库中的几乎所有代码文件都以各种方式使用 list()

该方法 list() 有两个参数 selectoroptions

1.第一个 list() 论点: selector

参数 selector 是具有三个可选字段的对象: prefix 、 和 start end

prefix selector 领域

与 和 get delete() 的 CRUD 方法的关键 set() 参数不同, prefix 键是关键部分的子集。所有关键部分通常是字符串文字。

假设您只需要一个用户或用户管理员列表,您的 list() 呼叫将如下所示:

// Assumes kv is a Deno.Kv object
// Get and iterator with a list of users
const iterUser = kv.list<User>({prefix: ["user"]});
// An iterator with a list of admins
const iterAdmin = kv.list<User>({prefix: ["user", "admin"]});

💡 若要避免 TypeScript 错误,请使用泛型参数进行调用 list() (在前面的示例中 User )。

除了在查询中的使用之外,当数据存储在远程 KV 实例(如 Deno Deploy)中时,索引前缀也很重要,因为具有相同前缀的索引彼此靠近,便于快速检索。

Selector 字段 startend

除了 prefixendselector start 字段选项。该方法 list() 采用其中一个或两个字段。第一个可以是 或, prefix 第二个可以是 startstart end

startend 选项定义要选择的 list() KV 记录范围。但是为了能够使用 start or end ,您需要了解键排序是如何完成的(请参阅下面的索引排序部分)。

虽然键排序有助于排序,但 with startend order 用于定义结果集中返回的一系列有序键的开始和结束。

start 选项从第一个匹配的记录开始,而 end 该方法包括所有以前的记录,但不包括它指向的记录。

// Assumes kv is a Deno.Kv object
const rows = kv.list({
  start: ["user_by_name", "Dow"],
  end: ["user_by_name", "Zozos"],
});
for await (const row of rows) {
  const user = row.value as User;
  console.log(user.name);
}

💡 为了使该 start 选项正常工作,您需要确保排序时使用的索引是唯一的。例如,如果示例中显示的“user_by_name”索引是仅使用姓氏创建的,并且如果存在具有相同姓氏的记录,则结果中将只包含一个。若要了解这种情况是如何发生的,请参阅博客存储库中的此示例代码。

您可以配对 startendprefix 字段配对。当与 配对 startprefix ,终点是索引中的最后一条记录。或者,当与 配对 endprefix ,起点是第一个索引记录。下面是一个示例:

// Assumes kv is a Deno.Kv object
const rows = kv.list({
  prefix: ["user_by_name"], // points to first record
  end: ["user_by_name", "Zozos"],
});
// Loop through results
for await (const row of rows) {
  const user = row.value as User;
  console.log(user.name);
}

2.第二个 list() 论点: options

参数 options list() 是一个具有字段集合的对象:

  • limit: number - 限制 list() 结果集的大小。

  • cursor: string - cursor 恢复迭代。回想一下, list() 该方法返回包含字段 cursor 的 a Deno.KvListIterator 。这在分页用例中很重要。

  • reverse: boolean - 以相反的顺序返回列表

    • prefixstart 或者 end 可以与 一起使用 reverse
  • batchSize: number - 执行列表操作的批次的大小。默认值等于“限制”值或 100

  • consistency: Deno.KvConsistencyLevel - 事务一致性, "strong""eventual" (请参阅下面的一致性部分)。

list() 分页

在分页组中迭代列表的关键是返回 list() Deno.KvListIterator .该迭代器具有一个 cursor 字段和一个 next() 方法,这是它实现异步迭代器协议的结果。

若要分页,请通过迭代器结果将光标从一个调用传递到下一个 list() 调用。该结果的值 iterator.cursor 将成为下一次 list() 调用中 cursor 选项的值。下面是它的外观:

// Assumes kv is a Deno.Kv object
// First call to list() returns iterator
let iterator = kv.list<User>({ prefix: ["user_by_age"] }, {limit: 25})
// Second call sets the cursor using the iterator from the first call to set the second call's cursor
iterator = kv.list<User>({ prefix: ["user_by_age"] }, {limit: 25, cursor: iterator.cursor});

当您不知道要查询的 list() 索引中的项数(在本例中为“user_by_age”)时,在获得 null 迭代器之前不能只调用,因为 list() 即使没有可以从迭代器获得的结果,也始终返回一个 KvListIterator 对象。

而是使用 KvListIterator.next() 方法 done 的属性。最好通过示例来说明这一点。

首先,从用户列表开始:

// start with a User interface
interface User {
  id: number;
  name: string;
  age: number;
}
const users: User = [/* Multiple User objects here */];

接下来,我们将创建一个函数来获取迭代器。每次我们想在页面上显示另一组用户时都会调用它。对此方法的第一次调用会将参数设置为空字符串,用于确定是否 cursor 将成为第二个 list() cursor 参数的一部分。

// Obtain a new User iterator
function getIterator<T>(cursor: string, limit: number): Deno.KvListIterator<T> {
  const optionsArg = cursor !== "" ? { limit, cursor } : { limit };
  // Assumes kv is a Deno.Kv object
  const iterator = kv.list<User>({ prefix: ["user_by_age"] }, optionsArg);
  return iterator;
}

processIterator<User>() 函数提取迭代的数据(在本例中为用户),并使用光标返回它们,以便在下次调用 getIterator()

// Called with `User` generic param below (item=User; items=User{})
async function processIterator<T>(
  iterator: Deno.KvListIterator<T>,
): Promise<{cursor: string, items: T[]}> {
  let cursor = "";
  let result = await iter.next();
  const items = [];
  while (!result.done) {
    cursor = iterator.cursor;
    // result.value returns full KvEntry object
    const item = result.value.value as T;
    items.push(item);
    result = await iter.next();
  }
  return {cursor, users};
}

虽然前面的函数是通用的, printUsers 但用于打印出带有页码的对象数组 User

function printUsers(users: User[], pageNum: number) {
  console.log(`Page ${pageNum}`);
  for (const u of users) {
    console.log(`${u.name} ${u.age}`);
  }
}

最后,启动分页,将用户数据打印到控制台:

// print out users in batches of USERS_PER_PAGE
const USERS_PER_PAGE = 3; // aka page size
let pageNumber = 1;
let cursor = "";
let iter = getIterator<User>(cursor, USERS_PER_PAGE);
await processIterator(iter, pageNumber);
printUsers(processedItems.items as User[], pageNum);
pageNumber++;
// point the cursor to the next batch of data
cursor = iter.cursor;
// stop iteration when cursor is empty
while (cursor !== "") {
  iter = getIterator<User>(cursor, USERS_PER_PAGE, keyPart);
  const iterRet = await processIterator<User>(iter);
  cursor = iterRet.cursor;
  // cast items to User[]
  const items: User[] = iterRet.items as User[];
  if (items.length > 0) {
    printUsers(items, pageNum);
  }
  pageNum++;
}

💡 从演示 list() 分页的代码中获取的示例片段可以在与此博客关联的存储库中找到。

在 Web 应用程序中对 KV 结果进行分页

对 Web 应用程序结果进行分页会使用许多以前的代码,包括 getIteratorprocessIterator 。您还需要跟踪 cursor 新页面的请求 pageNumber ,通过页脚中“下一页”和“上一页”链接中的参数将它们传递到下一个 URL 调用中。

如果没有示例代码,这是一件很难用黑白解释的事情(并且在此过程中会延长这篇已经太长的帖子),所以我将把它留给读者作为练习。但是,请继续关注。我希望在以后的文章中详细介绍这一点。

💡 在这一点上,我正在制作一个演示KV分页的Deno Deploy游乐场的原型,但它还没有准备好迎接黄金时间。

将来自多个索引 getMany() 的记录与

该方法 getMany() 提供了在一个操作中对单独的索引执行多次 get() 调用的机会。该方法接受键数组并返回 Deno.KvEntryMaybe 记录数组、包含 keyvalue timestamp 字段的对象。

💡 演示 getMany() 该方法的工作代码可以在与本博客关联的存储库中找到。

重要的是要知道每个键都应该像 get() 在调用中一样返回单个 KvEntryMaybe 对象。因此, getMany() 参数中的键数将始终等于调用返回数组中的结果数。

例如,假设我们有三个环境变量存储在三个单独的索引中。如下所示:

// keys for storing env variables in KV
const githubSecretKey = ["env_var", "GITHUB_ACCESS_KEY"];
const googleSecretKey = ["env_var", "GOOGLE_ACCESS_KEY"];
const discordSecretKey: Deno.KvKey = ["env_var", "DISCORD_ACCESS_KEY"];

// Assumes kv is a Deno.Kv object
// store env vars in separate indexes
await kv.set(discordSecretKey, "password1");
await kv.set(githubSecretKey, "password2");
await kv.set(googleSecretKey, "password3");

// get all env var entries in one call to separate indexes
const envVars = await kv.getMany([
  githubSecretKey,
  googleSecretKey,
  discordSecretKey,
]);
// the 'enVars' result array would contain these values:
//
// enVars[0] = {key: ["env_var", "GITHUB_ACCESS_KEY"], value: "password2", versionstamp: "001234" }
// enVars[1] = {key: ["env_var", "GOOGLE_ACCESS_KEY"], value: "password3", versionstamp: "001235" }
// enVars[2] = {key: ["env_var", "DISCORD_ACCESS_KEY"], value: "password1", versionstamp: "001236" }

该方法 getMany() 采用第二个参数, options 该参数是可选的。参数 option 有一个属性 consistency ,可以是“强”或“最终”。有关此主题的更多详细信息,请参阅数据一致性部分。

交易 atomic()

on atomic() Deno.Kv 方法用于使用 KV 执行事务。它返回一个 Deno.AtomicOperation 类实例。事务操作链接到 atomic() 。必须通过调用 终止 commit() 链,才能完成事务操作并保留数据。

💡 演示原子事务的工作代码可以在本博客附属的存储库中找到

用于 check() 验证交易记录

atomic() 事务链应首先为每个持久操作调用 check() 该方法。该方法 check() 可确保 versionstamp KV 存储中已有的数据与要持久保存的数据匹配 versionstamp 。此功能称为乐观并发控制,可确保 KV 存储中的数据一致性和完整性。

必须先进行调用 get()check() 然后才能提供 的 key check()versionstamp 参数。对于新数据插入, get() 调用将为 versionstamp (和 value ) 返回 null。如果失败,则 check() 事务将失败,并且不会提交数据。

之后 check() ,执行一系列 set() and/or delete() 调用以根据提供的密钥保留或删除 KV 数据。这两种方法的行为方式与 atomic() 在链外调用时的行为方式相同(请参阅上面的 CRUD 部分)。

一个例子将阐明如何使用 check() 。在这里,我们尝试使用更改的电话号码来持久化用户对象。

// Assumes kv is a Deno.Kv object
const user = await kv.get(["user", 123])
const result = await kv.atomic()
  // Make sure versionstamp has not changed after last get()..
  // This method requires both a key and versionstamp
  .check({key: user.key, versionstamp: user.versionstamp})
  // update phone number
  .set(["user", user.id], {...user.value, phone: "2070987654"  })
  .commit();

如果失败,则将 check() 绕过链中的任何持久操作,并且调用将 commit() 返回 Deno.KvCommitError .

使用 set()delete() 用于事务持久性

在前面的示例中, set() 用于创建或更新事务链中的数据。它是一种方法, Deno.AtomicOperation 其工作方式与 Deno.Kv.set() 将键和值作为参数相同。它将值保留在 KV 存储中,并将键作为主键(记录还包含一个 versionstamp )。

同样,也是一种 Deno.AtomicOperation 方法, delete() 其工作方式与其 Deno.Kv 对应方法相同。它需要一个键参数,用于从 Deno KV 存储中删除记录。

两者 delete() 都可以 set() 链接到 atomic() 多个次,但单个持久链中的写入次数不能超过 10 次。

当您需要存储相关对象时,KV 事务持久性的全部功能就会显现出来。例如,如果您的用户拥有地址和电话号码。您应该从定义模型的 Typescript 类型开始:

// userId added prior to persistence
interface User {
  id?: string;
  name: string;
  email: string;
}
interface Address {
  userId?: string;
  street: string;
  city: string
  state: string;
  country: string;
  postalCode: string;
}
interface Phone {
  userId?: string;
  cell: string;
  work?: string;
  home?: string;
}

每个实体都将持久化到原子事务中的单独 KV 索引中。假设此信息是从 HTML 用户注册表单中收集的,并处理成 UserAddress Phone 模型对象。下面是在 KV 事务中插入或更新该数据的函数:

function persistUser(user: User, address: Address, phone: Phone) {
  if (!user.id) {
    const userId = crypto.randomUUID();
    address.userId = userId;
    phone.userId = userId;
  }
  // Assumes kv is a Deno.Kv object
  // get current records
  const userRecord = await kv.get(["user", userId]);
  const addressRecord = await kv.get(["address", userId]);
  const phoneRecord = await kv.get(["phone", userId]);
  await kv.atomic()
    // check that data has not changed
    .check({key: userRecord.key, versionstamp: userRecord.versionstamp})
    .check({key: addressRecord.key, versionstamp: addressRecord.versionstamp})
    .check({key: phoneRecord.key, versionstamp: phoneRecord.versionstamp})
    .set(userKey, user)
    .set(addressKey, address)
    .set(phoneKey, phone)
    .commit();
}

kv.atomic()....commit() 如果事务成功,则调用链的返回值为 a Promise<Deno.KvCommitResult> 。该对象包含两个字段: okversionstamp 。该 ok 值将在成功的事务中。 true

事务失败

如果 atomic() 调用链中的 KV 事务因失败或其他事务错误而失败 check() ,则返回 。 Deno.KvCommitError 在这种情况下,不会保留事务。该 KvCommitError 对象有一个字段 ok 设置为发生故障 false 时。

如果您尝试插入已插入 check() 的数据导致失败,则会发生以下情况:

// Assumes that User 1 has already been added to the KV index
const id = 1; // user id
// Assumes kv is a Deno.Kv object
const result = kv.atomic()
  // Initial insert has null versionstamp
  .check({key: [
    "user", id], versionstamp: null})
  .set(["user", id], `User ${id}`)
  .commit();

if (result.ok === false) {
  throw new Error(`Data insert failed for User ${id} because it already exists`);
}

跟踪记录的历史记录

成功 atomic() 事务中返回的版本 versionstamp 戳是 atomic() 提供给调用链中所有操作的版本戳。这可用于构造记录的版本历史记录。为此,您需要将 持久 versionstamp 化到单独的索引。如下所示:

💡 演示如何跟踪记录历史记录的工作代码可以在此博客关联的存储库中找到。

const userId = crypto.randomUUID();
// Assumes kv is a Deno.Kv object
const user = await kv.get(["user", userId])
const result = await kv.atomic()
  .check(user)
  .set(["user", userId], {id: userId, name: "Joan Smith", email: "jsmith@example.com"})
  .commit();
if (result.ok) {
// add the result versionstamp to a separate "user_versionstamp" index with a timestamp
  await kv.set(["user_versionstamp", userId, result.versionstamp ], {version: result.versionstamp, date: new Date().getTime()});
}

由于 是 versionstamp 键的一部分,因此将 versionstamp 按每个用户的记录排序。

然后,您可以使用 显示 list() 特定用户记录的历史记录。

// display version in reverse chronological order
const iter = kv.list({ prefix: ["user_versionstamp", userId] },
    {reverse:  true});
for await (const version of iter) {
  // display to stdout here; I'm sure you can do better
  console.log(`Version: ${version.value.version} Date: ${version.value.date}`);
}

二级索引

您通常通过创建主索引来开始 Deno KV 实现。主索引将包含一个模型标识符部分和一个对要持久保存的值唯一的部分,如“用户”模型的用户 ID。在这种情况下,将使用 id 找到特定的“用户”记录。

💡 演示如何创建和使用二级索引的工作代码可以在此博客关联的存储库中找到。

创造和突变

二级索引用于查找一条或多条记录,超出您可以使用主索引执行的按 ID 查找搜索的范围。

例如,如果要按电子邮件地址搜索用户,则可以使用电子邮件地址作为搜索条件的索引。这些索引通常具有描述索引的名称部分,例如“user_by_email”。

当您有二级索引时,您需要使用主索引在事务上下文中创建它们,以保持数据一致性。

const userId = crypto.randomUUID();
const user = {id: userId, name:"Joan Smith", email: "joan.smith@example.com"};
const userKey = ["user", userId];
const userEmailLookupKey = ["user_by_email", user.email];
// Assumes kv is a Deno.Kv object
const result = kv.atomic()
  .check({key: userKey, versionstamp: null})
  .check({key: userEmailLookupKey, versionstamp: null})
  .set(userKey, user) // primary index
  .set(userEmailLookup, user.id) // secondary index
  .commit();
if (!result.ok) {
  throw new Error("Problem persisting user & user_by_email");
}

对“user_by_email”索引 get() 的调用将返回用户的 ID。这将用于在“用户”主索引中查找记录。

// The "user_by_email" record's value is the userId (see previous code)
const userByEmailRecord = await kv.get<User>(["user_by_email", userEmail]);
// The value is the user Id
const userByIdRecord = await kv.get<User>(["user", userByEmailRecord.value]);
const user: User = userByIdRecord.value;

更新或删除数据时,需要对主索引和二级索引执行此操作

可以使用多个查找条件(如名称和 id)创建二级索引。在这种情况下,应包含 id,因为多个用户可能具有相同的名称。下面是一个示例:

// Assumes kv is a Deno.Kv object
// A secondary index by name and id
kv.set(["user_by_name", "John Smith", 1], {id: 1, name: "John Smith", email: "jsmith@example.com"})

还可以构建二级索引以按组或类别存储记录。例如,您可能希望按足球运动员在球场上的位置对他们进行分组。这可能看起来像这样:

type Position = "Goalkeeper" | "Defender" | "Midfielder" | "Forward";
interface Player {
  id?: string; // id added later
  name: string;
  position: Position;
}
const players: Player[] = [/* player data goes here */]
// persist players to KV
for (const player of players) {
  player.id = crypto.RandomUUID();
  // Assumes kv is a Deno.Kv object
  await kv.atomic()
    // skip check() calls here;
    .set(["players", player.id], player);
    .set(["players_by_position", player.position, player.id], player);
    .commit();
}
// lookup players by a position
const findPlayersByPosition = async (position: Position) => {
  const iter =  kv.list({prefix: ["players_by_position", position]});
  console.log(`Players in ${position} position:`);
  for await (const player of iter) {
    const playerPosition = await kv.get<Player>([
      "players_by_position",
      player.value.position,
      player.value.id ?? "",
    ]);
    console.log(playerPosition.value?.name);
  }
}
// Lookup midfielders
await findPlayersByPosition("Midfielder");

使用索引排序

在 KV 中,关键部分按其类型按字典顺序(大致字典顺序)排序,在给定类型中,它们按其值排序。类型排序遵循> Uint8Array string > number > bigint > boolean 。在每种类型中,也有一个定义的顺序。

💡 显示索引排序示例的代码可以在与此博客关联的存储库中找到。

键排序的意义在于它可用于对值进行排序。为此,您可以使用所需的排序标准作为关键部分创建二级索引。

例如,如果要按姓氏对用户进行排序,则可以创建如下的主索引和辅助排序索引:

// Assumes kv is a Deno.Kv object
// create indexes within a transaction
await kv.atomic()
  // check() calls omitted for brevity
  .set(["user", <userId>], <user object>) // primary index
  // sorting by name
  .set(["user_by_name", <lastName>, <firstName>, <userId>], <user object>)
  .commit();

您会注意到我已添加到 userId 二级索引中。否则,创建索引时将忽略具有相同名字和姓氏的重复记录。您也可以使用 email (或其他唯一标识符) 代替 userId

💡 有关重复记录排序行为的进一步探索,请参阅本博客所属的存储库。

若要显示排序的值,请使用该方法 list() 。如果要按相反顺序(最大值到最小值)对列表进行排序,请设置 reverse: true .一个很好的例子是按日期排序,最近的日期排序在顶部:

// create user and user_create_date indexes
// Assumes kv is a Deno.Kv object
await kv.atomic()
  // check() calls omitted
  .set(["user", <userId>], <user object>) // primary index
  .set(["user_by_create_date", <createDate>, <userId>], <user object>)
  .commit();
// print out the index previously created in reverse chronological order
const iter = kv.list({prefix: ["user_by_create_date"]}, {reverse: true});
for await (const user of iter) {
  console.log(user.value);
}

💡 演示 KV 排序的工作代码可以在与本博客关联的存储库中找到。

数学运算: summin max

有三个聚合操作可用于跟踪存储在另一个索引中的一系列值的总和、最小值和最大值。每个操作都有一个方法和一个可用于整理这些统计信息 mutate() 的方法。

💡 演示 min()max() sum() 方法的工作代码可以在与本博客关联的存储库中找到。

所有这些操作都是 Deno.AtomicOperations 方法,因此必须将它们链接到 atomic() 作为链终止符 commit()

sum()min() max() 方法

若要跟踪聚合总和、最小值和最大值,可以使用 sum()min() max() 方法。所有这些方法都接受一个键和一个 bigint 类型的值。这是一个例子

//shopping cart item
interface CartItem {
  userId: string;
  itemDesc: string;
  price: number;
}
// cart data
const cart: CartItem[] = [
  { userId: "100", itemDesc: "Arduino Uno kit", price: 60 },
  { userId: "100", itemDesc: "Temp sensor", price: 10 },
  { userId: "100", itemDesc: "Humidity sensor", price: 15 },
  { userId: "100", itemDesc: "Power cord with 5V regulator", price: 18 },
  { userId: "100", itemDesc: "Servo", price: 8 },
];

// add data to indexes
for (const item of cart) {
// Assumes kv is a Deno.Kv object
  kv.atomic()
    .set(["cart", item.userId], item) // primary index
    .min(["cart_min"], BigInt(item.price))
    .max(["cart_max"], BigInt(item.price))
    .sum(["cart_sum"], BigInt(item.price))
    .commit();
}
// get stats
const cartMin = await kv.get(["cart_min"]);
const cartMax = await kv.get(["cart_max"]);
const cartSum = await kv.get(["cart_sum"]);
// print out cart stats
console.log("Shopping cart data");
console.log(`Min price: ${(cartMin as Deno.KvEntry<bigint>).value}`);
console.log(`Max price: ${(cartMax as Deno.KvEntry<bigint>).value}`);
console.log(`Total price: ${(cartSum as Deno.KvEntry<bigint>).value}`);

mutate() 方法

该方法 mutate() 是获取聚合统计信息的另一种方法。它接受一个带有 和 key value 属性的对象 type 。类型值可以是“总和”、“最小值”或“kax”。

// Assumes kv is a Deno.Kv object
// keep track of website visits
await kv.atomic()
  .mutate({
    type: "sum", // valid types are 'sum', 'min' & 'max'
    key: ["hit_counter"],
    value: new Deno.KvU64(1n),
  })
  .commit();

Deno.KvU64() 构造函数是无符号 bigint 值的包装器。该值通过构造函数参数设置,并使用 Deno.KvU64() 实例上的 value 字段进行检索。值参数 mutate() 始终为 Deno.KvU64 .

使用 KV 队列

在Deno v1.34.3中,Deno KV中添加了一个队列。如果您还记得您的数据结构类(如果您像我一样拿了一个或即时拿起它),队列是一个线性序列,其中操作按先进先出 (FIFO) 顺序执行。排队是将项目添加到队列的操作,取消排队是从队列中删除项目的操作。

💡 演示队列的工作代码可以在此博客关联的存储库中找到。

KV 队列实现是使用 和 Deno.Kv.enqueue() Deno.Kv.queueListen() 方法完成的。该方法 enqueue() 将项目添加到数据库队列,而 queueListen() 当项目取消排队时,将调用该方法的回调函数参数。

除了作为独立方法调用外, enqueue() 还可以链接到原子事务 Deno.Kv.atomic() 并成为原子事务的一部分。

有两个 Deno.Kv.enqueue() 参数。第一个是要排队的值,这是一个有效的 KV 值。它返回一个 Promise<Deno.KvCommitResult> 用布尔 ok 值解析的。

第二个 enqueue() 参数是可选的。它是一个包含两个属性(它们都是可选的)的对象:

  • delay - 延迟排队对象的传递的时间(以毫秒为单位)。默认值为零。
  • keysIfUndelivered - 一个键数组 Deno.Kv.Key[] ,用于在多次重试后向队列侦听器传递值不成功时存储值。

该方法 Deno.Kv.queueListen() 有一个参数,它是一个回调处理程序函数。它需要一个参数,该参数是队列值。

// Assumes kv is a Deno.Kv object
// Listen to enqueued objects
kv.listenQueue(async (msg: unknown) => {
  // Do a operation on the queued object
  await kv.set(msg.key, msg.value);
  console.log("Value delivered: ", msg);
});

const res = await kv.enqueue(
  { key: ["test1"], value: "testing 1,2,3" },
  {
    delay: 1000, // delay delivery for 1 second
    keysIfUndelivered: [["queue_failed", "test1`"]],
  },
);
console.log("Queue result: ", res);

// Output:
// Queue result:  { ok: true, versionstamp: "0000000000001d100000" }
// Value delivered:  { key: [ "test1" ], value: "testing 1,2,3" }

atomic() 该方法一样,该方法 enqueue() 返回包含 okversionstamp 字段的结果。如果排队失败, ok 将为 false ;否则 true .

KV on Deno Deploy

Deno 运行时有一个使用 sqlite 实现持久性的 Deno KV。此实现与 Deno Deploy KV 数据库(基于 Foundation DB)兼容,因此本地开发的代码在部署到 DD 时将无缝工作。

如果您在 Deno 部署测试期间无法访问 KV,您可以在此处请求它。

德诺部署数据中心

目前,Deno KV 数据库至少跨 6 个数据中心进行复制,跨越 3 个区域(美国、欧洲和亚洲)。

数据一致性

数据一致性是指保证所有数据中心即使在保留新数据后也保持相同的数据。

Deno Deploy 中有两种类型的数据一致性。当从Deno KV读取数据时,可以使用该 consistency 选项配置它们。有两种选择:

  • consistency: "strong" (默认值)- 从 KV 读取的数据将来自最近的区域。强一致性意味着:

    • 可序列化性:意味着事务在最高级别被隔离。它确保多个事务的并发执行会导致系统状态与按顺序执行事务相同。
    • 线性化:保证读写操作似乎是即时的,并且是实时发生的。线性化性确保了强大的实时操作顺序。
  • consistency: "eventual" - 使用全局副本和缓存进行数据读取和写入,以最大限度地提高性能。在大多数情况下,强一致性速度和最终一致性速度之间的差异范围从零到大约 100 毫秒,这取决于部署的应用区域与美国北弗吉尼亚州的距离,北弗吉尼亚州拥有 KV 的主写入数据库(此时)。

有关更多信息,请参阅 Deno 部署文档的 Deno KV 一致性部分。

数据访问速度更快,最终一致性更高,但如果查询在一个或多个 KV 写入后不久完成,则不同复制的 KV 实例之间的数据可能不一致。

有关更多详细信息,请参阅 https://deno.com/blog/kv#consistency-and-performance

数据中心之间的同步

将数据写入 Deno KV 存储时,会发生以下情况:

  1. 数据同步复制到同一 Deno 部署区域中的两个数据中心。
  2. 数据以异步方式复制到其他数据中心。

Deno Deploy 文档指出,数据的完全异步复制应在 10 秒内完成。

将 KV 数据加载到 Deno 部署中

在Deno Deploy上使用KV的一个问题是如何在启动应用程序之前为数据库设定种子。团队建议您创建 API 路由来处理数据加载,并从使用命令行调用发送数据的本地命令行应用程序调用 API 路由。

大型数据集的加载应使用对 API 的批量调用来完成,以避免系统过载并最大限度地减少服务器 CPU 使用率。在任何情况下,都应确保 API 在持久性成功时返回 OK (202) 响应,或者返回失败响应 (500),枚举哪些记录未持久保存到 KV 中。

少量数据的替代方法是在应用程序启动时加载一次数据。这可以在带有空数组参数的 useEffect 钩子中完成,该参数将在初始加载完成时设置带有 true 值的“is_loaded”索引,并在加载前检查该值以确保它们不会多次加载。

结论

Deno KV还没有完成,所以你应该期待它的发展。以下是一些期望:

  • KV API 的稳定性
  • 更多KV功能和抽象构建在它之上。
  • 成熟的工具,可帮助将其用作数据库以及查看和编辑 KV 数据存储。

这些项目是来自Deno团队还是外部贡献者还有待观察。

由于技术还很年轻,使用KV还是有些犹豫的。阻碍人们使用它的另一个问题是,目前唯一的部署选项是Deno Deploy。KV定价尚未确定,价格是否基于Deno Deploy存储和/或吞吐量。目前,它可以在本地和部署中免费使用。

KV的心智模型与关系数据库不同,这对某些人来说也是一个缺点。KV没有像SQL这样的工具可用于轻松的持久性和查询。

尽管如此,KV在Deno社区中引起了很多兴趣,并且有许多应用程序示例和工具正在开发中。

应用程序内容数据并不是唯一可以存储在 KV 中的内容。其他示例包括配置数据、逻辑标志和系统范围的属性。它的使用只是开发人员的想象力问题。

如果本文激起您对Deno KV的兴趣,请务必查看Deno手册和API文档,如下所述。了解Deno KV开发和应用的最新新闻的最佳地点是Deno Discord实例上的kv频道。

最后,查看与这篇文章相关的代码,了解本文中详细介绍的内容的简单命令行示例。

附录

使用 Deno KV 的应用

  • SaasKit - 使用Fresh,OAuth,Stripe和Deno KV创建SAAS应用程序的样板。
  • kv-sketchbook - 一个使用Deno Fresh和KV 非常简单的素描本应用程序。
  • tic-tac-toe - 井字游戏 - 使用Deno Fresh和KV构建的经典游戏。
  • kv-notepad - 使用Deno Fresh和KV构建的经典记事本应用程序的多版本。
  • PixelPage - 使用Deno Fresh和KV构建的共享像素艺术画布。
  • Todo List - 使用Deno Fresh和KV构建协作待办事项列表。
  • fresh-kv-demo - 一个使用 Fresh 和 Deno KV 进行持久性的样板文件。
  • Multiplayer KV Beats - 使用 Deno KV 的多人节拍盒机器。
  • Reddino - 使用Deno KV的Reddit克隆
  • Stone, Bone, Cone - Deno KV驱动的石头,纸,剪刀游戏
  • Deno KV Hackathon submissions - 最近创建使用 Deno KV 的应用程序和库的黑客马拉松的最终参赛作品。

正在开发中KV工具

Deno KV还处于起步阶段,因此没有成熟的工具来使用它。以下是一些与KV一起使用的有前途的实用程序的列表: