在 PlanetScale,我们打造了 PlanetScaleDB,这是一个基于开源工具 Vitess 的完全托管数据库即服务平台,它使 MySQL 的水平扩展超越单实例的能力。在这篇博客中,我们将解释 Vitess 和 PlanetScaleDB 中的分片工作原理。

一个分片数据库是由拥有相同关系型架构的多个数据库(分片)组成的集合。Vitess 允许你的应用程序将分片数据库视为一个庞大的单体数据库,而不必担心分片的复杂性。正因如此,你可以在 PlanetScaleDB 上以一个小型数据库开始,并扩展至庞大的规模,而无需更改应用逻辑。

在这篇博客文章中,我们将通过一个名为“Goodest Doggo”的样例狗狗评分应用程序探索 Vitess 的分片概念,包括 VSchema(分片架构)、Vindex(分片索引)以及 Vitess 序列。这个样例应用允许用户为小狗评分,并需要设计为支持大规模扩展。


Vitess 的分片有何不同?

分片数据库是由多个拥有相同关系型架构的数据库组成的集合。许多数据库系统通过分片对表数据进行扩展时,通常忽略了将关联数据共同存储的考虑。这会导致在事务一致性写入和数据读取方面效率低下。而 Vitess 则允许共同存储相关数据。

假设你有一个 users 表,其中包含 id 列作为主键,还有一个 orders 表,其自身的 id 列是主键,同时具备 user_id 列作为辅助键。Vitess 允许你基于 id 列对用户表进行分片,同时基于 user_id 列对订单表进行分片。这确保了特定用户的用户行和订单行位于同一个分片中。一个 VSchema(分片架构)允许你表达这一信息。

换句话说,就像关系型架构用表、列和索引来定义单一数据库内数据的组织方式,Vitess 使用 VSchema 来定义跨多个数据库中分片数据的组织方式。这使我们能够通过高效的机制访问这些分片上的数据。


分片架构的元素有哪些?

就像使用 SQL 语句定义关系型数据库架构一样(通过创建表定义来设置列和索引),你可以通过填写包含以下信息的 JSON 文档来定义 VSchema(或 PlanetScaleDB 中所称的分片方案):

  1. “sharded”:数据库是否分片
  2. “vindexes”:定义 VSchema 所使用的所有分片索引类型
  3. “tables”:关于每个表的条目,其中每个表条目包含以下信息:
    • 基于表中某一列的主分片索引
    • (可选)辅助分片索引
    • (可选)用于生成 id 的序列定义

示例如下,一个名为 “puppers” 的表表示我们的应用中的狗狗数据:

CREATE TABLE IF NOT EXISTS puppers (
 `id` BIGINT(22),
 `name` VARCHAR(256),
 `image` VARCHAR(256),
 PRIMARY KEY(id)
);

以下是针对上述表的一个简单 VSchema,用于分片数据库,它展示了分片架构的三个元素:

{
 "sharded": true,
 "vindexes": {
   "hash_vdx": {
     "type": "hash"
  }
},
 "tables": {
   "puppers": {
     "column_Vindexes": [
      {
         "column": "id",
         "name": "hash_vdx"
      }
    ]
  }
}
}

什么是分片索引(Vindex)?

关系型数据库架构包含表、列以及索引等元素。在单一数据库中,索引提高了访问某表中特定行的效率。同样,在分片化数据库中,分片索引(Vindex)允许你快速访问某一行,通过指定方法确定该行所在的分片。

分片索引有主索引和辅助索引两类。一个表只能有一个主分片索引,但可以有多个辅助分片索引。


分片索引如何将行映射到分片?

为了理解这一点,需要掌握关键空间(keyspaces)和关键空间 ID(keyspace_id)的概念。在 Vitess 中,每个分片数据库中的行都有一个 keyspace_id,但这个 keyspace_id 并未存储,而是通过对该行某列的值应用特定分片函数计算得出。

keyspace_id 的范围从 0x00 到 0xFF,这些范围代表整个关键空间。Vitess 分片覆盖这个范围。这也是为什么在 Vitess 中,分片数据库被称为关键空间(keyspace)。每个分片覆盖一个关键空间范围,并根据起始和结束 keyspace_id 命名。例如,在一个包含四个分片的关键空间中,范围划分如下:

10x00-0x40
20x40-0x80
30x80-0xC0
40xC0-0xFF

以下内容继续解释如何定义序列、组织数据以及运行示例应用程序。

什么是 Vitess 序列?

在非分片数据库中,当你需要为某一行分配单调递增的值时,可以定义一个 “自增(autoincrement)” 类型的列。而在分片的场景中,如果直接使用自增类型,你可能会在不同分片中生成重复的值。

Vitess 通过在 VSchema 中定义序列来解决这个问题,从而能够为某个表生成跨分片唯一且单调递增的 ID。Vitess 序列的底层实现依赖一个表中的行,该表存在于次数据库(secondary database)中。Vitess 通过允许缓存一定数量的值来降低写入操作的次数。

让我们通过在上述 puppers 表中使 id 列类型设置为 Vitess 序列来说明操作,这可以通过在 VSchema 中添加以下片段实现:

"auto_increment": {
"column": "id",
"sequence": "pupper_seq"
}

这样可以获得如下结构的分片架构:

{
 "sharded": true,
 "vindexes": {
   "hash": {
     "type": "hash"
  }
},
 "tables": {
   "puppers": {
     "column_Vindexes": [
      {
         "column": "id",
         "name": "hash"
      }
    ],
     "auto_increment": {
       "column": "id",
       "sequence": "pupper_seq"
    }
  }
}
}

如何定义序列?

你可能注意到我们使用了 pupper_seq 名称,但并未定义它。这是如何运作的呢?

序列通常定义在一个非分片的关键空间,它与主要关键空间一起存在。在 Vitess 中,这个关键空间通常被称为 “查找关键空间(lookup keyspace)”。以下是定义查找关键空间中的序列的方法:

首先,创建一个名为 pupper_seq 的表,并在其中插入一行以初始化序列。注意 CREATE TABLE 语句中的注释 vitess_sequence。注释非常重要,因为它被 Vitess 用来将该表与其他存储真实数据的表区分开。

CREATE TABLE IF NOT EXISTS pupper_seq (
`id` INT,
`next_id` BIGINT,
`cache` BIGINT,
PRIMARY KEY (id)
) comment 'vitess_sequence';

INSERT INTO pupper_seq (id, next_id, cache) VALUES (0, 1, 3);

接着,将以下 VSchema 应用于查找关键空间:

{
"sharded": false,
"tables": {
  "pupper_seq": {
    "type": "sequence"
  }
}
}

通过上述两个步骤,可以将 pupper_seq 定义为 Vitess 序列,并在分片关键空间的 VSchema 中使用。


整合所有内容

最后,我们将为 Goodest Doggo 应用程序设计数据库架构。我们将创建两个关键空间:一个名为 lookup,未分片;另一个名为 puppers,分片。我们将从两个分片开始,并根据需要进行分片扩展。

数据组织方式

  • lookup 关键空间包含序列所需的表以及查找分片索引所需的表。
  • puppers 关键空间包含实际数据表。

以下是这两个关键空间的数据库架构和 VSchema:

puppers 关键空间

puppers 关键空间中有三张表:puppersratingsusers

  • 我们希望 id 列具有自增类型。
  • 因为这是分片数据库,我们在架构中不直接指定自增类型,而在 VSchema 中定义基于序列的 id 列。
  • 我们对 puppers 表基于 id 列进行分片,而对 ratings 表基于 user_id 列分片。

以下是 puppers 数据库的架构和 VSchema:

CREATE TABLE `puppers` (
 `id` BIGINT(22) NOT NULL,
 `name` VARCHAR(256) DEFAULT NULL,
 `image` VARCHAR(256) DEFAULT NULL,
 PRIMARY KEY (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8;

CREATE TABLE `ratings` (
 `id` BIGINT(22) DEFAULT NULL,
 `user_id` BIGINT(22) DEFAULT NULL,
 `rating` BIGINT(20) DEFAULT NULL,
 `pupper_id` BIGINT(22) DEFAULT NULL,
 KEY `pupper_id` (`pupper_id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8;

CREATE TABLE `users` (
 `id` BIGINT(22) NOT NULL,
 `email` VARCHAR(64) DEFAULT NULL,
 `password` VARBINARY(256) DEFAULT NULL,
 PRIMARY KEY (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8;

对应的 VSchema:

{
 "sharded": true,
 "vindexes": {
   "binary_md5_vdx": { "type": "binary_md5" },
   "hash_vdx": { "type": "hash" }
},
 "tables": {
   "puppers": {
     "columnVindexes": [{ "column": "id", "name": "hash_vdx" }],
     "autoIncrement": { "column": "id", "sequence": "pupper_seq" }
  },
   "ratings": {
     "columnVindexes": [{ "column": "pupper_id", "name": "hash_vdx" }],
     "autoIncrement": { "column": "id", "sequence": "rating_seq" }
  },
   "users": {
     "columnVindexes": [{ "column": "id", "name": "binary_md5_vdx" }]
  }
}
}

lookup 关键空间

以下是查找数据库的架构:

CREATE TABLE IF NOT EXISTS pupper_seq (
id INT,
next_id BIGINT,
 cache BIGINT,
 PRIMARY KEY (id)
) comment 'vitess_sequence';

INSERT INTO pupper_seq (id, next_id, cache) VALUES (0, 1, 3);

CREATE TABLE IF NOT EXISTS rating_seq (
id INT,
next_id BIGINT,
 cache BIGINT,
 PRIMARY KEY (id)
) comment 'vitess_sequence';

INSERT INTO rating_seq (id, next_id, cache) VALUES (0, 1, 3);

对应的 VSchema:

{
 "sharded": false,
 "tables": {
   "pupper_seq": {
     "type": "sequence"
  },
   "rating_seq": {
     "type": "sequence"
  }
}
}

想要亲自体验?

我们创建了一个 PlanetScaleDB 快速入门 Demo,它会为上述的狗狗评分应用程序创建数据库并填充数据。

操作步骤:

  1. 创建 PlanetScaleDB 账号
  2. 选择快速入门 Demo VSchema。
  3. 这将启动一个集群和两个数据库,完成后会提供一个数据库 URL。
  4. 在此期间,下载并启动应用程序。
  5. 启动应用程序后,它会提供一个 URL(如 http://localhost:8000),可从本地浏览器访问。
  6. 应用程序会提示输入快速入门 Demo 生成的集群连接字符串。
  7. 将连接字符串输入到应用程序中,开始评分狗狗。
  8. 点击 “Show Data” 标签查看数据如何分布在分片中。
  9. 还可以通过 MySQL 客户端连接到数据库并运行查询,观察数据分布。


使用分片MySQL 数据库的 Rust 应用程序中进行狗狗评分插图

关注公众号:程序新视界,一个让你软实力、硬技术同步提升的平台

除非注明,否则均为程序新视界原创文章,转载必须以链接形式标明本文链接

本文链接:http://www.choupangxia.com/2025/05/17/mysql-rust/