ThinkPHP项目定时任务与守护进程:从入门到生产级实践指南
目录导读
- 为什么ThinkPHP项目需要定时任务与守护进程?
- 基础篇:使用Crontab实现ThinkPHP定时任务
- 进阶篇:自定义守护进程的完整实现
- 生产级方案:Supervisor与ThinkPHP的完美结合
- 常见问题与性能优化(QA)
- 总结与最佳实践
为什么ThinkPHP项目需要定时任务与守护进程?
在实际业务场景中,我们经常遇到需要周期性执行的任务:如订单超时自动关闭、用户积分结算、报表生成、邮件/短信发送队列消费等,如果依赖用户请求触发,不仅效率低下,还会造成极差的用户体验。

ThinkPHP作为国内最流行的PHP框架之一,官方提供了 think 命令行工具,但多数开发者仅停留在手动执行层面。真正的生产环境必须解决两个核心问题:
- 定时调度:在指定时间点或间隔执行特定ThinkPHP命令。
- 常驻进程:保证任务不间断执行(如队列消费者),并具备崩溃自恢复能力。
本文将综合现有最佳实践,带你从零构建一套稳定、可维护的任务体系。
基础篇:使用Crontab实现ThinkPHP定时任务
1 传统Crontab方案
Linux系统的Crontab是最简单的定时方案,假设你的ThinkPHP项目位于 /var/www/html,那么一个典型任务配置如下:
* * * * * cd /var/www/html && php think order:close >> /var/log/order_close.log 2>&1
注意点:
- 必须使用绝对路径或先进入项目目录。
- 需要为PHP设置环境变量(可在crontab顶部添加
PATH=/usr/local/bin:/usr/bin:/bin)。 - 日志重定向务必设置,否则错误信息会丢失。
2 单入口Shell脚本优化
直接写多条crontab规则容易混乱,建议封装成脚本 task.sh:
#!/bin/bash
PHP_BIN=$(which php)
PROJECT_DIR="/var/www/html"
cd $PROJECT_DIR
case $1 in
close)
$PHP_BIN think order:close ;;
report)
$PHP_BIN think report:generate ;;
*)
echo "Usage: $0 {close|report}" ;;
esac
然后在crontab中调用:* * * * * /var/www/html/task.sh close。
3 局限性
Crontab最小粒度是分钟级,且无法处理并发冲突、无法自动重试失败任务,对于要求较高的场景(如秒级轮询、队列消费),需要更高级的守护进程方案。
进阶篇:自定义守护进程的完整实现
ThinkPHP允许你完全自定义一个命令行程序作为长期运行的守护进程,这通常应用于消息队列的worker。
1 创建守护进程命令类
在 application/command 目录下创建 Worker.php:
<?php
namespace app\command;
use think\console\Command;
use think\console\Input;
use think\console\Output;
use think\console\input\Option;
class Worker extends Command
{
protected function configure()
{
$this->setName('worker:start')
->addOption('daemon', 'd', Option::VALUE_NONE, '后台运行')
->setDescription('启动队列消费者守护进程');
}
protected function execute(Input $input, Output $output)
{
$output->writeln("Worker started.");
while (true) {
// 从队列取任务 (示例为Redis)
$job = \think\facade\Cache::store('redis')->lPop('task_queue');
if ($job) {
$this->processJob($job);
} else {
usleep(100000); // 无任务时休眠0.1秒
}
}
}
private function processJob($job)
{
// 处理业务逻辑...
}
}
2 后台守护化实现
一个简单的守护化fork实现(Unix环境):
protected function execute(Input $input, Output $output)
{
if ($input->getOption('daemon')) {
$pid = pcntl_fork();
if ($pid == -1) {
die('fork failed');
} else if ($pid) {
// 父进程退出
exit(0);
}
// 子进程成为会话组长
if (posix_setsid() == -1) {
die('setsid failed');
}
// 第二次fork,防止获取终端
$pid = pcntl_fork();
if ($pid == -1) {
die('fork failed');
} else if ($pid) {
exit(0);
}
// 重定向标准输入输出
fclose(STDIN);
fclose(STDOUT);
fclose(STDERR);
$stdin = fopen('/dev/null', 'r');
$stdout = fopen('/var/log/worker.log', 'a');
$stderr = fopen('/var/log/worker_error.log', 'a');
}
// ... 业务循环
}
3 运行与停止
启动:php think worker:start -d
停止:复杂,推荐使用 Supervisor 管理生命周期。
生产级方案:Supervisor与ThinkPHP的完美结合
Supervisor 是Linux下最常用的进程管理工具,能监控进程状态,崩溃自动重启,且支持日志轮转。
1 安装与配置Supervisor
# Ubuntu/Debian apt-get install supervisor # CentOS yum install supervisor
配置 /etc/supervisor/conf.d/think-worker.conf:
[program:think_worker] command=php /var/www/html/think worker:start ; 这里不用-d,交给supervisor管理 process_name=%(program_name)s_%(process_num)02d numprocs=2 ; 启动2个进程实例,提高消费能力 directory=/var/www/html autostart=true ; 随supervisor启动 autorestart=true ; 崩溃自动重启 user=www-data ; 降低权限运行 redirect_stderr=true stdout_logfile=/var/log/think_worker.out.log stderr_logfile=/var/log/think_worker.err.log stdout_logfile_maxbytes=10MB stopasgroup=true ; 重要,确保子进程被kill killasgroup=true
2 关键配置说明
- numprocs:多进程并发,解决单进程阻塞问题。
- stopasgroup 与 killasgroup:ThinkPHP命令中可能存在子进程(如异步任务),设置后确保整个进程组被杀掉,防止僵尸进程。
3 管理命令
supervisorctl reread supervisorctl update supervisorctl status supervisorctl restart think_worker:*
优势对比:Crontab无法解决常驻进程,纯PHP自守护易内存泄漏,Supervisor提供最大可靠性。
常见问题与性能优化(QA)
Q1: 定时任务与守护进程如何区分使用?
答:
- 定时任务:适合非实时、周期性任务(如每天凌晨清缓存),用Crontab即可。
- 守护进程:适合实时、高频率任务(如消息队列消费),若任务有延迟容忍度,也可使用Crontab低频触发(但并发需加锁)。
Q2: 多个实例同时执行定时任务造成重复处理怎么办?
答:必须引入互斥锁,ThinkPHP中可使用 Cache::set($lockKey, 1, 60) 结合 Cache::has 实现原子性,或者使用Mysql GET_LOCK() 函数作为分布式锁。
Q3: ThinkPHP的 Cache::store('redis')->lPop 守护进程,如果任务执行一半崩溃怎么办?
答:这涉及任务确认机制,更安全的做法是使用 brPop 或者先 lrem 后处理,处理失败重新入队,或者引入简单的消息队列实现(如RabbitMQ)提供 ack 机制。
Q4: 守护进程内存增长,出现内存溢出?
答:长期循环运行PHP脚本,必须在每次迭代后释放大变量、关闭连接,使用 gc_collect_cycles() 强制垃圾回收,建议每处理N条任务后 die 并由Supervisor自动拉起,这是一种防内存泄漏的常见“自愈”策略。
$processed = 0;
while (true) {
$job = $redis->lPop('queue');
if ($job) {
// 处理...
if (++$processed >= 1000) {
$output->writeln("Recycling...");
exit(0); // 安全退出,supervisor自动重启
}
} else {
usleep(100000);
}
}
Q5: 如何监控守护进程的健康状态?
答:
- 利用Supervisor的
mem和cpu指标。 - 在业务代码中记录心跳到Redis(key带过期时间),监控系统定期检查。
- 使用
pm2(如果迁移Node.js)或Docker容器编排。
总结与最佳实践
- 简单定时任务:首选Crontab + Shell脚本。
- 复杂常驻任务:优先Supervisor管理
php think xxx:start。 - 业务代码应无状态:保证进程重启不产生副作用(如幂等性设计)。
- 日志必须隔离:任务日志与业务日志分开,并开启文件轮转。
- 多进程考虑资源使用:按CPU核数合理设置
numprocs。 - 测试环境预演:在使用
daemon模式前,务必在CLI前台运行验证逻辑。
通过上述方案,你的ThinkPHP项目将具备媲美Java/Go微服务的后台任务处理能力,掌握定时任务与守护进程,是PHP工程师走向高级架构的必经之路。