CodeIgniter本身不提供异步任务机制,必须手动构建Redis队列+CLI守护进程;exec()或pcntl_fork()在控制器中“假装异步”会因请求生命周期短、FPM禁用扩展、子进程被终止、无错误捕获等问题导致任务丢失或崩溃。

CodeIgniter 本身不提供异步任务机制,必须手动构建轻量级队列 + 守护进程组合;直接用 $this->redis->lpush() 或 exec("nohup php job.php &") 都会掉坑里。
为什么不能用 exec() 或 pcntl_fork() 在控制器里“假装异步”
CI 请求生命周期短(通常几十毫秒),exec("php job.php &") 启动的子进程会被父进程退出时一并终止(尤其在 Apache/FPM 下);pcntl_fork() 在 FPM 环境下被禁用,且无法跨请求共享状态。更危险的是:没做错误捕获时,后台脚本崩溃无声无息,日志都留不下。
- Apache mod_php 下 fork 可能触发 SIGPIPE 导致整个 worker crash
- FPM 默认关闭
pcntl扩展,开启后仍无法保证子进程存活超过请求周期 - 用
exec()启动的脚本若依赖 CI 的$this->load或 session,运行时直接 fatal error
Redis 队列 + CLI Worker 是最稳的落地方式
核心是分离“投递”和“执行”:控制器只负责序列化任务并 LPUSH 到 Redis 列表,真正干活的是常驻的 CLI 进程。这个方案不依赖扩展、兼容 CI3/CI4、故障隔离清晰。
- 任务结构必须含
class、method、args、attempts、max_attempts字段,全部 JSON 序列化(禁止传Closure或 resource) - Worker 脚本开头加
set_time_limit(0)和ignore_user_abort(TRUE),否则超时或用户关页面就中断 - 用
BLPOP queue:default 30阻塞读取,比轮询LPOP节省 CPU,但一个进程只能监听一个队列名 - 失败重试别直接
LPUSH回原队列——要先INCR attempts,再判断是否超max_attempts,否则无限循环
RabbitMQ 方案适合需要强可靠性的场景
如果任务丢不得(比如支付回调通知、财务对账),RabbitMQ 比 Redis 更合适,但集成成本高一倍。关键不是装插件,而是三个地方必须设对:
- 声明队列时
durable => true(CI 调用$channel->queue_declare()第三个参数) - 发消息时
delivery_mode => 2(否则重启 RabbitMQ,消息全丢) - 消费端必须手动
$channel->basic_ack($delivery_info['delivery_tag']),不能靠 auto-ack - 生产者代码外层包
try/catch,捕获AMQPConnectionException,否则网络抖动导致整个 HTTP 请求 500
前端触发的“伪异步”其实只是 AJAX,不是真后台任务
像点击分类加载新闻、上传文件后生成缩略图这类需求,本质是「减少页面刷新」,不是「后台长时间运行」。这类完全不需要队列,直接走 CI 的 AJAX 接口就行:
- 控制器方法返回
json_encode($data),header 设为Content-Type: application/json - 前端用
fetch()或$.ajax()拿到数据后,用 JS 动态插入 DOM - 如果操作耗时(如生成 PDF),才需要配合队列——此时前端应返回 “任务已提交,ID: xxx”,再轮询
/api/task/status/xxx
最容易被忽略的是:所有 CLI Worker 进程必须用 nohup php application/cli/queue_worker.php >> /var/log/ci-worker.log 2>&1 & 启动,并配 systemd 或 supervisor 管理重启;没人盯着的日志文件和失控的进程,半年后准出事。


















