一、Laravel 在协程环境中的核心风险在传统同步模型中一个请求结束后进程也会被释放或进入干净状态。即使服务对象内部保存了当前请求的状态也不会影响下一个请求。但在协程模型中一个进程会同时处理多个请求。这意味着以下状态如果继续存放在共享对象、静态属性或单例中就可能在请求之间互相污染当前请求对象当前 Session当前认证用户当前路由当前 Locale延迟事件队列数据库事务计数器Facade 已解析实例缓存服务对象内部的可变属性。因此Laravel 协程化的关键不是重写框架而是解决一个问题如何把“请求级状态”从“进程级共享对象”中剥离出来。二、两条改造路径处理这个问题大致有两条思路。路径一为每个请求创建新的服务实例第一种方式最直接不再让所有请求共享同一个服务对象而是让容器在每个请求内重新创建服务实例。服务仍然通过依赖注入容器解析只是容器不再跨请求复用同一个对象。请求 A → AuthManager A 请求 B → AuthManager B 请求 C → AuthManager C这种方式的优点非常明显优点无需大规模修改服务代码。原有服务仍然使用对象属性保存状态只要这些对象是请求级实例就不会发生跨请求泄漏。但它也有明显缺点缺点如果服务内部缓存了大量跨请求数据内存占用会膨胀。例如某些服务原本作为单例存在是为了缓存配置、解析结果或元数据。如果每个请求都重新创建这些服务内存使用和初始化成本都会上升异步化带来的收益也会被部分抵消。因此这条路径适合用来快速完成初步适配尤其适合那些状态复杂、短期内难以拆分的服务。路径二保留共享服务将可变状态移入上下文第二种方式更精细也更符合协程运行时的设计。服务对象仍然可以作为所有请求共享的单例存在但它的可变状态不再放在对象属性中而是放入请求级上下文。共享服务对象 ├── 不变逻辑继续保留在服务实例中 └── 可变状态移入请求 Scope Context例如当前请求对象当前 Session当前认证状态当前路由当前 Locale当前 defer 队列。这些都不应该继续作为共享单例的属性存在而应该进入当前请求的上下文中。更优的方向是共享不可变逻辑隔离可变状态。这条路径的优点是内存更稳定也更适合长期维护。缺点是需要识别框架中所有可能发生状态泄漏的位置并逐一适配。三、TrueAsync 的两层上下文模型理解 TrueAsync 的关键在于它提供了两层上下文。1. Coroutine Context协程级上下文Coroutine Context是单个协程的私有存储。它适合保存只属于当前协程的数据例如当前协程使用的数据库事务计数器当前协程的临时状态不应该被同一请求内其他协程共享的数据。也就是说Coroutine Context 解决的是“同一请求内多个协程之间的隔离问题”。2. Scope Context作用域级上下文Scope Context是一组协程共享的上下文。Scope 可以形成层级结构子 Scope 会继承父 Scope 中的内容。服务器可以先创建一个全局的 Server Scope然后每个请求再基于它创建自己的 Request Scope$requestScope Scope::inherit($serverScope);这样请求 Scope 会继承服务器层级设置的共享内容同时又可以保存只对当前请求可见的数据。例如current_context()-set(ScopedService::REQUEST, $request); current_context()-set(ScopedService::SESSION, $session); current_context()-set(ScopedService::AUTH, $auth);在 Controller、Middleware 或嵌套协程中都可以通过当前上下文读取这些数据$request current_context()-find(ScopedService::REQUEST);这意味着如果某个请求内部启动了多个嵌套协程例如并行查询数据库这些协程仍然可以访问父请求 Scope Context 中的 request、session、auth 等数据。同时它们又与其他请求的 Scope Context 完全隔离。Server Scope ├── Request Scope A │ ├── Coroutine A1 │ └── Coroutine A2 │ └── Request Scope B ├── Coroutine B1 └── Coroutine B2Scope Context 解决的是“不同请求之间的状态隔离问题”。四、laravel-spawn 的整体策略laravel-spawn 并没有选择单一方案而是结合了两条路径对部分服务使用请求级实例避免共享可变状态对部分服务保留单例但将可变状态移动到 Scope Context对 Laravel Facade 使用代理对象避免静态缓存真实服务实例对隐藏较深的状态通过 Trait 局部替换相关行为对数据库事务计数器等协程级状态使用 Coroutine Context 隔离。这种方式的核心优势在于不需要重写 Laravel也不需要推翻现有服务体系而是针对状态泄漏点做局部适配。五、使用枚举作为上下文键Scope Context 本质上是一个键值存储。为了避免字符串键冲突可以使用对象作为键。PHP 的枚举也就是 Enum本身是对象非常适合作为上下文键。enum ScopedService: string { case REQUEST request; case SESSION session; case AUTH auth; case AUTH_DRIVER auth.driver; case COOKIE cookie; }写入当前请求对象current_context()-set(ScopedService::REQUEST, $request);在代码任意位置读取$request current_context()-find(ScopedService::REQUEST);这种设计有三个好处。1. 避免字符串键冲突如果使用字符串键不同模块可能不小心使用同一个键名request session auth但使用 Enum 后即使两个枚举的 backed value 相同它们仍然是不同的对象键。ScopedService::REQUEST SomeOtherEnum::REQUEST即使二者的值都是request也不会互相覆盖。不同 Enum case 是不同对象不会因为字符串值相同而冲突。2. 提高访问边界清晰度使用公开 Enum 并不是安全边界但它能显著减少误读、误写和误覆盖。如果上下文键是普通字符串任何代码都可以随手写入current_context()-set(session, $value);而使用 Enum 后代码必须显式依赖对应的枚举定义current_context()-set(ScopedService::SESSION, $session);这让上下文访问变得更加可控也更容易审查。3. 对静态分析更友好Enum 键可以被 IDE、PHPStan 或 Psalm 追踪。例如搜索ScopedService::AUTH就可以直接找到所有读取或写入认证状态的位置。相比字符串键Enum 键更适合长期维护。对于协程适配来说可追踪性非常重要。因为状态泄漏问题往往不是语法错误而是运行时行为错误。六、Facade 静态缓存带来的问题Laravel Facade 有一个重要优化它会把已经解析过的实例缓存到静态数组中。例如调用Auth::user();Laravel 并不会每次都执行$app-make(auth);第一次访问时Facade 会把解析结果保存到静态属性中static::$resolvedInstance[auth]后续调用会直接复用这个实例。在传统同步模型中这是合理优化。因为一个进程只处理一个请求请求结束后状态自然清理。但在协程模型中这会变成严重问题。请求 A 首次调用 Auth::user() → Facade 缓存 AuthManager A 请求 B 调用 Auth::user() → Facade 直接复用 AuthManager A如果 AuthManager 内部保存了当前用户、Guard 或 Session 状态请求 B 就可能拿到请求 A 的认证信息。Facade 的问题不在于静态调用本身而在于它会静态缓存真实服务实例。七、使用 ScopedServiceProxy 屏蔽 Facade 缓存laravel-spawn 使用代理对象解决这个问题。Facade 可以继续缓存对象但缓存的不再是真实服务而是一个代理。class ScopedServiceProxy { public function __construct( private readonly \Closure $resolver, ) {} public function __call(string $method, array $args): mixed { return ($this-resolver)()-$method(...$args); } public function __get(string $property): mixed { return ($this-resolver)()-$property; } }这个代理本身可以被 Facade 安全缓存。关键在于每次调用方法时它都会重新执行$resolver。而$resolver会通过current_context()从当前请求的 Scope Context 中取出真正的服务实例。Facade 缓存的对象ScopedServiceProxy 真实服务实例每次从当前 Scope Context 解析这样一来Facade 的缓存机制仍然存在但它缓存的是无状态代理而不是带有请求状态的服务对象。八、AsyncApplication 中的替换逻辑在 Laravel 中Facade 通过容器读取服务。laravel-spawn 在AsyncApplication::offsetGet()中加入了替换逻辑。public function offsetGet($key): mixed { if ($this-asyncMode) { $alias $this-getAlias($key); if (isset(self::FACADE_PROXIED_MAP[$alias])) { return new ScopedServiceProxy( fn () $this-tryResolveScoped($alias) ); } } return parent::offsetGet($key); }其中FACADE_PROXIED_MAP只包含需要代理的服务例如auth session也就是说只有通过Auth::和Session::这类 Facade 访问的高风险服务才会被代理。这是一个低侵入改造不修改业务代码也不修改 Facade 调用方式只替换 Facade 最终拿到的对象。九、Auth::user() 的完整调用链适配后Auth::user()的调用链变成Auth::user() → Facade::__callStatic(user) → static::$resolvedInstance[auth] → ScopedServiceProxy → proxy-__call(user) → resolver() → tryResolveScoped(auth) → current_context()-find(...) → 当前请求的 AuthManager → $authManager-user()两个并行请求同时调用Auth::user();它们命中的可能是同一个ScopedServiceProxy但每次调用时代理都会从当前协程所属的 Scope Context 中解析真实服务。请求 A → ScopedServiceProxy → Request Scope A → AuthManager A 请求 B → ScopedServiceProxy → Request Scope B → AuthManager BFacade 仍然可以缓存但缓存的是代理真实服务始终从当前请求上下文中获取。这正是代理模式在 Laravel 协程适配中的价值。十、使用 Trait 局部替换状态相关行为并不是所有状态泄漏都适合通过代理或重新实例化解决。有些服务会把可变状态隐藏在类的内部属性、静态数组或深层方法中。如果为了一个属性替换整个类成本会非常高。这种情况下可以使用 Trait 局部替换相关方法。Trait 的作用是只替换与状态相关的行为保留原类其余逻辑。这种策略尤其适合 Laravel 内部复杂类。它能在尽量少改代码的前提下把高风险状态迁移到协程上下文中。十一、数据库事务计数器的协程隔离一个典型例子是数据库连接中的事务计数器。TrueAsync 中的 PDO Pool 会为每个协程提供自己的物理数据库连接。但 Laravel 的Connection类会把嵌套事务计数器保存在对象属性中$this-transactions如果多个协程共享同一个Connection实例就会出现问题。协程 A 开启事务 → $this-transactions 1 协程 B 查询 transactionLevel() → 也看到 1但协程 B 实际上并没有开启事务。这就是典型的共享对象状态泄漏。十二、CoroutineTransactions 的处理方式laravel-spawn 通过CoroutineTransactionsTrait 将事务计数器移入 Coroutine Context。trait CoroutineTransactions { private const CTX_TRANSACTIONS db.transactions; public function transactionLevel() { if ($this-asyncTransactions) { return coroutine_context()-find(self::CTX_TRANSACTIONS) ?? 0; } return $this-transactions; } private function setTransactionLevel(int $level): void { if ($this-asyncTransactions) { $ctx coroutine_context(); $ctx-set(self::CTX_TRANSACTIONS, $level, replace: true); } else { $this-transactions $level; } } }这个 Trait 会拦截以下方法transactionLevel();beginTransaction();commit();rollBack();事务错误处理相关方法。每个方法都不再直接依赖$this-transactions而是通过coroutine_context()读取当前协程自己的事务状态。这里需要特别注意事务计数器应该绑定到 Coroutine Context而不是 Scope Context。原因是 Scope Context 用于请求级共享状态而事务计数器与当前协程使用的物理数据库连接相关。如果一个请求内部启动多个并行协程每个协程都可能从连接池中获取不同的物理连接。此时事务状态应该跟随协程而不是跟随整个请求。Request Scope ├── Coroutine A → PDO Connection A → transactionLevel A └── Coroutine B → PDO Connection B → transactionLevel B因此事务计数器使用coroutine_context()是更合理的选择。十三、推荐的 Laravel 协程化改造顺序结合 laravel-spawn 的实践比较现实的改造路径可以分为五步。第一步识别可变状态优先扫描以下位置静态属性写入单例服务中的可变属性Facade 已解析实例缓存保存 request、session、auth、route、locale 的对象数据库连接、事务、事件队列等运行时状态。这一阶段可以借助 PHPStan 自定义规则检测对可变静态属性的写入。协程适配的第一步不是改代码而是找出所有可能被请求共享的可变状态。第二步为请求创建 Scope Context每个请求进入时创建独立的请求 Scope并将请求级数据写入其中。典型数据包括current_context()-set(ScopedService::REQUEST, $request); current_context()-set(ScopedService::SESSION, $session); current_context()-set(ScopedService::AUTH, $auth);这样Controller、Middleware、事件监听器和嵌套协程都可以通过当前上下文读取请求数据。第三步处理 Facade 缓存对高风险 Facade 使用代理对象例如Auth;Session;Cookie;其他持有请求状态的服务。核心原则是Facade 可以缓存代理但不能缓存请求级真实服务。第四步对深层状态使用 Trait 局部替换对于无法简单拆分的框架内部类可以使用 Trait 替换少量关键方法。例如数据库事务计数器$this-transactions可以迁移到coroutine_context()这样既能避免重写整个类又能隔离协程状态。第五步用并发测试验证状态隔离协程适配不能只靠单元测试还需要构造并发场景。重点测试请求 A 的用户不会泄漏到请求 B请求 A 的 Session 不会影响请求 B并发数据库事务互不干扰Locale、路由、Cookie、事件队列不会串请求嵌套协程能正确继承父请求上下文请求结束后上下文能被释放。协程问题通常不是“功能不可用”而是“高并发下偶发串状态”。测试必须围绕状态隔离设计。十四、异步化的真正收益协程异步化并不会让 PHP 代码本身执行得更快。它提升吞吐量的关键在于当一个请求正在等待数据库、Redis、HTTP API 或文件 I/O 时当前进程可以切换去处理其他请求。在传统同步模型中一个请求的执行时间可能大部分都消耗在等待 I/O 上。例如某个请求总耗时 28 毫秒其中 23 毫秒都在等待数据库响应。同步模型下这 23 毫秒内 worker 基本处于空转状态。在协程模型中这段等待时间可以用来处理其他请求。同步模型 worker 等待 I/O → 空转 协程模型 协程 A 等待 I/O → worker 切换处理协程 B因此异步化带来的收益主要体现在更少的 worker更低的内存占用更高的并发承载能力更少的服务器资源I/O 密集型场景下更高的吞吐量。异步化优化的不是单个 PHP 函数的执行速度而是整个进程在 I/O 等待期间的利用率。十五、结论Laravel 虽然基于同步的 request-per-process 模型设计但这并不意味着它无法适配协程运行环境。真正需要解决的问题是将请求级可变状态从进程级共享对象中剥离出来。laravel-spawn 展示了一条现实路径对部分服务使用请求级实例对部分服务保留单例但将状态移入 Scope Context使用 Enum 作为上下文键减少键冲突并提升可维护性使用代理对象解决 Facade 静态缓存问题使用 Trait 局部替换深层状态逻辑使用 Coroutine Context 隔离协程级状态例如数据库事务计数器使用静态分析和并发测试发现潜在泄漏点。这说明Laravel 协程化并不一定意味着重写框架也不一定需要大规模修改业务代码。更准确地说它是一项明确的工程适配工作。当运行时提供合适的原语例如 Coroutine Context、Scope Context、Scope 继承和原生连接池时Laravel 的协程异步化就不再是架构重写问题而是状态隔离问题。