service类在swoole或--daemon模式下因单例常驻导致变量污染,需禁用static属性、隔离请求上下文、事务加try/catch、避免构造函数注入request等请求对象,并为日志设置独立可写路径。

Service 类在常驻进程里会变量污染
ThinkPHP 的 Service 类默认是单例,一旦被容器加载,实例就常驻内存。在 Swoole 或 php think queue:work --daemon 这类常驻模式下,静态属性、私有成员变量、未重置的缓存数组(比如 private static $cache = [])不会随请求结束而销毁,下次调用可能读到上一次遗留的数据。
常见现象:用户 A 的订单数据被缓存在 $this->tempResult 里,用户 B 下次调用同一 Service 方法时直接返回了 A 的结果;或数据库连接未显式关闭,导致连接数缓慢上涨。
- 禁止在 Service 中使用
static属性存业务状态或中间结果 - 若必须缓存,改用请求上下文隔离的存储,例如
app('request')->service_cache或 SwooleTable - 构造函数中注入的对象(如
Cache、Request)需确认是否支持多请求复用;否则应在方法内重新获取
Service 事务和数据库连接不自动释放
传统 FPM 模式下,脚本结束即释放连接;但常驻进程里,Db::transaction() 开启的事务若没显式 commit 或 rollback,可能卡住连接池,甚至阻塞后续请求。
更隐蔽的是:Service 方法抛出异常但没被捕获,导致事务未回滚,连接被标记为“busy”却未归还。
- 所有事务逻辑必须包裹在
try/catch中,catch里确保Db::rollback() - 避免在 Service 构造函数里执行 DB 查询——它只运行一次,结果会被所有后续请求共享
- 使用
Db::connect()->close()主动释放连接(尤其在长循环消费队列任务后)
依赖注入对象生命周期错配
ThinkPHP 容器默认将 Service 绑定为单例,但如果它依赖了 Request、Session、Cookie 等请求级对象,这些对象在常驻进程中不会自动刷新,容易拿到过期或错乱的上下文。
典型错误:Service 构造函数写 public function __construct(private Request $request),然后在 fire() 里读 $this->request->param('id') —— 实际取到的是第一次初始化时的值。
- 请求相关依赖不要塞进构造函数,改在方法内通过
app('request')按需获取 - 若必须注入,需手动绑定为“每次解析新建”,例如在
provider.php中:$app->bind(Service::class, function ($app) { return new Service($app->make(Request::class)); }); - 检查第三方扩展(如 Redis 封装类)是否也做了静态连接复用,必要时加
reset()逻辑
日志和 trace 写入路径要可写且隔离
常驻进程运行时间长,如果 Service 里用 trace() 或 Log::write() 写日志,默认路径(如 runtime/log/)可能因权限、磁盘满、文件锁等问题静默失败;更麻烦的是多个进程共用一个日志文件,内容混杂难排查。
Supervisor 启动的多个 queue:work 实例,若都往同一个 queue.log 写,会出现日志截断或覆盖。
- 为每个常驻进程指定独立日志路径,例如在 Supervisor
.ini中设环境变量:environment=QUEUE_LOG_PATH="/var/log/tp8-queue-worker1.log" - Service 内部日志写入前先判断路径可写:
is_writable($path) && file_put_contents($path, ...) - 避免在循环体中高频调用
trace(),改用计数采样或错误触发式记录
最易被忽略的是:你以为 Service 只是“封装逻辑”,但它在常驻进程里实际成了状态中心。任何没被清理的引用、没被重置的连接、没被隔离的上下文,都会像雪球一样越滚越大,直到某天某个请求突然超时或返回错数据——而错误日志里什么痕迹都没有。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











