Storh
面向 PHP 的文件优先记录:JSONC 文档、只追加的分段日志,以及由日志支持、使用 UUIDv7 id 的队列。
composer require storh/storh最新版本 v0.5.2 MIT PHP 8.2+
$pages = $docs
->query()
->where('kind')->eq('page')
->orderBy('title')
->limit(20)
->get();实践应用
两部分快速入门:先在存储根目录中创建文档,再在旁边添加日志条目和队列作业。
一个根目录,三种引擎
StorageRoot::resolve() 会将基础目录和命名空间规范化为一个根目录,每种引擎首次使用时都会在其中创建自己的目录。
每种引擎都会返回带有 id() 和 data() 的 StorageRecord 对象。队列会将每次领取和完成操作追加到其事件日志中。
use Storh\DocStore;
use Storh\Queue;
use Storh\RecordQuery;
use Storh\SegmentedLog;
use Storh\StorageRoot;
$root = StorageRoot::resolve(__DIR__ . '/var/storh', 'demo');
$docs = new DocStore($root, 'pages');
$home = $docs->put([
'kind' => 'page',
'title' => 'Home',
]);
$loaded = $docs->get($home->id());$log = new SegmentedLog($root, 'events');
$log->put([
'type' => 'page.saved',
'pageId' => $home->id(),
]);
foreach ($log->stream(RecordQuery::all()->after($home->id())->limit(50)) as $record) {
// Process records lazily.
}
$queue = new Queue($root, 'jobs');
$jobId = $queue->enqueue(['task' => 'render', 'pageId' => $home->id()]);
$job = $queue->claim();
if (null !== $job) {
$queue->complete($job->id());
}JSONC 文档存储
每条记录对应一个 JSONC 文件、使用 UUIDv7 id、按 UUID 最后一组中的两个字符进行分片、原子替换,并支持流式字段筛选。
分段日志
只追加的分段包含带长度和校验和前缀的 JSON 行,并支持稀疏索引、游标和时间范围读取、崩溃恢复及压缩。
日志队列
作业通过紧凑的只追加队列事件,在待处理、处理中和已完成状态之间流转。
UUIDv7 记录
可排序、可按时间定位的 id,适用于稳定游标、有界扫描和便于分片的存储路径。
原子写入
临时文件写入和原子重命名可确保记录完整。写入进程崩溃后,下次打开时会清理其孤立的临时文件。
写入截断恢复
分段读取会验证长度和校验和字段;恢复时会截断末尾不完整的行。
文件优先
无需数据库服务器、框架适配器或 Composer 运行时软件包;调用方只需传入一个目录。
读取 JSONC
记录文件可以包含注释和尾随逗号。storh 会写入紧凑的单行 JSON,因此注释会一直保留到下次写入。
查询构建器
DocStore 和 SegmentedLog 支持流畅查询,例如 where('status')->in([...]);DocStore 会使用现有的二级索引。
二级索引
相等、唯一和范围索引以文件形式存储在集合旁边,由记录派生生成,并可随时重建。
架构
可选的集合架构会验证每次写入,并声明哪些字段需要建立索引。
SQL 镜像
将集合推送到 SQLite 或 MySQL,即可进行联接、搜索和报告,同时文件仍是权威数据源。只有在使用时才需要 ext-pdo 或 ext-mysqli。
为什么选择 Storh
一个目录,无需服务器
你只需传入一个基础目录,storh 就会在那里写入。它需要 PHP 8.2 和 ext-json,无需 Composer 运行时软件包或框架适配器。
一种记录格式,三种存储布局
文档、日志和队列都会存储一个 UUIDv7 id 和一个数据数组。你可以根据工作负载选择磁盘布局,并将三者都放在同一个根目录下。
可运行的检查
verify() 会将索引和日志状态与文件的完整扫描结果进行核对。repair() 会隔离损坏的记录并截断日志末尾的不完整部分;CLI 会为 cron 作业和健康检查打印 JSON。
附有配置说明的基准测试
README 会公布在配备 PHP 8.5 的 Apple M1 Pro 上,各引擎处理 50,000 条记录时的中位数,例如每次点读取耗时 5.6 µs。这些测试中的每次写入都会执行 fsync,因此写入速率取决于文件系统。
对比
storh 是文件优先的存储,而非关系型查询引擎,因此这些工具可以完成它未涵盖的一些工作。
| 工具 | 擅长领域 | 与 Storh 的区别 |
|---|---|---|
| SQLite | 擅长领域一个自包含的 SQL 引擎,全部封装在单个文件中,并支持可序列化的 ACID 事务,即使程序崩溃或断电也能保持事务完整。其文档将其描述为零散数据文件的替代方案。 | 与 Storh 的区别storh 为每条记录保留一个 JSON 文件,其核心无需 PDO 驱动。它不支持跨多条记录的事务,相关工作应交由真正的数据库处理。它可以将集合推送到 SQLite 以进行联接和报告,同时文件仍是权威数据源。 |
| MySQL | 擅长领域由多线程 SQL 服务器提供服务的关系型数据库,配有客户端程序和库、管理工具以及丰富的 API。 | 与 Storh 的区别storh 在 PHP 进程内针对一个目录运行,因此无需运维服务器。若要进行联接、搜索和报告,它可以将集合镜像到 MySQL,也可以使用现有的 mysqli 句柄;文件仍是事实来源。 |
| SleekDB | 擅长领域一个基于 PHP 的平面文件文档数据库,无第三方依赖,以纯 JSON 文件存储无架构数据。其查询 API 支持联接、文本搜索、group by 和 having,以及对嵌套属性进行筛选。 | 与 Storh 的区别storh 将文档存储与只追加的分段日志和日志队列配对,统一置于一个根目录下,并在报告每次写入已存储之前执行 fsync。联接并非 storh 的功能;它会将集合推送到 SQLite 或 MySQL 来实现联接。 |
问答
来自 Storh 文档的简明解答。
可以。DocStore 使用集合级写锁来串行处理变更;对同一个 id 的并发写入采用最后重命名者胜出。队列领取任务时会持有队列锁,因此两个工作进程不会领取同一个待处理作业。
是。读取时接受注释和尾随逗号,但下次对该 ID 执行 put() 时,会将文件重写为紧凑的 JSON 并删除这些内容。使用 TRUST 缓存模式时,不会检测到在 storh 外部更改的文件。
只要文件系统能够处理文件数量,按 id 点读取就会保持快速。未建立索引的 DocStore 字段扫描在记录数超过约 10k 后会变慢,因此请为需要筛选的字段声明索引。分段日志扫描和范围读取可轻松应对约 1M 条记录。
写入会执行 fsync 并通过原子重命名发布;重新打开日志或队列时,会将不完整的末尾截断到最后一个已提交事件。断电后的持久性仍取决于文件系统和挂载选项是否遵循这些同步操作。
不支持。使用 SqlMirror 将集合推送到 SQLite 或 MySQL,以进行联接、搜索和报告。跨多条记录的原子写入仍应交由作为权威数据源的真正数据库处理。
不能。队列不是通用记录存储,作业完成后,其负载就会被丢弃。
从快速入门开始,然后使用“选择引擎”,根据工作负载选择 DocStore、SegmentedLog 或 Queue。