Sfoglia il codice sorgente

去掉fastcgi_finish_request

lizhen 14 ore fa
parent
commit
a84b946ba3
2 ha cambiato i file con 418 aggiunte e 2 eliminazioni
  1. 2 2
      addons/exam/controller/Paper.php
  2. 416 0
      fastcgi_finish_request原理说明.md

+ 2 - 2
addons/exam/controller/Paper.php

@@ -463,8 +463,8 @@ class Paper extends Base
             
             Db::name('icbc_queue')->insert($data);
             
-            // 立即尝试异步处理(不阻塞当前请求)
-            $this->processIcbcQueueAsync();
+            // 不立即处理,完全由定时任务处理
+            // 这样可以避免 fastcgi_finish_request() 影响正常响应
             
         } catch (\Exception $e) {
             // 队列写入失败,记录日志但不影响主流程

+ 416 - 0
fastcgi_finish_request原理说明.md

@@ -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);  // 记录所有操作
+```
+
+这是一个**生产级**的异步处理方案! 🎉
+