
# 引擎

## 选择引擎

这三种引擎存储的内容相同：一个 UUIDv7 ID 和一个数据数组，但它们在磁盘上的布局不同。布局决定了每种引擎的适用场景：

|            | DocStore | SegmentedLog | Queue |
| ---------- | -------- | ------------ | ----- |
| 磁盘存储    | 每条记录一个 JSONC 文件，按 UUID 尾部字符分片 | 仅追加的分段，每行以长度和校验和为前缀 JSON，另有清单 | 一个仅追加的事件日志 |
| 写入       | 每条记录原子替换文件 | 带校验和的追加写入；覆盖和删除会追加新条目，由 `compact()` 回收 | 每个队列事件都带校验和追加写入 |
| 读取       | 按 ID `get()`，支持索引查询 | 游标和时间范围扫描，通过状态索引使用 `get()` | 按入队顺序 `claim()` |
| 适用场景   | 任意集合大小下的点查和索引查找 | 约 1M 条记录以内的追加密集型数据流 | 进程间的持久化任务分发 |
| 薄弱之处   | 超过约 10k 条记录后的非索引字段扫描 | 频繁改写单条记录（压缩前旧版本会占用空间） | 不是通用记录存储；任务完成后负载数据会被丢弃 |

`DocStore` 和 `SegmentedLog` 使用相同的流式
[查询API](https://storh.inline0.dev/zh/docs/query-builder)：同一条 `where()` 链可在任一引擎上运行，只有执行方式不同。`DocStore` 可以通过二级索引作答，而 `SegmentedLog` 则会在扫描分段时进行筛选。
`Queue` 基于认领，而非查询：工作进程会 `claim()` 下一个任务，而不是筛选任务。

经验法则：

- 需要逐条获取、更新或删除，并按字段查询的记录：
  `DocStore`。为筛选所用字段添加 [索引](https://storh.inline0.dev/zh/docs/indexes)。
- 主要以追加方式写入，并按顺序或时间窗口读回的事件或版本：
  `SegmentedLog`。
- 从生产者交给工作进程的任务：`Queue`。认领和完成操作在多进程环境下安全，`claimMany()`/`completeMany()`
  可分摊锁和 fsync 开销。
- 对任意集合进行联接、搜索或报告：添加
  [SQL 镜像](https://storh.inline0.dev/zh/docs/sql-mirror)。文件仍是权威数据；镜像是存储在 SQLite 或 MySQL 中、可重建的投影。

各引擎可以组合使用：它们只共享根目录，因此一个应用可以在同一存储根目录下，将内容保存在 `DocStore` 中，将审计记录保存在 `SegmentedLog` 中，并将后台任务保存在 `Queue` 中。

## DocStore

`DocStore` 适用于对持久化记录进行点查和简单扫描。每条记录写入一个 JSONC 文件：

```php
$store = new Storh\DocStore($root, 'articles');
$record = $store->put(['slug' => 'hello', 'published' => true]);
$same = $store->get($record->id());
```

记录文件按 UUID 尾部字符分片，因此 UUIDv7 写入突发会分散到不同目录：

```text
articles/data/2d/018bcfe5-6800-7000-8000-2d5dba6274da.jsonc
```

## SegmentedLog

`SegmentedLog` 适用于追加密集型记录、游标读取和时间范围读取。
记录会写入活动分段，每条记录占一行，格式为
`<byte-length>\t<xxh32>\t<json>`（参见 [原子性](https://storh.inline0.dev/zh/docs/atomicity)）。分段文件使用
`.ndjson` 扩展名，但长度和校验和前缀使普通 NDJSON 工具无法读取；请使用 `stream()` 或查询构建器读回记录。完整分段会滚动封存，并附带稀疏索引。

```php
$events = new Storh\SegmentedLog($root, 'events');
$events->put(['type' => 'user.created']);
$events->compact();
```

## 队列

`Queue` 适用于高吞吐量的持久化工作队列。入队、认领、完成、重新入队和清空操作会向 `queue.log` 写入仅追加的事件；打开队列时，会将队列状态从日志重放到内存中。批量方法可将锁和刷新开销分摊到多个任务上。日志会随每个事件增长；`stats()` 会报告当前字节大小。

```php
$queue = new Storh\Queue($root, 'mail');
$queue->enqueue(['to' => 'person@example.test']);
$job = $queue->claim();
```

## 持久性与并发

storh 在目标目录内通过原子重命名发布文件写入。重新打开和 `repair()` 会移除所有者进程已退出的遗弃临时文件，同时保留仍有活动写入进程的临时文件。写入者标记使检查的时间复杂度为常数，因此只有发生崩溃后，打开存储时才会扫描整个集合。

`SegmentedLog` 和 `Queue` 写入带长度和校验和保护的日志行。重新打开或修复时，会将残缺的日志尾部截断至最后一个已提交事件，并根据持久化日志内容重建内存状态。日志和队列写入通过文件系统锁串行执行。

`DocStore` 的变更通过集合级写锁串行执行，因此记录文件和二级索引会在跨进程的一致性边界内一并更新。在相同文件系统原子重命名假设下，并发写入不同记录 ID 是安全的。并发写入相同 ID 时，以最后一次重命名为准。

storh 会在重命名或追加完成前刷新文件句柄并执行 fsync，并会在原子重命名后尝试对父目录执行 fsync。断电后的持久性仍取决于底层文件系统和挂载选项是否遵循这些同步操作。
