跳到主要内容

二级索引

Deno KV 目前处于测试阶段

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

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

deno run -A --unstable my_kv_code.ts

像 Deno KV 这样的键值存储将数据组织成键值对的集合,其中每个唯一键都与一个单一值相关联。这种结构使根据其键轻松检索值,但不允许基于值本身进行查询。为了克服这一限制,您可以创建二级索引,它们在附加键下存储相同的值,包括该值的一部分。

在使用二级索引时,保持主键和二级键之间的一致性至关重要。如果在主键上更新值而不更新二级键,从针对二级键的查询返回的数据将不正确。为确保主键和二级键始终代表相同的数据,请在插入、更新或删除数据时使用原子操作。这种方法确保一组变异操作作为单个单元执行,要么全部成功,要么全部失败,以防止不一致性。

唯一索引(一对一)

唯一索引使索引中的每个键与正好一个主键相关联。例如,当存储用户数据并按其唯一 ID 和电子邮件地址查找用户时,将用户数据存储在两个单独的键下:一个用于主键(用户 ID),另一个用于二级索引(电子邮件)。这种设置允许根据其 ID 或电子邮件查询用户。二级索引还可以对存储中的值强制执行唯一性约束。在用户数据的情况下,使用索引来确保每个电子邮件地址只与一个用户相关联 - 换句话说,电子邮件是唯一的。

要为此示例实现唯一二级索引,请执行以下步骤:

  1. 创建代表数据的 User 接口:

    interface User {
    id: string;
    name: string;
    email: string;
    }
  2. 定义一个 insertUser 函数,将用户数据存储在主键和二级键下:

    async function insertUser(user: User) {
    const primaryKey = ["users", user.id];
    const byEmailKey = ["users_by_email", user.email];
    const res = await kv
    .atomic()
    .check({ key: primaryKey, versionstamp: null })
    .check({ key: byEmailKey, versionstamp: null })
    .set(primaryKey, user)
    .set(byEmailKey, user)
    .commit();
    if (!res.ok) {
    throw new TypeError("具有相同 ID 或电子邮件的用户已存在");
    }
    }

    此函数使用原子操作执行插入,检查是否已存在具有相同 ID 或电子邮件的用户。如果违反了这些约束之一,插入将失败,不会修改任何数据。

  3. 定义一个 getUser 函数,按其 ID 检索用户:

    async function getUser(id: string): Promise<User | null> {
    const res = await kv.get<User>(["users", id]);
    return res.value;
    }
  4. 定义一个 getUserByEmail 函数,按其电子邮件地址检索用户:

    async function getUserByEmail(email: string): Promise<User | null> {
    const res = await kv.get<User>(["users_by_email", email]);
    return res.value;
    }

    此函数使用二级键(["users_by_email", email])查询存储。

  5. 定义一个删除用户的函数,按其 ID 删除用户:

    async function deleteUser(id: string) {
    let res = { ok: false };
    while (!res.ok) {
    const getRes = await kv.get<User>(["users", id]);
    if (getRes.value === null) return;
    res = await kv
    .atomic()
    .check(getRes)
    .delete(["users", id])
    .delete(["users_by_email", getRes.value.email])
    .commit();
    }
    }

    此函数首先按其 ID 检索用户以获取用户的电子邮件地址。这是为了检索构建用于此用户地址的二级索引键所需的电子邮件。然后,它执行原子操作,检查数据库中的用户是否已更改,然后删除指向用户值的主键和二级键。如果此操作失败(用户在查询和删除之间已被修改),则原子操作中止。整个过程将重试,直到删除成功。

    检查是为了防止在检索和删除之间发生值可能已经被修改的竞争情况。如果更新更改了用户的电子邮件,因为在这种情况下二级索引移动,所以二级索引的删除失败。删除针对旧的二级索引键,因为删除针对旧的二级索引键的原子操作中止。

非唯一索引(一对多)

非唯一索引是二级索引,其中一个键可以与多个主键相关联,允许您根据共享属性查询多个项。例如,按其最喜欢的颜色查询用户时,使用非唯一二级索引实现。最喜欢的颜色是一个非唯一属性,因为多个用户可以有相同的最喜欢的颜色。

要为此示例实现非唯一二级索引,请执行以下步骤:

  1. 定义 User 接口:

    interface User {
    id: string;
    name: string;
    favoriteColor: string;
    }
  2. 定义 insertUser 函数:

    async function insertUser(user: User) {
    const primaryKey = ["users", user.id];
    const byColorKey = ["users_by_favorite_color", user.favoriteColor, user.id];
    await kv.atomic()
    .check({ key: primaryKey, versionstamp: null })
    .set(primaryKey, user

) .set(byColorKey, user) .commit(); }


3. 定义一个函数,按其最喜欢的颜色检索用户:

```ts
async function getUsersByFavoriteColor(color: string): Promise<User[]> {
const iter = kv.list<User>({ prefix: ["users_by_favorite_color", color] });
const users = [];
for await (const { value } of iter) {
users.push(value);
}
return users;
}

此示例演示了非唯一二级索引 users_by_favorite_color 的使用,它允许根据用户的最喜欢的颜色查询用户。主键仍然是用户的 id

唯一索引和非唯一索引的实现主要区别在于二级键的结构和组织。在唯一索引中,每个二级键与正好一个主键相关联,确保索引的属性在所有记录中都是唯一的。在非唯一索引的情况下,一个二级键可以与多个主键相关联,因为索引的属性可能在多个记录之间共享。为了实现这一点,非唯一二级键通常以键的一部分具有附加的唯一标识符(例如,主键),允许具有相同属性的多个记录共存而不发生冲突。