lucinda / db
Lucinda DB:纯PHP基于标签的键值存储
Requires
- php: ^8.1
- ext-simplexml: *
- ext-spl: *
- ext-xml: *
Requires (Dev)
- lucinda/unit-testing: ^2.0
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!根据谁计算键名而定的规则如下:
键的创建被封装在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”等)
- 模式: (必填) 存储待负载均衡的模式列表,每个模式作为一个 模式 标签
- 模式: (必填) 包含参与负载均衡方案的单一模式的路径
- 模式: (必填) 存储待负载均衡的模式列表,每个模式作为一个 模式 标签
- {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连接到副本,而无需数据库停机。使用的算法是
- 如果SCHEMA不存在或它已经连接,将抛出一个Lucinda\DB\ConfigurationException
- 将第一个副本的所有文件复制到SCHEMA
- 将SCHEMA连接到XML
- 将可能已插入到先前副本中作为初始复制过程的剩余文件复制到SCHEMA
移除模式
方法plugOut @ 类Lucinda\DB\DatabaseMaintenance用于将SCHEMA从副本中拔出,而无需数据库停机。使用的算法是
- 如果 SCHEMA 不存在或未连接,则会抛出 Lucinda\DB\ConfigurationException 异常
- SCHEMA 已从 XML 中移除,确保不会发生进一步的写入
- 在 SCHEMA 中删除所有文件
按标签删除条目
方法 deleteByTag 位于类 Lucinda\DB\DatabaseMaintenance 中,用于从所有副本中移除所有其 KEY 与 TAG 匹配的 VALUE。使用的算法是
按时间删除条目
方法 deleteUntil 位于类 Lucinda\DB\DatabaseMaintenance 中,用于移除所有其最后修改时间超过 $secondsBeforeNow 的 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)来覆盖所有用例(一个用于持久化可查询数据,另一个用于易失性数据)。
使用示例
要查看使用示例,以下单元测试应该足够