如何创建低CPU占用的PHP Cron任务?解决Messenger推送高负载问题
优化Facebook Messenger订阅者提醒Cron任务CPU高占用的方案
你的Cron任务每5分钟执行一次却把服务器CPU拉到100%,核心问题出在嵌套循环的同步阻塞操作、无索引的全表查询、低效的任务执行逻辑这几个点上。下面是针对性的优化建议,附代码修改示例:
1. 先解决并发冲突:加锁避免重复执行
如果上一次任务还没处理完(比如用户量较大时),新的任务又启动,多个进程同时运行会直接把CPU占满。给任务加个锁,确保同一时间只有一个进程在执行:
// 任务开头加锁逻辑 $lockFile = '/tmp/messenger_job.lock'; if (file_exists($lockFile)) { exit("Previous job is still running, exiting.\n"); } touch($lockFile); // 任务执行完后一定要删除锁,即使报错也要删 try { // 你的任务逻辑放在这里 } finally { if (file_exists($lockFile)) { unlink($lockFile); } }
2. 优化数据库查询:减少全表扫描+避免嵌套查询
你的代码里有两个致命的查询问题:
- 用
SELECT *拉取所有字段,浪费内存和带宽,只查需要的字段即可; - 遍历每个
subscriber时都重新查一次msnUsers,而且动态拼接的OR条件会让数据库无法使用索引,导致全表扫描。
优化方案:
- 给
msnUsers表添加复合索引,让查询能快速定位活跃且符合学历条件的用户:ALTER TABLE msnUsers ADD INDEX idx_status_edu (status, u_secondary, secondary, h_secondary, diploma, bachelor_honors, master); - 批量拉取所有数据到内存中处理,避免嵌套查询:
// 先批量拉取所有订阅者的核心数据 $subscribersStmt = $dbcon->query("SELECT id, u_secondary, secondary, h_secondary, diploma, bachelor_honors, master FROM subscriber"); $subscribers = $subscribersStmt->fetch_all(MYSQLI_ASSOC); // 批量拉取所有活跃的Messenger用户 $msnUsersStmt = $dbcon->query("SELECT msn_id, u_secondary, secondary, h_secondary, diploma, bachelor_honors, master FROM msnUsers WHERE status='1'"); $msnUsers = $msnUsersStmt->fetch_all(MYSQLI_ASSOC); // 内存中匹配符合条件的用户,避免多次数据库查询 foreach ($subscribers as $subscriber) { $targetUsers = array_filter($msnUsers, function($user) use ($subscriber) { // 匹配任意学历条件 return ($subscriber['u_secondary'] == '1' && $user['u_secondary'] == '1') || ($subscriber['secondary'] == '1' && $user['secondary'] == '1') || ($subscriber['h_secondary'] == '1' && $user['h_secondary'] == '1') || ($subscriber['diploma'] == '1' && $user['diploma'] == '1') || ($subscriber['bachelor_honors'] == '1' && $user['bachelor_honors'] == '1') || ($subscriber['master'] == '1' && $user['master'] == '1'); }); // 后续处理targetUsers... }
3. 异步发送消息:摆脱同步阻塞的CPU浪费
$botman->say()是同步操作,每发送一条消息都要等Facebook的响应,这会让进程一直处于阻塞状态,CPU空转等待。改用消息队列异步发送:
- 把需要发送的消息任务(比如
msn_id和消息内容)放到队列(比如Redis队列); - 用一个单独的守护进程(比如用Supervisor管理)来消费队列,负责实际发送消息。
示例代码(Cron任务只负责入队):
// 假设已经有Redis连接 $redis = new Redis(); $redis->connect('127.0.0.1', 6379); foreach ($targetUsers as $user) { $task = json_encode([ 'msn_id' => $user['msn_id'], 'message' => 'Message' // 注意你代码里的拼写错误:Massage → Message ]); $redis->lPush('messenger_send_queue', $task); } // 所有任务入队后,再更新订阅者的发送状态 $dbcon->query("UPDATE submit_msn SET msnStatus=3 WHERE id='{$subscriber['id']}'");
然后写一个消费者脚本consumer.php:
<?php require_once "btm-config.php"; $redis = new Redis(); $redis->connect('127.0.0.1', 6379); while (true) { // 从队列取任务,阻塞等待新任务 $task = $redis->brPop('messenger_send_queue', 0); if ($task) { $data = json_decode($task[1], true); try { $botman->say($data['message'], $data['msn_id']); // 可选:控制发送频率,避免触发Facebook限流 usleep(50000); // 0.05秒间隔 } catch (Exception $e) { error_log("Send failed for {$data['msn_id']}: " . $e->getMessage()); // 可以把失败任务重新放回队列 $redis->lPush('messenger_send_queue', $task[1]); } } }
用Supervisor配置守护这个消费者进程,这样Cron任务只需要快速把任务入队,不会占用CPU,所有发送逻辑交给后台进程处理。
4. 其他细节优化
- 降低进程优先级:把
proc_nice(10)改成proc_nice(19)(Linux下最低优先级),让系统优先处理其他业务进程; - 去掉
usleep(100000):这个延迟在Cron主进程里会浪费CPU,把频率控制放到消费者进程里即可; - 错误处理:给数据库操作和消息发送添加异常捕获,避免任务中途崩溃导致锁文件无法删除。
内容的提问来源于stack exchange,提问作者Al Amin
相关产品推荐
相关产品推荐

