Shared\Registry
OxPHP\Shared\Registry 是 OxPHP\Shared\* 其余部分的按名称索引的搭档。new Shared\Map() 产生的是匿名条目,只能通过句柄传播(use 捕获、异步纤程、嵌套)来共享;而 Registry::map('cache', fn() => new Shared\Map(...)) 会把一个条目绑定到某个字符串键之下。任何工作进程线程、任何请求中的每一个 Registry::map('cache', …) 调用方,都会拿到同一个条目。
它回答的是一个问题:"我该如何在所有工作进程之间、或在传统模式下的所有请求之间,共享同一个 Shared\Map?" 其他 Shared\* 类型仍然是可变状态的恰当单元;Registry 只是你给其中某一个起名字的方式。
心智模型
graph TD
R["Registry::map('cache', $factory)"]
W1["工作进程 #1"] --> R
W2["工作进程 #2"] --> R
W3["工作进程 #3"] --> R
R --> S["SharedRegistry(进程全局)<br/>names: { 'cache' → Bound(Arc<E>) }<br/>entries: { id=7: Map { … } }"]
- 对于尚未绑定的键,第一个调用
Registry::map($key, $factory)的调用方会运行工厂函数,并把生成的条目按名称固定下来。 - 之后的每一个调用方(同一线程、其他工作进程、后续请求)都会拿到同一个条目。工厂函数不会重新运行;命中时它会被忽略。
- 并发的首次访问会在一个按键的门闩上阻塞:恰好一个线程运行工厂函数,其余线程等待并拿到胜出者的条目。这可以避免连接池的重复资源获取。
按名称标识是对按句柄标识的补充。匿名条目(new Shared\*())与具名条目共存于同一个进程全局注册表中。名称索引只是在其之上加了一层字符串查找。
快速上手 —— 在所有工作进程间共享一个计数器
<?php
// worker.php — entry script in worker mode, executed once per worker thread
require __DIR__ . '/vendor/autoload.php';
$requests = OxPHP\Shared\Registry::counter(
'request-counter',
fn() => new OxPHP\Shared\Counter(),
);
oxphp_worker(function () use ($requests) {
$n = $requests->add(); // atomic across ALL workers — one shared int64
header('X-Request-Count: ' . $n);
echo "hello\n";
});可以与捕获句柄的模式对比一下(在 bootstrap 中写 $x = new Shared\Counter())。那种模式会为每个工作进程线程产生一个计数器:每个工作进程运行各自的 bootstrap,拿到各自的匿名条目。聚合出的计数会相差工作进程池大小这么大的倍数。而 Registry::counter('request-counter', …) 会让每个工作进程都汇聚到同一个条目上,因此计数就是真实的总数。
同样的写法在传统模式下(未设置 WORKER_MODE_ENABLED)也适用。第一个访问 'request-counter' 的请求会创建条目;之后的每一个请求(无论在哪个工作进程线程上)都能看到它。这就是同主机上替代 APCu 的方案,用带类型的原语和原子操作取代 apcu_fetch / apcu_store。
API 参考
namespace OxPHP\Shared;
final class Registry
{
// Typed get-or-create. On hit, the factory is ignored; on miss it
// runs at most once across all workers (block-losers) and must
// return a fresh instance of the matching type.
public static function map(string $key, callable $factory): Map;
public static function counter(string $key, callable $factory): Counter;
public static function atomic(string $key, callable $factory): Atomic;
public static function flag(string $key, callable $factory): Flag;
public static function once(string $key, callable $factory): Once;
public static function mutex(string $key, callable $factory): Mutex;
public static function channel(string $key, callable $factory): Channel;
public static function pool(string $key, callable $factory): Pool;
// Untyped escape hatch — returns whatever is bound (no type guard).
public static function global(string $key, callable $factory): Shareable;
// Namespace management — operates on the name index, NOT the objects.
public static function remove(string $key): bool;
public static function keys(): array; // list<string>
// Layer-wide introspection.
public static function memoryUsage(): int; // estimated bytes, all Shared\* entries
public static function count(): int; // live Shared\* entries (named + anonymous)
}| 方法 | 返回 | 用途 |
|---|---|---|
map / counter / atomic / flag / once / mutex / channel / pool |
所请求的 Shared\* 类型 |
主要接口。命中时做类型守卫;工厂返回时做类型校验。 |
global |
Shareable |
无类型的 get-or-create。只有当你确实事先不知道绑定类型时才用它。 |
remove |
bool |
丢弃名称绑定与固定引用。不会销毁对象。 |
keys |
list<string> |
当前已绑定的键(仅 Bound;处于创建中的 Creating 槽位不会列出)。 |
memoryUsage |
int |
进程范围内的估算字节数 —— 参见 内存与自省。 |
count |
int |
进程范围内的存活条目(具名与匿名)。 |
Registry 是一个静态外观(facade):new Registry() 会抛出 Shared\SharedException。
生命周期 —— 默认固定
已绑定的键持有对其条目的强引用;除非你显式调用 remove(key) 或进程被拆除,否则条目在整个进程生命周期内都存活。这是有意为之的:在传统模式下,每个请求都会创建各自的 PHP 端句柄,并在请求结束时销毁,此时名称索引的固定引用是条目能在请求之间存活下来的唯一原因。
要让具名条目的内容失效,请就地修改它($cache->clear()、$counter->set(0)、$bucket->remove($k)),而不是移除名称。修改是按引用共享的:持有同一个键的每个持有者都会立即看到变化。
remove 是命名空间管理,而非对象销毁
remove($key) 会丢弃绑定和固定引用。只要还有任何其他句柄引用它(bootstrap 中捕获的变量、嵌套在另一个 Shared\Map 中的值、进行中的 oxphp_async),条目本身就会继续存活。当最后一个句柄消失时,条目会像往常一样自行注销。
remove 之后,该键就空闲了。下一次 Registry::map($key, …) 会创建一个 id 不同的全新条目。
指向先前绑定的已捕获句柄会继续操作旧的(现已匿名的)条目;它们不会自动汇聚到新条目上。
$cache = Registry::map('cache', fn() => new Shared\Map());
$id_a = $cache->id();
Registry::remove('cache');
$cache->set('x', 1); // still mutates the OLD entry — fine, but it's no longer "cache"
$fresh = Registry::map('cache', fn() => new Shared\Map());
$id_b = $fresh->id(); // different id — this is a new entry
assert($id_a !== $id_b);
assert($cache->get('x') === 1); // OLD entry retained value
assert($fresh->get('x') === null); // NEW entry is empty如果你会轮换键(随来随走的按租户条目、键版本化),请在每次调用时按名称寻址(每个请求都调用 Registry::map($key, …)),而不是在 bootstrap 中只捕获一次句柄。捕获句柄加上键轮换会悄无声息地发散;按名称寻址则始终汇聚到当前的绑定上。
如果丢弃了一个已绑定的键,remove 返回 true;如果该键不存在,则返回 false。
错误
| 异常 | 何时发生 |
|---|---|
Shared\TypeException |
在绑定到其他类型的键上调用了带类型的方法;工厂返回了错误的 Shared\* 类型或非 Shareable 值。 |
Shared\CapacityException |
创建会超出 SHARED_MAX_ENTRIES / SHARED_MAX_BYTES 上限。 |
Shared\DeadlockException(重入) |
在同一线程上,从某个键自己的工厂内部再对同一个 $key 调用 Registry::map($key, …)。 |
Shared\DeadlockException(跨键环路) |
在另一个线程的 Creating 槽位上等待超过 30 s —— 最可能的情况是工厂 A 持有键 K1 并等待 K2,而 K2 的工厂由线程 B 持有,B 又在等待 K1。其消息与重入情形不同。 |
Shared\SharedException(正在排空) |
服务器正在关闭 —— 注册表拒绝新的获取和绑定。这在优雅关闭期间是预期行为;不是代码 bug。 |
Shared\SharedException(绑定竞争) |
在本线程的工厂运行期间,某个对等创建者已经先一步占据了该槽位(工厂的条目没有被固定到该键之下)。重试该调用。 |
\InvalidArgumentException(SPL) |
$key 为空。参数校验,与领域类型错误不同。 |
| (工厂抛出的异常) | 如果工厂抛出异常,槽位会被中止(Creating → 不存在,等待者被唤醒并重试),原始异常会传播给创建者。 |
Shared\DeadlockException 继承自 OxPHP\Async\AsyncException,因此 catch (AsyncException) 会把它与 Shared\* 中其他地方的有界等待超时一并捕获。两种不同的 DeadlockException 情形共用同一个类;靠消息来区分它们("reentrant get-or-create" 与 "waited too long … cross-key cycle")。
内存与自省
Registry::memoryUsage() 和 Registry::count() 报告的是整个 Shared* 层,而不仅仅是具名条目。通过 new Shared\*() 创建的匿名条目(当前 Shared\* 用法的大头:bootstrap 捕获、Map 和 Channel 内部的在途值、异步纤程捕获)也包含在内。
这是有意为之的。这两个数字是为容量 / OOM 监控而存在的;这类监控必须看到每个工作进程的以及在途的匿名状态,而不只是具名命名空间。因此:
- 这两个数字都是瞬时的:它们随在途请求和每个工作进程的句柄而起伏。
Registry::count()不等于count(Registry::keys())。keys()仅指具名命名空间。memoryUsage()是一个静态记账估算值,不是实际的 RSS。它就是SHARED_MAX_BYTES所限制的那个数字。要获取真实的堆内存占用,请使用堆性能分析器(heaptrack、jemalloc_stats_print、mi_stats_print)或容器内存指标。
每个条目的细节(id、类型、引用计数、字节开销)位于内部自省端点的 /__ox_shared/entries。这里有意不提供逐条目的 PHP API,以避免重复那一层接口。
何时不该使用
- **跨进程、跨主机。**注册表存在于单个 OxPHP 进程内部。多个 OxPHP 实例之间并不共享它。请使用 Redis / NATS / 你现有的消息代理;参见迁移到外部存储。
- **跨重启的持久性。**注册表会在进程退出时蒸发。请通过同样的外部存储来持久化。
- **高频变动的临时键。**默认固定的语义意味着你按请求生成的动态键会不断泄漏条目,直到你调用
remove。虽然受SHARED_MAX_*上限约束,但仍然是糟糕的做法。对于短命的按请求状态,请使用普通的 PHP 变量。 - 缓存失效原语。
remove($key)用于退役一个名称,而不是"清空缓存"。请就地让内容失效($map->clear()、$map->remove($member_key));名称绑定会保留。
另请参阅
- 共享状态。该层的概览、按句柄标识,以及何时
new Shared\*()是恰当的工具。 - Shared\Map、Shared\Counter、Shared\Pool,以及
Registry返回的其他带类型的原语。 - 共享可观测性。
/__ox_shared/*的 JSON API 与 Prometheus 指标。 - 迁移到外部存储。当你超出单个进程的能力范围时。