redant/doctrine-dbal-sharding

Doctrine DBAL 3.x 的分片支持

dev-main 2022-02-04 08:45 UTC

This package is auto-updated.

Last update: 2024-09-04 14:08:55 UTC


README

Doctrine DBAL 是一个功能强大的数据库抽象层,具有许多特性。在其 3.0 版本中,移除了分片支持。此包提供了略微修改过的 DBAL 2.x 分片代码,用于与 DBAL 3.x 一起使用。

升级

  • 将命名空间从 Doctrine\DBAL\Sharding 更改为 RedAnt\DBALSharding
  • 配置键 shardChoser 和所有相关类都被重命名为 shardChooser

分片

此包包含一些功能,可简化水平分片应用程序的开发。在这个版本中,它包含一个 ShardManager 接口。此接口允许程序选择要发送查询的分区。目前还没有根据 ID、查询或数据库行动态选择分区的功能。这意味着分片扩展主要适用于

  • 多租户应用程序或
  • 具有完全分离的数据集的应用程序(例如:天气数据)。

这两种类型的应用程序都将与 DBAL 和 ORM 一起工作。

Horizontal sharding is an evasive architecture that will affect your application code and using this
extension to Doctrine will not make it work "magically".

您必须理解和整合以下缺点

  • 需要预先生成跨所有分片都是唯一的 ID。
  • 不支持跨分片的事务。
  • 不支持跨分片的外键(这意味着没有“真实”的关系)。
  • 跨分片查询聚合数据非常复杂(或不可能)。
  • 反规范化:复合键是必需的,而规范化的非分片数据库模式不需要这些键。
  • 必须在所有分片上执行模式操作。

分片架构中的主要问题是

  • 我的数据在哪里?
  • 我应该将新数据保存到哪里才能以后找到它?

为了回答这些问题,通常需要创建一个函数,该函数将告诉您给定 ID 的数据位于哪个分片上。为了简化这种方法,通常只需选择一个作为相关数据集合根的表,并决定该表的 IDs。所有属于此表的相关数据都保存在同一个分片上。

以一个多用户博客应用程序为例,该应用程序具有以下表

  • 博客 [id, name]
  • 帖子 [id, blog_id, subject, body, author_id]
  • 评论 [id, post_id, comment, author_id]
  • 用户 [id, username]

一个合理的分片架构将通过博客来分割应用程序。这意味着特定博客的所有数据都将位于单个分片上,而扩展是通过将博客的数量放在许多不同的数据库服务器上来实现的。

现在用户可以在不同的分片上发布和评论不同的博客。这使得上面的数据库模式略微复杂,因为两个 author_id 列不能将外键指向 User (id)。相反,用户表位于应用程序中完全不同的“维度”中,从分片架构的角度来看。

为了简化处理这种类型的多维数据库模式,您可以将作者 IDs 替换为更“有意义”的东西,例如如果始终知道用户的电子邮件地址,则可以使用用户的电子邮件地址。然后,“用户”表可以与上面的数据库模式分开,并放置在第二个水平扩展的分片架构中。

如您所见,即使只有上面提到的四个表,分片实际上也变得相当复杂。

本节其余部分将详细讨论 Doctrine 分片功能。

ID 生成

为了解决跨所有分片唯一 ID 生成的问题,您应该评估以下几种方法

使用 GUID/UUIDs

最简单的分片ID生成机制是通用唯一标识符(UUID)。这些是16字节(128位)的数字,保证在不同服务器之间唯一。您可以在维基百科上了解更多关于UUID的信息:通用唯一标识符

UUID的缺点是它们在索引上造成的分割。因为UUID不是按顺序生成的,它们可能会对索引访问性能产生负面影响。此外,它们比数值主键(通常是4字节长度)要大得多。

目前,Doctrine DBAL驱动程序MySQL和SQL Server支持UUID/GUID的生成。您可以使用以下代码在不同平台上生成它们:

<?php
use Doctrine\DBAL\DriverManager;
use Ramsey\Uuid\Uuid;

$conn = DriverManager::getConnection(/**..**/);
$guid = Uuid::uuid1();

$conn->insert('my_table', [
    'id'  => $guid->toString(),
    'foo' => 'bar',
]);

在您的应用程序中,应该将这些细节隐藏在ID生成服务中。

<?php
namespace MyApplication;

use Ramsey\Uuid\Uuid;

class IdGenerationService
{
    public function generateCustomerId() : Uuid
    {
        return Uuid::uuid1();
    }
}

了解GUID(与数值ID相比)的一个好的起点是这篇博客文章:Coding Horror: 主键:ID与GUID

表生成器

在某些情况下,没有其他方法可以绕过数值、自动增长的ID。然而,MySQL和SQL Server中实现的自增ID完全不适合分片。记住,在分片架构中,您必须知道特定ID的行位于何处,并且ID必须在所有服务器上全局唯一。自增主键缺少这两个属性。

为了解决这个问题,您可以使用所谓的“表生成器”策略。在这种情况下,您定义一个单一代理数据库,负责自动增长ID的生成。您在这个数据库上创建一个表,并通过使用锁定来创建新的顺序ID。

这种策略有三个重要的缺点:

  • 单点故障
  • 当应用程序写负载高时会出现瓶颈
  • 需要第二个独立数据库连接来保证事务安全性。

如果您可以接受这些缺点,则可以使用以下代码在Doctrine中使用表生成器:

<?php
use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\Id\TableGenerator;

$conn = DriverManager::getConnection(/**..**/); // connection 1

// creating the TableGenerator automatically opens a second connection.
$tableGenerator = new TableGenerator($conn, "sequences_tbl_name");

$id1 = $tableGenerator->nextValue("sequence_name1");
$id2 = $tableGenerator->nextValue("sequence_name2");

表生成器显然需要一个表来工作。该表的架构在TableGenerator类文档块中描述。或者,您可以使用Doctrine\DBAL\Id\TableGeneratorSchemaVisitor并将其应用于您的Doctrine\DBAL\Schema\Schema实例。它将自动添加所需的序列表。

自然标识符

有时您很幸运,应用程序的数据模型自带自然ID。这通常适用于从其他地方(外源ID生成)生成ID的应用程序,或者处理时间序列数据的应用程序。在这种情况下,您只需定义自然主键,并根据此数据对应用程序进行分片。

事务

在分片中的事务只能用于位于单个分片上的数据。如果您需要在分片架构中进行事务,那么您必须确保事务期间更新的数据位于单个分片上。

外键

由于您无法在远程数据库服务器之间创建外键,在分片架构中,您应该将数据放在属于彼此的分片上。即使您可以隔离大多数行在单个分片上,也可能存在位于不同分片上的表之间的关系。在这种情况下,您的应用程序应该意识到潜在的不一致性,并妥善处理。

复杂查询

GROUP BY、DISTINCT和ORDER BY是难以在分片架构中使用的子句。如果您必须对多个分片执行这些查询,则不能简单地将不同结果附加在一起。

您必须意识到这个问题,并相应地设计查询,或者以这种方式分片数据,以至于您永远不需要查询多个分片来计算结果。

ShardManager接口

分片扩展的核心API是ShardManager接口。它包含两组不同的与分片相关的功能。

首先,它包含分片选择API。您可以根据所谓的“分布值”选择分片,或将连接重置到“全局”分片,全局分片是一个必要的数据库,通常包含大量缓存的、与分片无关的数据,例如元表或“用户/租户”表。

<?php
use Doctrine\DBAL\DriverManager;
use Doctrine\Shards\DBAL\SQLAzure\SQLAzureShardManager;

$conn = DriverManager::getConnection(array(
    'sharding' => array(
        'federationName' => 'my_database',
        'distributionKey' => 'customer_id',
    )
));
$shardManager = new SQLAzureShardManager($conn);

$currentCustomerId = 1234;
$shardManager->selectShard($currentCustomerId);
// all queries after this call hit the shard
// where customer with id 1234 is on.

$shardManager->selectGlobal();
// the global database is selected.

要访问当前选定的分布值,请使用以下API方法

<?php
$value = $shardManager->getCurrentDistributionValue();

当事务打开时,分片管理器将阻止您切换分片。当使用ORM进行分片时,这一点尤为重要。因为ORM在flush操作期间使用单个事务,这意味着您只能使用一个来自单个分片的数据EntityManager

第二个API是“fan-out”查询API。这允许您对所有分片执行查询。此操作的结果顺序是未定义的,这意味着您的查询必须以适用于应用程序的方式返回数据,或者您必须在应用程序中对数据进行排序。

<?php
$sql = "SELECT * FROM customers";
$rows = $shardManager->queryAll($sql, $params);

模式操作:SchemaSynchronizer接口(已弃用)

在分片架构中进行模式操作很棘手。您必须同时在所有数据库实例(分片)上执行这些操作。此外,Doctrine在这一点上尤其有问题,因为在任何开发机器上都无法再生成更改的SQL文件并在生产环境中应用它。所需更改取决于分片数量。

为了允许在分片架构上使用Doctrine Schema API操作,我们对ORM中的代码进行了重构,从Doctrine\ORM\Tools\SchemaTool类内部提取了操作Schema实例的代码,并将其提取到新的Doctrine\Shards\DBAL\SchemaSynchronizer接口中。

每个分片实现都可以实现此接口,并允许模式操作在多个分片上参与。

通用SQL分片支持

这个通用的实现与所有数据库驱动程序一起工作,需要您指定所有数据库连接,当使用ShardManager API时,它会在不同的连接之间切换。这也是这种方法的最大缺点,因为fan-out查询需要在单个请求中连接到所有数据库。

查看配置以获取分片连接示例

<?php
use Doctrine\DBAL\DriverManager;

$conn = DriverManager::getConnection(array(
    'wrapperClass' => 'RedAnt\DBALSharding\PoolingShardConnection',
    'driver'       => 'pdo_sqlite',
    'global'       => array('memory' => true),
    'shards'       => array(
        array('id' => 1, 'memory' => true),
        array('id' => 2, 'memory' => true),
    ),
    'shardChooser' => 'RedAnt\DBALSharding\ShardChooser\MultiTenantShardChooser',
));

您必须配置以下选项

  • 'wrapperClass' - 选择如上所示的PoolingShardConnection。
  • 'global' - 用于连接全局数据库的数据库参数数组。
  • 'shards' - 分片数据库参数数组。您必须为每个分片配置指定一个'id'参数。
  • 'shardChooser' - 实现RedAnt\DBALSharding\ShardChooser\ShardChooser接口。

ShardChooser接口将分布值映射到分片-id。这为您提供了实施自己的水平数据分片策略的自由。