|
@@ -0,0 +1,416 @@
|
|
|
|
+# fastcgi_finish_request() 原理详解
|
|
|
|
+
|
|
|
|
+## 🎯 核心概念
|
|
|
|
+
|
|
|
|
+`fastcgi_finish_request()` 是 **PHP-FPM** (FastCGI Process Manager) 提供的特殊函数,用于在不关闭 PHP 进程的情况下,立即将响应发送给客户端。
|
|
|
|
+
|
|
|
|
+## 🔧 技术原理
|
|
|
|
+
|
|
|
|
+### 1. FastCGI 协议层面
|
|
|
|
+
|
|
|
|
+```
|
|
|
|
+Nginx/Apache PHP-FPM PHP脚本
|
|
|
|
+ │ │ │
|
|
|
|
+ ├─── 请求 ──────────→│ │
|
|
|
|
+ │ ├─── 执行 ─────────→│
|
|
|
|
+ │ │ │ 处理业务逻辑
|
|
|
|
+ │ │ │ 生成响应内容
|
|
|
|
+ │ │ │
|
|
|
|
+ │ │ │ fastcgi_finish_request()
|
|
|
|
+ │ │←─── 响应 ─────────┤
|
|
|
|
+ │←─── 响应 ──────────┤ │
|
|
|
|
+ │ │ │ 继续执行后续代码
|
|
|
|
+ │ 用户已收到响应 │ │ 调用第三方API
|
|
|
|
+ │ │ │ 发送邮件等
|
|
|
|
+ │ │ │ 处理完毕
|
|
|
|
+ │ │ │
|
|
|
|
+```
|
|
|
|
+
|
|
|
|
+### 2. 函数调用时发生了什么
|
|
|
|
+
|
|
|
|
+```php
|
|
|
|
+<?php
|
|
|
|
+echo "开始处理...";
|
|
|
|
+
|
|
|
|
+// 1. 刷新输出缓冲区
|
|
|
|
+ob_flush();
|
|
|
|
+flush();
|
|
|
|
+
|
|
|
|
+// 2. 调用 fastcgi_finish_request()
|
|
|
|
+fastcgi_finish_request();
|
|
|
|
+
|
|
|
|
+// ===== 分界线 =====
|
|
|
|
+// 用户已经收到响应,但 PHP 继续执行
|
|
|
|
+
|
|
|
|
+sleep(10); // 用户不会等待这10秒
|
|
|
|
+echo "这个不会发送给用户";
|
|
|
|
+```
|
|
|
|
+
|
|
|
|
+#### 内部执行步骤:
|
|
|
|
+
|
|
|
|
+1. **刷新所有输出缓冲区**
|
|
|
|
+ - 将所有待发送的内容发送出去
|
|
|
|
+
|
|
|
|
+2. **关闭与客户端的连接**
|
|
|
|
+ - 告诉 Web 服务器响应已完成
|
|
|
|
+ - Web 服务器将响应返回给用户
|
|
|
|
+
|
|
|
|
+3. **PHP 进程继续运行**
|
|
|
|
+ - 不会终止脚本
|
|
|
|
+ - 可以继续执行任何代码
|
|
|
|
+
|
|
|
|
+4. **资源保持**
|
|
|
|
+ - 数据库连接保持打开
|
|
|
|
+ - Session 仍然可用
|
|
|
|
+ - 文件句柄保持有效
|
|
|
|
+
|
|
|
|
+## 📊 对比分析
|
|
|
|
+
|
|
|
|
+### 方案一:不使用 fastcgi_finish_request()
|
|
|
|
+
|
|
|
|
+```php
|
|
|
|
+// Paper.php
|
|
|
|
+public function submit() {
|
|
|
|
+ // 1. 保存成绩
|
|
|
|
+ saveGrade($data); // 100ms
|
|
|
|
+
|
|
|
|
+ // 2. 调用工行接口
|
|
|
|
+ callIcbcApi(); // 3000ms (3秒)
|
|
|
|
+
|
|
|
|
+ // 3. 返回响应
|
|
|
|
+ $this->success('提交成功');
|
|
|
|
+}
|
|
|
|
+```
|
|
|
|
+
|
|
|
|
+**用户等待时间**: 100ms + 3000ms = **3100ms** ❌
|
|
|
|
+
|
|
|
|
+### 方案二:使用 fastcgi_finish_request()
|
|
|
|
+
|
|
|
|
+```php
|
|
|
|
+// Paper.php
|
|
|
|
+public function submit() {
|
|
|
|
+ // 1. 保存成绩
|
|
|
|
+ saveGrade($data); // 100ms
|
|
|
|
+
|
|
|
|
+ // 2. 立即返回响应
|
|
|
|
+ $this->success('提交成功');
|
|
|
|
+
|
|
|
|
+ // 3. 结束FastCGI响应
|
|
|
|
+ if (function_exists('fastcgi_finish_request')) {
|
|
|
|
+ fastcgi_finish_request();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 4. 后台调用工行接口(用户已收到响应)
|
|
|
|
+ callIcbcApi(); // 3000ms (用户不用等)
|
|
|
|
+}
|
|
|
|
+```
|
|
|
|
+
|
|
|
|
+**用户等待时间**: 100ms ✅
|
|
|
|
+
|
|
|
|
+### 方案三:写入队列(我们采用的)
|
|
|
|
+
|
|
|
|
+```php
|
|
|
|
+// Paper.php
|
|
|
|
+public function submit() {
|
|
|
|
+ // 1. 保存成绩
|
|
|
|
+ saveGrade($data); // 100ms
|
|
|
|
+
|
|
|
|
+ // 2. 写入队列
|
|
|
|
+ addQueue($data); // 5ms
|
|
|
|
+
|
|
|
|
+ // 3. 返回响应
|
|
|
|
+ $this->success('提交成功');
|
|
|
|
+
|
|
|
|
+ // 4. 异步处理队列
|
|
|
|
+ if (function_exists('fastcgi_finish_request')) {
|
|
|
|
+ fastcgi_finish_request();
|
|
|
|
+ }
|
|
|
|
+ processQueue(); // 用户不用等
|
|
|
|
+}
|
|
|
|
+```
|
|
|
|
+
|
|
|
|
+**用户等待时间**: 100ms + 5ms = **105ms** ✅✅✅
|
|
|
|
+
|
|
|
|
+## 💡 实际应用场景
|
|
|
|
+
|
|
|
|
+### 1. 第三方API调用
|
|
|
|
+
|
|
|
|
+```php
|
|
|
|
+public function order() {
|
|
|
|
+ // 创建订单
|
|
|
|
+ $order = createOrder($data);
|
|
|
|
+
|
|
|
|
+ // 返回给用户
|
|
|
|
+ $this->success('订单创建成功', $order);
|
|
|
|
+
|
|
|
|
+ // 异步执行
|
|
|
|
+ fastcgi_finish_request();
|
|
|
|
+
|
|
|
|
+ // 调用支付接口(慢)
|
|
|
|
+ notifyPaymentGateway($order);
|
|
|
|
+
|
|
|
|
+ // 发送短信通知(慢)
|
|
|
|
+ sendSMS($order->phone);
|
|
|
|
+
|
|
|
|
+ // 更新统计数据(慢)
|
|
|
|
+ updateStatistics();
|
|
|
|
+}
|
|
|
|
+```
|
|
|
|
+
|
|
|
|
+### 2. 日志记录
|
|
|
|
+
|
|
|
|
+```php
|
|
|
|
+public function action() {
|
|
|
|
+ // 业务处理
|
|
|
|
+ $result = doSomething();
|
|
|
|
+
|
|
|
|
+ // 返回响应
|
|
|
|
+ $this->success('操作成功', $result);
|
|
|
|
+
|
|
|
|
+ // 异步记录详细日志
|
|
|
|
+ fastcgi_finish_request();
|
|
|
|
+
|
|
|
|
+ // 写入详细日志(可能很慢)
|
|
|
|
+ writeDetailLog($result);
|
|
|
|
+
|
|
|
|
+ // 上报到监控系统
|
|
|
|
+ reportToMonitor($result);
|
|
|
|
+}
|
|
|
|
+```
|
|
|
|
+
|
|
|
|
+### 3. 数据同步
|
|
|
|
+
|
|
|
|
+```php
|
|
|
|
+public function update() {
|
|
|
|
+ // 更新本地数据
|
|
|
|
+ updateLocalData($data);
|
|
|
|
+
|
|
|
|
+ // 返回成功
|
|
|
|
+ $this->success('更新成功');
|
|
|
|
+
|
|
|
|
+ // 异步同步到其他系统
|
|
|
|
+ fastcgi_finish_request();
|
|
|
|
+
|
|
|
|
+ // 同步到ES
|
|
|
|
+ syncToElasticsearch($data);
|
|
|
|
+
|
|
|
|
+ // 同步到Redis
|
|
|
|
+ syncToRedis($data);
|
|
|
|
+
|
|
|
|
+ // 推送到消息队列
|
|
|
|
+ pushToQueue($data);
|
|
|
|
+}
|
|
|
|
+```
|
|
|
|
+
|
|
|
|
+## ⚠️ 注意事项
|
|
|
|
+
|
|
|
|
+### 1. 仅在 PHP-FPM 下可用
|
|
|
|
+
|
|
|
|
+```php
|
|
|
|
+// 必须先检查函数是否存在
|
|
|
|
+if (function_exists('fastcgi_finish_request')) {
|
|
|
|
+ fastcgi_finish_request();
|
|
|
|
+} else {
|
|
|
|
+ // CLI 模式或 Apache mod_php 下不可用
|
|
|
|
+ // 需要其他方案
|
|
|
|
+}
|
|
|
|
+```
|
|
|
|
+
|
|
|
|
+### 2. Session 处理
|
|
|
|
+
|
|
|
|
+```php
|
|
|
|
+// 如果使用了 Session,需要先关闭
|
|
|
|
+session_write_close();
|
|
|
|
+
|
|
|
|
+// 然后再调用
|
|
|
|
+fastcgi_finish_request();
|
|
|
|
+
|
|
|
|
+// 否则 Session 文件会一直被锁定
|
|
|
|
+```
|
|
|
|
+
|
|
|
|
+### 3. 数据库连接
|
|
|
|
+
|
|
|
|
+```php
|
|
|
|
+// 响应后继续使用数据库
|
|
|
|
+fastcgi_finish_request();
|
|
|
|
+
|
|
|
|
+// 数据库连接仍然可用
|
|
|
|
+Db::name('log')->insert($data);
|
|
|
|
+
|
|
|
|
+// 但要注意连接超时问题
|
|
|
|
+```
|
|
|
|
+
|
|
|
|
+### 4. 错误处理
|
|
|
|
+
|
|
|
|
+```php
|
|
|
|
+fastcgi_finish_request();
|
|
|
|
+
|
|
|
|
+// 此后的异常不会被用户看到
|
|
|
|
+try {
|
|
|
|
+ dangerousOperation();
|
|
|
|
+} catch (Exception $e) {
|
|
|
|
+ // 必须记录日志,用户已经看不到错误了
|
|
|
|
+ Log::error('后台任务失败: ' . $e->getMessage());
|
|
|
|
+}
|
|
|
|
+```
|
|
|
|
+
|
|
|
|
+### 5. 超时限制
|
|
|
|
+
|
|
|
|
+```php
|
|
|
|
+// PHP 脚本执行时间限制仍然有效
|
|
|
|
+fastcgi_finish_request();
|
|
|
|
+
|
|
|
|
+// 如果后续操作耗时很长,需要设置
|
|
|
|
+set_time_limit(300); // 5分钟
|
|
|
|
+// 或者在 php.ini 中设置 max_execution_time
|
|
|
|
+```
|
|
|
|
+
|
|
|
|
+## 🎯 我们项目中的应用
|
|
|
|
+
|
|
|
|
+### 代码位置: `addons/exam/controller/Paper.php`
|
|
|
|
+
|
|
|
|
+```php
|
|
|
|
+private function processIcbcQueueAsync()
|
|
|
|
+{
|
|
|
|
+ // 使用fastcgi_finish_request()立即返回响应给用户
|
|
|
|
+ if (function_exists('fastcgi_finish_request')) {
|
|
|
|
+ fastcgi_finish_request();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 后台处理队列
|
|
|
|
+ $this->processIcbcQueue();
|
|
|
|
+}
|
|
|
|
+```
|
|
|
|
+
|
|
|
|
+### 执行流程
|
|
|
|
+
|
|
|
|
+```
|
|
|
|
+1. 用户交卷
|
|
|
|
+ ↓
|
|
|
|
+2. 保存成绩到数据库 (100ms)
|
|
|
|
+ ↓
|
|
|
|
+3. 写入积分队列表 (5ms)
|
|
|
|
+ ↓
|
|
|
|
+4. 返回成功响应给用户
|
|
|
|
+ ↓
|
|
|
|
+5. 调用 fastcgi_finish_request() ← 用户已收到响应
|
|
|
|
+ ↓
|
|
|
|
+6. 处理队列,调用工行接口 (3000ms) ← 用户不用等
|
|
|
|
+ ↓
|
|
|
|
+7. 更新队列状态
|
|
|
|
+ ↓
|
|
|
|
+8. 记录日志
|
|
|
|
+ ↓
|
|
|
|
+9. 脚本结束
|
|
|
|
+```
|
|
|
|
+
|
|
|
|
+### 优势
|
|
|
|
+
|
|
|
|
+- ✅ **用户体验好**: 交卷后立即看到结果(~105ms)
|
|
|
|
+- ✅ **可靠性高**: 即使工行接口超时,也不影响交卷
|
|
|
|
+- ✅ **可追踪**: 队列表记录了所有任务状态
|
|
|
|
+- ✅ **可重试**: 失败任务自动重试3次
|
|
|
|
+
|
|
|
|
+## 🔄 替代方案对比
|
|
|
|
+
|
|
|
|
+### 1. 不使用异步
|
|
|
|
+```
|
|
|
|
+优点: 简单
|
|
|
|
+缺点: 用户等待时间长
|
|
|
|
+```
|
|
|
|
+
|
|
|
|
+### 2. 使用 exec() 执行后台脚本
|
|
|
|
+```php
|
|
|
|
+exec('php task.php > /dev/null 2>&1 &');
|
|
|
|
+```
|
|
|
|
+```
|
|
|
|
+优点: 完全异步
|
|
|
|
+缺点: 创建新进程,开销大,不好管理
|
|
|
|
+```
|
|
|
|
+
|
|
|
|
+### 3. 使用消息队列 (Redis/RabbitMQ)
|
|
|
|
+```
|
|
|
|
+优点: 专业的异步方案
|
|
|
|
+缺点: 需要额外的服务,复杂度高
|
|
|
|
+```
|
|
|
|
+
|
|
|
|
+### 4. 使用数据库队列 + fastcgi_finish_request()
|
|
|
|
+```
|
|
|
|
+优点: 简单可靠,无需额外服务
|
|
|
|
+缺点: 处理能力受限于数据库
|
|
|
|
+```
|
|
|
|
+
|
|
|
|
+**我们的方案**: ✅ 方案4(数据库队列 + fastcgi_finish_request())
|
|
|
|
+
|
|
|
|
+## 📚 技术细节
|
|
|
|
+
|
|
|
|
+### FastCGI 协议
|
|
|
|
+
|
|
|
|
+```
|
|
|
|
+REQUEST_BEGIN
|
|
|
|
+ ↓
|
|
|
|
+PARAMS (请求参数)
|
|
|
|
+ ↓
|
|
|
|
+STDIN (输入数据)
|
|
|
|
+ ↓
|
|
|
|
+处理脚本
|
|
|
|
+ ↓
|
|
|
|
+STDOUT (输出数据) ← fastcgi_finish_request() 在这里关闭连接
|
|
|
|
+ ↓
|
|
|
|
+REQUEST_END ← 但脚本继续运行
|
|
|
|
+```
|
|
|
|
+
|
|
|
|
+### PHP-FPM 进程管理
|
|
|
|
+
|
|
|
|
+```
|
|
|
|
+[PHP-FPM Master]
|
|
|
|
+ ├─ [Worker 1] ← 处理请求1
|
|
|
|
+ ├─ [Worker 2] ← 处理请求2
|
|
|
|
+ ├─ [Worker 3] ← 调用了 fastcgi_finish_request()
|
|
|
|
+ │ 用户已收到响应
|
|
|
|
+ │ 但进程仍在执行后续代码
|
|
|
|
+ └─ [Worker 4] ← 处理请求4
|
|
|
|
+```
|
|
|
|
+
|
|
|
|
+## 🎓 总结
|
|
|
|
+
|
|
|
|
+### fastcgi_finish_request() 的本质
|
|
|
|
+
|
|
|
|
+1. **不是真正的多线程或异步**
|
|
|
|
+ - 仍然是单进程执行
|
|
|
|
+ - 只是提前关闭了与客户端的连接
|
|
|
|
+
|
|
|
|
+2. **适用场景**
|
|
|
|
+ - 需要快速响应用户
|
|
|
|
+ - 有耗时的后续操作
|
|
|
|
+ - 后续操作不影响响应内容
|
|
|
|
+
|
|
|
|
+3. **不适用场景**
|
|
|
|
+ - 需要返回异步操作的结果
|
|
|
|
+ - CLI 命令行模式
|
|
|
|
+ - Apache mod_php 环境
|
|
|
|
+
|
|
|
|
+4. **最佳实践**
|
|
|
|
+ - 结合数据库队列使用
|
|
|
|
+ - 添加错误处理和日志
|
|
|
|
+ - 设置合理的超时时间
|
|
|
|
+ - 定时任务兜底处理失败任务
|
|
|
|
+
|
|
|
|
+### 我们的实现优势
|
|
|
|
+
|
|
|
|
+```php
|
|
|
|
+// 1. 快速响应用户
|
|
|
|
+fastcgi_finish_request(); // 立即返回
|
|
|
|
+
|
|
|
|
+// 2. 有队列保证可靠性
|
|
|
|
+addQueue($data); // 数据不丢失
|
|
|
|
+
|
|
|
|
+// 3. 有定时任务兜底
|
|
|
|
+php think icbc_queue // 处理失败任务
|
|
|
|
+
|
|
|
|
+// 4. 有日志可追踪
|
|
|
|
+writeLog($result); // 记录所有操作
|
|
|
|
+```
|
|
|
|
+
|
|
|
|
+这是一个**生产级**的异步处理方案! 🎉
|
|
|
|
+
|