Lucinda DB:纯PHP基于标签的键值存储

v3.0.1 2022-06-18 08:23 UTC

This package is auto-updated.

Last update: 2024-09-18 13:09:44 UTC


README

目录

关于

Lucinda DB是一款无服务器KEY-VALUE存储,最初设计用于帮助开发人员缓存基于特定标准(即标签)查询结果的资源密集型SQL查询。它与其他KV存储不同,因为它可以根据依赖的标签查询结果自动生成KEYs,并将VALUEs作为以KEY命名的单独JSON文件(而不是RAM)保存到一个或多个模式中。这带来了以下优势

  • 无服务器工作能力:主机机器上的操作系统,已经优化用于操作文件,成为服务器
  • 平台无关性:可以在任何操作系统上用任何编程语言实现的数据库规范
  • 键标准化:键的值是根据依赖的标签的值按可预测的规则生成的
  • 无条目重复:标签的组合总是唯一的
  • 易于维护:可以通过标签查询条目,这在仅依赖于以累积键索引的RAM散列表的标准KV存储中是不可能实现的
  • 可移植性:要传输/备份数据库,只需复制模式文件夹即可
  • 可伸缩性:数据库可以在多个磁盘上实时分布

API依赖于以下概念之间的相互作用

  • 数据:这是要缓存的值的DATA(例如:SQL查询或查询组合的结果)
  • 标签:根据DATA生成标准(例如:“用户”和“角色”)的准则
  • :KV存储中的键,其名称基于依赖的标签自动生成(例如:“roles_users”)
  • :KV存储中的值,以JSON格式化的DATA保存,作为单独的文件存储在以KEY命名的模式文件夹中
  • 模式:存储KV条目的文件夹/磁盘

数据

这是作为KV存储中的VALUE缓存的值的DATA,可转换为JSON。

示例查询

SELECT t1.name AS user, t3.name AS role
FROM users AS t1
INNER JOIN users__roles AS t2 ON t1.user_id = t2.user_id
INNER JOIN roles AS t3 ON t2.role_id = t3.id
WHERE t1.active = 1

处理成以下PHP数组结构

$data = ["John Doe"=>["Administrator"], "Jane Doe"=>["Assistant Manager", "Team Leader"]];

标签

标签对应于根据其生成DATA的准则的名称(例如:上面的“用户”和“角色”)。标签的值必须遵守以下要求

  • 必须是小写
  • 只能包含a-z0-9字符
  • 允许使用“-”作为多词名称的分隔符

键是名为组合(例如:“roles_users”上面所示)的TAG所依赖的KV存储中DATA的唯一标识符。为了便于维护,无论调用者如何排序,每个有限组合的TAG都会对应一个唯一的KEY!根据谁计算键名而定的规则如下:

  • 检查TAG是否遵循上述规范
  • 按字母顺序排序TAG
  • 使用_符号连接所有TAG

键的创建被封装在Lucinda\DB\Key类中。用法示例

$object = new Lucinda\DB\Key(["users", "roles"]);
$key = $object->getValue(); // key will be "roles_users"

值是DATA的JSON表示形式,以JSON格式保存在名为KEY的文件中,存储在SCHEMA文件夹中,遵循以下规则

  • DATA必须可被JSON编码

值操作被封装在Lucinda\DB\Value类中。用法示例

$key = new Lucinda\DB\Key(["users", "roles"]); // initializes KEY
$value = new Lucinda\DB\Value("/usr/local/share/db", $key->getValue()); // instances VALUE by KEY and SCHEMA
$value->set($data); // saves DATA by KEY, creating a KEY.json file within SCHEMA

负载均衡

出于性能、一致性和可扩展性等原因,用户可以选择将VALUE在多个SCHEMA之间进行负载均衡。这是通过使用而不是强化的,它封装了以确保

  • 所有写入(设置/删除)均匀地分布在整个副本中
  • 所有读取都从随机副本进行
  • 所有竞争条件操作(增加/减少)将在第一个副本中完成,然后分布到所有其他副本

用法示例

$key = new Lucinda\DB\ValueDriver(["schema1", "schema2"], ["users", "roles"]); //instances VALUE by KEY and SCHEMAs
$value->set($data); // saves DATA by KEY, creating a KEY.json file within all SCHEMAs

模式

模式仅仅是保存VALUE的文件夹。模式可以是一个文件夹,也可以是一个副本数组,这些副本可以位于同一服务器的不同磁盘上,甚至是不同服务器上(例如:通过符号链接)。类Lucinda\DB\Schema封装了对单个模式可以执行的操作。用法示例

$key = new Lucinda\DB\Schema("schema1"); // initializes SCHEMA
$value->deleteAll(); // deletes all VALUEs in SCHEMA

负载均衡

出于性能、一致性和可扩展性等原因,用户可以选择将SCHEMAs进行负载均衡和均匀分配。这是通过类完成的,该类确保所有模式操作都会自动反映在副本中。用法示例

$key = new Lucinda\DB\SchemaDriver(["schema1", "schema2"]); // initializes SCHEMAs
$value->deleteAll(); // deletes all VALUEs in all SCHEMAs

安装

首先选择一个文件夹,然后在控制台使用该命令写入

composer require lucinda/db

然后创建至少一个SCHEMA,并遵循配置指南,以便在配置API所需的XML中设置它/它们。完成后者后,您将能够使用Lucinda\DB\Wrapper对象查询数据库。用法示例

require 'vendor/autoload.php';

$object = new Lucinda\DB\Wrapper(XML_CONFIGURATION_PATH, DEVELOPMENT_ENVIRONMENT);
$value = $wrapper->getEntryDriver(["users", "roles"]);
$value->set(["John Doe"=>["Administrator"], "Jane Doe"=>["Assistant Manager", "Team Leader"]]);

API使用composer自动加载,需要PHP 8.1+,除了SimpleXML、DOM和SPL扩展之外,没有其他外部依赖。所有代码都是100%单元测试和基于简洁和优雅原则开发的!

配置

要配置此API,您必须有一个包含lucinda_db标签的XML文件

<lucinda_db>
	<{ENVIRONMENT}>
		<schemas>
      <schema>{SCHEMA}</schema>
      ...
    </schemas>
	</{ENVIRONMENT}>
	...
</lucinda_db>

在哪里

  • lucinda_db:(必填)包含数据库配置
    • {ENVIRONMENT}:(必填)开发环境名称(将被替换为“local”、“dev”、“live”等)
      • 模式: (必填) 存储待负载均衡的模式列表,每个模式作为一个 模式 标签
        • 模式: (必填) 包含参与负载均衡方案的单一模式的路径

示例

<lucinda_db>
    <local>
        <schemas>
          <schema>C:\db</schema>
        </schemas>
    </local>
    <live>
        <schemas>
          <schema>/usr/local/share/db</schema>
          <schema>/mnt/remote/db</schema>
        </schemas>
    </live>
</lucinda_db>

查询

XML 配置完成后,您可以通过 Lucinda\DB\Wrapper 类及其可用方法查询条目或模式

查询条目

使用示例:采用 Lucinda\DB\ValueDriver

$object = new Lucinda\DB\Wrapper("/var/www/html/myapp/configuration.xml", "local");
$driver = $wrapper->getEntryDriver(["users", "roles"]);
$driver->set(["John Doe"=>["Administrator"], "Jane Doe"=>["Assistant Manager", "Team Leader"]]);

直接使用 Lucinda\DB\Value 仅在您的应用程序不需要负载均衡、仅位于单个开发环境中且只需要基本维护时才有用。使用示例

$key = new Lucinda\DB\Key(["users", "roles"]);
$value = new Lucinda\DB\Value("/usr/local/share/db", $key->getValue());
$value->set(["John Doe"=>["Administrator"], "Jane Doe"=>["Assistant Manager", "Team Leader"]]);

无论是 Lucinda\DB\Value 还是其 Lucinda\DB\ValueDriver 包装器,都遵循一个合同,通过接口 Lucinda\DB\ValueOperations 定义了 VALUE 操作的蓝图,它具有以下原型方法

如上所示,如果开发人员选择直接使用 Lucinda\DB\Value,则需要手动实例化类 Lucinda\DB\Key。它封装了基于标签的键的创建,并定义以下公共方法

查询模式

使用示例:采用 Lucinda\DB\SchemaDriver

$object = new Lucinda\DB\Wrapper("/var/www/html/myapp/configuration.xml", "local");
$driver = $wrapper->getSchemaDriver();
$driver->deleteAll();

直接使用 Lucinda\DB\Schema 仅在您的应用程序不需要负载均衡、仅位于单个开发环境中且只需要基本维护时才有用。使用示例

$object = new Lucinda\DB\Schema("/usr/local/share/db");
$object->deleteAll();

无论是 Lucinda\DB\Schema 还是 Lucinda\DB\SchemaDriver,都遵循一个合同,通过接口 Lucinda\DB\SchemaOperations 定义了 SCHEMA 操作的蓝图,它具有以下原型方法

维护

现代操作系统允许一个文件夹中最多有 4,294,967,295 个文件,但您不应接近这个值!就像 MySQL 磁盘空间耗尽一样,LucindaDB 还可能耗尽 VALUE,这通常仅在 specialization 在大规模上使用时发生。

为了解决这类问题,创建了类 Lucinda\DB\DatabaseMaintenance,其目的是通过以下公共方法进行自动化维护

通过Cron作业

应该通过一个周期性取决于您项目填充机会的 cron job 来使用此类!示例

require 'vendor/autoload.php';

$maintenance = new Lucinda\DB\DatabaseMaintenance(XML_CONFIGURATION_PATH, DEVELOPMENT_ENVIRONMENT);
// checks schema health and plugs out unresponsive ones
$statuses  = $maintenance->checkHealth();
foreach ($statuses as $schema=>$status) {
  if (in_array($status, [DatabaseMaintenance::STATUS_OFFLINE, DatabaseMaintenance::STATUS_UNRESPONSIVE])) {
    $maintenance->plugOut($schema);
  }
}
// performs delete of all entries older than one day
$maintenance->deleteUntil(time()-(24*3600));

通过控制台命令

如果维护只涉及一个操作,可以通过调用 API 根目录中捆绑的 client.php 文件来不通过编程执行。控制台语法

php PATH_TO_CLIENT_PHP OPERATION ARGUMENTS

在哪里

  • PATH_TO_CLIENT_PHP: client.php 文件的绝对位置(例如:/var/www/html/mysite/vendor/lucinda/db/client.php
  • 操作:Lucinda\DB\DatabaseMaintenance类的名称(例如:deleteUntil
  • 参数:调用上述方法的参数,用空格分隔(例如:3600

这种解决方案的最大优点是它允许非程序员(DevOps)从命令行执行维护。示例

php /var/www/html/mysite/vendor/lucinda/db/client.php plugIn /my/new/schema

高级指南

特殊化键

有时,对于相同的TAG组合,需要生成不同的VALUE。这要求我们在遵守KEY部分的原则的同时,拥有不同的键。解决方案是在创建键时添加一个额外的特殊化标签(例如:查询的MD5校验和)

$object = new Lucinda\DB\Key(["users", "roles", md5($query)]);
$key = $object->getValue(); // key will be "54ed347f362bb056e4d6db0477bf19c9_roles_users"

通常,应尽可能避免特殊化,因为它会扩大数据库并具有潜在的重复性(例如,查询中的简单额外空格会生成另一个键)!

处理竞争条件

所有数据库都存在的一个复杂问题是管理竞态条件。当并行执行增加或减少操作时会发生什么?让我们假设DATA是8,并且用户X和Y在同一时刻Z尝试增加

# user X increments value at moment Z
$value->increment(1);
# user Y increments value at same moment Z
$value->increment(1);

最终结果会是预期的10吗?答案是,不会,因为两个进程同时得到了8来增加!这种情况被称为“竞态条件”,唯一的解决方案是将写入堆叠在该条目上,而不是让它们并行运行。

对于增加/减少VALUE操作,相应的文件在写入时被锁定,并且在值更新完成后才解锁。如果并发进程尝试写入仍然被锁定的文件,将抛出一个Lucinda\DB\LockException。开发人员可以捕获异常并在延迟后重试,而不是让异常向上冒泡。

try {
  $value->increment(1);
} catch(Lucinda\DB\LockException $e) {
  usleep(100);
  $value->increment(1);
}

检查模式健康状况

方法checkHealth @ 类Lucinda\DB\DatabaseMaintenance用于检查每个副本的SCHEMA状态并生成一个Lucinda\DB\SchemaStatus。生成状态所使用的算法是

  • 如果文件夹(schema)不存在,状态是OFFLINE
  • 否则,如果无法写入schema,状态是UNRESPONSIVE
  • 否则,如果文件写入时间超过最大写入时间,状态是OVERLOADED
  • 否则,状态是ONLINE

所有检查的结果将作为数组返回,其中键是schema,值是找到的状态。

插入模式

方法plugIn @ 类Lucinda\DB\DatabaseMaintenance用于将SCHEMA连接到副本,而无需数据库停机。使用的算法是

移除模式

方法plugOut @ 类Lucinda\DB\DatabaseMaintenance用于将SCHEMA从副本中拔出,而无需数据库停机。使用的算法是

按标签删除条目

方法 deleteByTag 位于类 Lucinda\DB\DatabaseMaintenance 中,用于从所有副本中移除所有其 KEYTAG 匹配的 VALUE。使用的算法是

  • 遍历随机副本 SCHEMA 中的条目
  • 如果条目 KEYTAG 匹配
    • 对于每个 SCHEMA 副本
      • 通过 KEY 上面的条目(VALUE)删除条目

按时间删除条目

方法 deleteUntil 位于类 Lucinda\DB\DatabaseMaintenance 中,用于移除所有其最后修改时间超过 $secondsBeforeNow 的 VALUE。使用的算法是

  • 遍历随机 SCHEMA 副本中的条目
  • 如果条目 VALUE 的最后修改时间超过 $secondsBeforeNow
    • 记住条目 KEY
    • 对于每个 SCHEMA 副本
      • 通过 KEY 上面的条目(VALUE)删除条目

按容量删除条目

方法 deleteByCapacity 位于类 Lucinda\DB\DatabaseMaintenance 中,用于确保数据库中的条目数量不超过 $maxCapacity,如果超过了,则通过删除较旧的条目将其缩小到 $minCapacity。使用的算法是

  • 遍历随机 SCHEMA 副本中的条目
  • 将条目 KEY 记录到一个固定容量的 SplMaxHeap 中,按 VALUE 的最后修改时间排序
  • 如果堆达到 $maxCapacity,弹出头部并删除条目,直到达到 $minCapacity

避免API缺点

这个基于磁盘的 API 与标准基于 RAM 的 KV 存储相比,有其自身的缺点

  • 略微降低的速度:硬盘总是比 RAM 慢,但考虑到 SSD 的速度如此之快,除非您的应用程序具有非常高的吞吐量,否则这不会成为问题
  • 没有条目过期:所有条目都是单独的文件,除非特别删除,否则将永久保留。这可以通过使用 Lucinda\DB\DatabaseMaintenance 来解决!
  • 不适用于临时/易失性数据:如果您的应用程序期望数据库条目是易失性的(随机更改)并且需要大规模的 特殊键,则强烈建议使用标准的基于 RAM 的 KV 存储库
  • 需要守护进程维护:如果预计会有波动性,则必须定期删除旧文件以防止磁盘(s)变满。这可以通过使用 Lucinda\DB\DatabaseMaintenance 来解决!

开发者总是需要决定哪种KV存储模型最适合他们的应用程序!很多时候,你需要使用多个存储(例如:LucindaDB和Redis)来覆盖所有用例(一个用于持久化可查询数据,另一个用于易失性数据)。

使用示例

要查看使用示例,以下单元测试应该足够