---
  PHP 常驻内存 + 协程模式完整指南                                                                                       
  先搞清楚:传统PHP vs 常驻内存的区别                                                                                   
  传统PHP(每次请求都死一次再活一次):
    请求来了 → 启动PHP → 加载框架 → 连接数据库 → 处理 → 返回 → 进程死掉
    请求来了 → 启动PHP → 加载框架 → 连接数据库 → 处理 → 返回 → 进程死掉
    (每次都重复这些废操作,慢且浪费)

  常驻内存PHP(进程一直活着):
    启动一次 → 加载框架 → 连接数据库池
    请求来了 → 直接处理 → 返回(进程继续活着等下一个)
    请求来了 → 直接处理 → 返回
    (框架和连接只加载一次,快10倍以上)

  ---
  技术选型:用 Swoole

  Swoole 是PHP的C扩展,给PHP加上了:
  - 常驻内存(进程不死)
  - 协程(一个进程同时处理多个请求,不用等)
  - 异步IO(等数据库的时候去干别的事)

  ---
  完整项目结构

  myapp/
  ├── server.php          # 启动入口
  ├── src/
  │   ├── App.php         # 应用核心
  │   ├── Router.php      # 路由
  │   ├── DbPool.php      # 数据库连接池
  │   └── Controller/
  │       └── UserController.php
  └── composer.json

  ---
  第一步:安装

  # 安装Swoole扩展
  pecl install swoole

  # php.ini 加上
  extension=swoole

  # 验证
  php -m | grep swoole

  # 安装依赖
  composer require psr/http-message

  ---
  第二步:数据库连接池(最核心的东西)

  <?php
  // src/DbPool.php

  /**
   * 大白话解释连接池:
   *
   * 没有连接池:每个请求都新建连接→用完→断开
   *   就像每次打电话都要重新买手机,用完扔掉
   *
   * 有了连接池:提前建好10个连接放着
   *   请求来了→从池子里借一个→用完→还回去
   *   就像公司有10部公用电话,用完放回去
   */

  use Swoole\Database\PDOConfig;
  use Swoole\Database\PDOPool;
  use Swoole\Coroutine;

  class DbPool
  {
      private static PDOPool $pool;
      private static bool $initialized = false;

      public static function init(array $config): void
      {
          if (self::$initialized) {
              return;
          }

          // 建立连接池,最多20个并发连接
          self::$pool = new PDOPool(
              (new PDOConfig())
                  ->withHost($config['host'])
                  ->withPort($config['port'])
                  ->withDbName($config['database'])
                  ->withUsername($config['username'])
                  ->withPassword($config['password'])
                  ->withCharset('utf8mb4'),
              20  // 池子大小:最多20个连接同时存在
          );

          self::$initialized = true;
      }

      /**
       * 借一个连接出来用
       * 用完必须还!用完必须还!用完必须还!
       */
      public static function borrow(): PDO
      {
          return self::$pool->get();
      }

      /**
       * 还回去
       */
      public static function return(PDO $pdo): void
      {
          self::$pool->put($pdo);
      }

      /**
       * 推荐用这个:自动借、自动还,不会忘记还
       * 就像图书馆自动还书机
       */
      public static function query(callable $callback): mixed
      {
          $pdo = self::borrow();
          try {
              return $callback($pdo);
          } finally {
              // finally保证不管成功失败都会还回去
              self::return($pdo);
          }
      }
  }

  ---
  第三步:协程是什么,用大白话说

  没有协程(同步阻塞):
    进程:我去查数据库... [等200ms] ...查完了,返回
    这200ms里进程什么都不干,就傻等着

  有了协程(异步非阻塞):
    协程A:我去查数据库... [挂起,让出CPU]
    协程B:我来处理!我去查Redis... [挂起,让出CPU]
    协程C:我来处理!我在计算...
    协程A:数据库返回了,我继续!
    协程B:Redis返回了,我继续!

    一个进程同时"并发"处理多个请求
    实际上是快速切换,不是真正的并行
    就像一个厨师同时做3道菜:
      下锅A → 等待 → 切菜B → 等待 → 翻炒C → 回来看A

  ---
  第四步:路由器

  <?php
  // src/Router.php

  class Router
  {
      // 存所有路由规则
      // 格式:['GET']['/users'] = [控制器类, 方法名]
      private array $routes = [];

      public function get(string $path, array $handler): void
      {
          $this->routes['GET'][$path] = $handler;
      }

      public function post(string $path, array $handler): void
      {
          $this->routes['POST'][$path] = $handler;
      }

      /**
       * 根据请求找到对应的处理函数
       * 支持 /users/123 这种动态路由
       */
      public function dispatch(string $method, string $path): ?array
      {
          // 先精确匹配
          if (isset($this->routes[$method][$path])) {
              return [
                  'handler' => $this->routes[$method][$path],
                  'params'  => [],
              ];
          }

          // 再尝试动态匹配(把 {id} 转成正则)
          foreach ($this->routes[$method] ?? [] as $pattern => $handler) {
              // /users/{id} → 正则 /users/(\d+)
              $regex = preg_replace('/\{(\w+)\}/', '(?P<$1>[^/]+)', $pattern);
              $regex = '#^' . $regex . '$#';

              if (preg_match($regex, $path, $matches)) {
                  // 只保留命名捕获组(过滤掉数字索引)
                  $params = array_filter(
                      $matches,
                      fn($k) => is_string($k),
                      ARRAY_FILTER_USE_KEY
                  );
                  return ['handler' => $handler, 'params' => $params];
              }
          }

          return null; // 没找到路由
      }
  }

  ---
  第五步:控制器

  <?php
  // src/Controller/UserController.php

  class UserController
  {
      /**
       * 获取用户列表
       * 大白话:这里的数据库查询是协程的
       * 查询期间这个协程挂起,Swoole去处理别的请求
       * 查完了再回来继续
       */
      public function index(array $params, array $query): array
      {
          $page  = (int)($query['page'] ?? 1);
          $limit = 10;
          $offset = ($page - 1) * $limit;

          // DbPool::query 内部用了协程安全的连接池
          $users = DbPool::query(function (PDO $pdo) use ($limit, $offset) {
              $stmt = $pdo->prepare(
                  'SELECT id, name, email, created_at FROM users
                   ORDER BY id DESC LIMIT :limit OFFSET :offset'
              );
              $stmt->bindValue(':limit',  $limit,  PDO::PARAM_INT);
              $stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
              $stmt->execute();
              return $stmt->fetchAll(PDO::FETCH_ASSOC);
          });

          return ['code' => 0, 'data' => $users, 'page' => $page];
      }

      /**
       * 获取单个用户
       */
      public function show(array $params, array $query): array
      {
          $id = (int)$params['id'];

          $user = DbPool::query(function (PDO $pdo) use ($id) {
              $stmt = $pdo->prepare(
                  'SELECT id, name, email, created_at FROM users WHERE id = :id'
              );
              $stmt->execute([':id' => $id]);
              return $stmt->fetch(PDO::FETCH_ASSOC);
          });

          if (!$user) {
              return ['code' => 404, 'message' => '用户不存在'];
          }

          return ['code' => 0, 'data' => $user];
      }

      /**
       * 创建用户
       * 演示协程并发:同时做两件事
       */
      public function store(array $params, array $body): array
      {
          $name  = trim($body['name']  ?? '');
          $email = trim($body['email'] ?? '');

          if (!$name || !$email) {
              return ['code' => 400, 'message' => '名字和邮箱不能为空'];
          }

          if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
              return ['code' => 400, 'message' => '邮箱格式不对'];
          }

          // 用协程并发做两件事:检查邮箱是否存在 + 记录日志
          // WaitGroup就像"等所有人都到齐再开会"
          $wg      = new Swoole\Coroutine\WaitGroup();
          $exists  = false;
          $logDone = false;

          // 协程1:检查邮箱重复
          $wg->add();
          Coroutine::create(function () use ($email, &$exists, $wg) {
              $exists = DbPool::query(function (PDO $pdo) use ($email) {
                  $stmt = $pdo->prepare(
                      'SELECT COUNT(*) FROM users WHERE email = :email'
                  );
                  $stmt->execute([':email' => $email]);
                  return (int)$stmt->fetchColumn() > 0;
              });
              $wg->done(); // 我做完了
          });

          // 协程2:同时写访问日志(模拟)
          $wg->add();
          Coroutine::create(function () use (&$logDone, $wg) {
              // 模拟异步写日志(实际可以写Redis或文件)
              Coroutine::sleep(0.001);
              $logDone = true;
              $wg->done(); // 我也做完了
          });

          $wg->wait(); // 等两个协程都完成

          if ($exists) {
              return ['code' => 409, 'message' => '邮箱已被注册'];
          }

          // 插入数据库
          $userId = DbPool::query(function (PDO $pdo) use ($name, $email) {
              $stmt = $pdo->prepare(
                  'INSERT INTO users (name, email, created_at)
                   VALUES (:name, :email, NOW())'
              );
              $stmt->execute([':name' => $name, ':email' => $email]);
              return (int)$pdo->lastInsertId();
          });

          return ['code' => 0, 'data' => ['id' => $userId], 'message' => '创建成功'];
      }
  }

  ---
  第六步:应用核心

  <?php
  // src/App.php

  class App
  {
      private Router $router;

      public function __construct()
      {
          $this->router = new Router();
          $this->registerRoutes();
      }

      private function registerRoutes(): void
      {
          $this->router->get('/users',       [UserController::class, 'index']);
          $this->router->get('/users/{id}',  [UserController::class, 'show']);
          $this->router->post('/users',      [UserController::class, 'store']);
      }

      /**
       * 处理一个HTTP请求
       * 这个函数在每个协程里运行
       * 多个请求 = 多个协程 = 并发处理
       */
      public function handle(Swoole\Http\Request $req, Swoole\Http\Response $res): void
      {
          $method = strtoupper($req->server['request_method']);
          $path   = $req->server['request_uri'];
          // 去掉query string部分
          $path   = strtok($path, '?');

          // 找路由
          $route = $this->router->dispatch($method, $path);

          if (!$route) {
              $res->status(404);
              $res->header('Content-Type', 'application/json');
              $res->end(json_encode(['code' => 404, 'message' => '接口不存在']));
              return;
          }

          [$class, $action] = $route['handler'];
          $params = $route['params'];

          // 解析请求体(POST的JSON数据)
          $body = [];
          if ($method === 'POST' || $method === 'PUT') {
              $rawBody = $req->rawContent();
              if ($rawBody) {
                  $body = json_decode($rawBody, true) ?? [];
              }
          }

          // 解析query参数(?page=1&size=10$query = $req->get ?? [];

          try {
              // 调用控制器方法
              $controller = new $class();
              $result     = $controller->$action($params, $method === 'GET' ? $query : $body);

              $res->header('Content-Type', 'application/json; charset=utf-8');
              $res->status($result['code'] === 0 ? 200 : $result['code']);
              $res->end(json_encode($result, JSON_UNESCAPED_UNICODE));

          } catch (Throwable $e) {
              $res->status(500);
              $res->header('Content-Type', 'application/json');
              $res->end(json_encode([
                  'code'    => 500,
                  'message' => '服务器内部错误',
                  // 生产环境不要暴露错误详情!
                  'debug'   => $e->getMessage(),
              ], JSON_UNESCAPED_UNICODE));
          }
      }
  }

  ---
  第七步:启动入口(最重要的文件)

  <?php
  // server.php

  // 加载所有类
  require_once __DIR__ . '/src/DbPool.php';
  require_once __DIR__ . '/src/Router.php';
  require_once __DIR__ . '/src/App.php';
  require_once __DIR__ . '/src/Controller/UserController.php';

  /**
   * 大白话解释整个启动过程:
   *
   * 1. 主进程启动(Master)
   *    └── 负责管理子进程,自己不干活
   *
   * 2. Manager进程
   *    └── 监控Worker进程,挂了就重启
   *
   * 3. Worker进程 × N个(你配置几个就几个)
   *    └── 真正干活的,每个Worker里跑协程
   *    └── 一个Worker可以同时跑成千上万个协程
   *
   * 内存布局:
   *   Worker1: [框架代码][DB连接池][协程A][协程B][协程C]...
   *   Worker2: [框架代码][DB连接池][协程A][协程B][协程C]...
   *   (每个Worker独立,不共享内存,所以不用加锁)
   */

  $server = new Swoole\Http\Server('0.0.0.0', 9501);

  $server->set([
      // Worker进程数,一般设为CPU核心数
      'worker_num'            => swoole_cpu_num(),

      // 每个Worker最多同时处理的协程数
      // 超过这个数新请求会排队
      'max_coroutine'         => 10000,

      // 每个Worker处理完这么多请求后自动重启
      // 防止内存泄漏(虽然写得好不会泄漏,但保险起见)
      'max_requests'          => 100000,

      // 开启协程化(把所有阻塞操作自动变成协程)
      // 这是关键!有了这个,普通的PDO查询也会自动协程化
      'enable_coroutine'      => true,

      // 日志
      'log_file'              => '/tmp/swoole.log',
      'log_level'             => SWOOLE_LOG_INFO,
  ]);

  /**
   * WorkerStart事件:每个Worker进程启动时执行一次
   *
   * 大白话:Worker刚出生时做的初始化工作
   * 这里初始化的东西在这个Worker的整个生命周期都存在
   * 不用每次请求都重新初始化!这就是常驻内存的核心价值
   */
  $server->on('WorkerStart', function (Swoole\Http\Server $server, int $workerId) {
      echo "Worker #{$workerId} 启动了\n";

      // 初始化数据库连接池
      // 这20个连接会一直存在,不会每次请求都新建
      DbPool::init([
          'host'     => '127.0.0.1',
          'port'     => 3306,
          'database' => 'myapp',
          'username' => 'root',
          'password' => 'your_password',
      ]);

      // 初始化应用(加载路由等)
      // 用全局变量存起来,所有请求共用这一个实例
      global $app;
      $app = new App();

      echo "Worker #{$workerId} 初始化完成,开始接受请求\n";
  });

  /**
   * Request事件:每次HTTP请求到来时触发
   *
   * 大白话:Swoole自动为每个请求创建一个协程
   * 所以这个回调函数是在协程里运行的
   * 多个请求 = 多个协程 = 并发执行
   */
  $server->on('request', function (
      Swoole\Http\Request  $request,
      Swoole\Http\Response $response
  ) {
      global $app;

      // 设置通用响应头
      $response->header('X-Powered-By', 'Swoole');
      $response->header('Access-Control-Allow-Origin', '*');

      // 处理请求(在协程里执行)
      $app->handle($request, $response);
  });

  /**
   * WorkerStop事件:Worker进程即将停止时
   * 做清理工作
   */
  $server->on('WorkerStop', function (Swoole\Http\Server $server, int $workerId) {
      echo "Worker #{$workerId} 停止了\n";
  });

  echo "服务器启动在 http://0.0.0.0:9501\n";
  echo "Worker数量: " . swoole_cpu_num() . "\n";

  $server->start();

  ---
  第八步:数据库建表

  CREATE TABLE users (
      id         INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
      name       VARCHAR(100) NOT NULL,
      email      VARCHAR(200) NOT NULL UNIQUE,
      created_at DATETIME     NOT NULL
  ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

  ---
  第九步:启动和测试

  # 启动服务器
  php server.php

  # 测试接口
  # 创建用户
  curl -X POST http://localhost:9501/users \
    -H "Content-Type: application/json" \
    -d '{"name":"张三","email":"zhangsan@example.com"}'

  # 获取列表
  curl http://localhost:9501/users?page=1

  # 获取单个
  curl http://localhost:9501/users/1

  ---
  内存里到底发生了什么(完整流程图)

  PHP server.php 启动
  │
  ├── 主进程(Master) 创建
  │   ├── Worker进程1 创建
  │   │   ├── WorkerStart触发
  │   │   ├── DbPool初始化(建20个DB连接,常驻内存)
  │   │   ├── App初始化(路由表加载,常驻内存)
  │   │   └── 开始监听请求...
  │   │
  │   └── Worker进程2 创建
  │       ├── WorkerStart触发
  │       ├── DbPool初始化(各自独立的20个连接)
  │       └── 开始监听请求...
  │
  │
  请求1到来 ──→ Worker1
  │              └── Swoole自动创建 协程A
  │                  ├── App::handle() 开始执行
  │                  ├── 路由匹配到 UserController::index
  │                  ├── DbPool::query() 借一个DB连接
  │                  ├── 执行SQL... [协程A挂起,等数据库]
  │                  │
  请求2到来 ──→ Worker1(同一个Worker!)
  │              └── Swoole自动创建 协程B
  │                  ├── App::handle() 开始执行
  │                  ├── 路由匹配到 UserController::show
  │                  ├── DbPool::query() 借另一个DB连接
  │                  ├── 执行SQL... [协程B挂起,等数据库]
  │                  │
  │              [数据库返回了协程A的结果]
  │              协程A恢复执行
  │                  ├── 拿到数据
  │                  ├── json_encode
  │                  └── response->end() 返回给客户端
  │
  │              [数据库返回了协程B的结果]
  │              协程B恢复执行
  │                  └── 同上,返回给客户端

  ---
  常见坑和解决方法

  // 坑1:全局变量在协程间共享,会互相污染
  // 错误写法:
  $GLOBALS['current_user'] = $user; // 协程A设置了,协程B读到的是A的数据!

  // 正确写法:用协程上下文(每个协程独立的存储空间)
  Swoole\Coroutine::getContext()['current_user'] = $user;
  // 每个协程有自己的context,互不干扰


  // 坑2:普通的sleep()会阻塞整个Worker进程
  // 错误写法:
  sleep(1); // 这1秒内Worker什么都干不了!

  // 正确写法:
  Swoole\Coroutine::sleep(1); // 只挂起当前协程,Worker继续处理其他请求


  // 坑3:忘记还连接回池子
  // 错误写法:
  $pdo = DbPool::borrow();
  $pdo->query('SELECT 1'); // 如果这里抛异常,连接就丢了!

  // 正确写法:用try/finally或者用DbPool::query()封装好的方法
  DbPool::query(function($pdo) {
      return $pdo->query('SELECT 1')->fetch();
  }); // 自动还,不会丢


  // 坑4:静态变量在同一Worker的所有协程间共享
  // 如果你在静态变量里存了请求相关的数据,会乱套
  // 解决:请求级别的数据用协程context存,不要用static

  ---
  性能对比(同一台机器)

  传统PHP-FPM(100并发):
    平均响应时间:~150ms
    QPS:~500
    内存:每个请求约8MB

  Swoole常驻内存+协程(100并发):
    平均响应时间:~15ms
    QPS:~8000
    内存:每个Worker约50MB(固定,不随请求增加)

  提升:响应快10倍,吞吐量高16倍,内存更可控

  ---
  一句话总结

  ▎ 传统PHP是"用完即弃的外卖餐具",每次都新建扔掉;Swoole常驻内存是"餐厅里的瓷器",洗干净反复用。协程是"一个服务员同时服
  ▎ 务多桌",等菜的时候去招呼别桌,不是傻站着等。

更多推荐