如何在Laravel中自定义Cron以按预设时间执行任务?
问题描述
我创建了一张名为schedule_demo的数据表,结构如下:
id name schedule_date tag status 1 test_1 2023-01-28 09:16:12 test_1_sch 1 2 test_2 2023-01-28 11:30:30 test_2_sch 1 3 test_3 2023-01-28 14:50:20 test_3_sch 1
在Commands/ScheduleDemo.php中编写了命令:
protected $signature = 'run:scheduleDemo'; public function handle() { $scheduleData = ScheduleDemo::where('status', '=', 1) ->where('scheduleDate', '=', strtotime('now'))->get(); if (count($scheduleData) > 0) { foreach ($scheduleData as $value) { $response = $this->client->request('POST', 'https://example.com', [ 'headers' => [ 'Content-Type' => 'application/x-www-form-urlencoded', ], 'form_params' => ['from' => $value->name, 'tag' => $value->tag] ]); $response = json_decode($response->getBody()->getContents()); if ($response && $response['error'] == 0) { $value->status = 2; $value->save(); } else { echo "\nFAILED\n"; } } } else { echo "\nSCHEDULE IS EMPTY\n"; } }
在Kernel.php中配置了任务调度:
protected $commands = [ 'App\Console\Commands\ScheduleDemo' ]; protected function schedule(Schedule $schedule) { $schedule->command('run:scheduleDemo') ->everyFiveMinutes() ->withoutOverlapping() ->runInBackground(); }
同时在crontab中添加了:
* * * * * php /job/project/artisan schedule:run >> /dev/null 2>&1
目前Cron任务每5分钟执行一次并成功调用API,但我希望改为当schedule_date等于当前时间时自动触发任务,而非固定间隔执行。例如当schedule_date为2023-01-28 09:16:12且等于当前时间时,自动执行任务。请问是否有实现方法?
实现方案
要实现按schedule_date精准触发任务,核心是让调度器每分钟检查一次,在命令内部匹配当前时间对应的任务,具体调整如下:
1. 调整调度执行频率
修改Kernel.php中的调度配置,改为每分钟执行一次,确保能精准检查到每分钟的任务:
protected function schedule(Schedule $schedule) { $schedule->command('run:scheduleDemo') ->everyMinute() // 改为每分钟执行 ->withoutOverlapping() ->runInBackground(); }
crontab配置保持* * * * *不变,保证Laravel调度器每分钟都能启动检查。
2. 修复时间匹配逻辑
原代码中strtotime('now')精确到秒,容易因为秒数差异错过任务,需按分钟级匹配:
场景1:schedule_date是datetime类型(数据库存储为时间字符串)
直接用数据库函数匹配当前分钟:
$scheduleData = ScheduleDemo::where('status', 1) ->whereRaw("DATE_FORMAT(schedule_date, '%Y-%m-%d %H:%i') = DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i')") ->get();
场景2:scheduleDate是时间戳字段
将当前时间取整到分钟,再匹配:
// 取当前分钟的起始时间戳(去掉秒数) $currentMinute = floor(time() / 60) * 60; $scheduleData = ScheduleDemo::where('status', 1) ->where('scheduleDate', $currentMinute) ->get();
3. 优化并发冲突问题
为避免同一任务被多个进程重复执行,增加原子性状态更新:
foreach ($scheduleData as $value) { // 原子更新状态为处理中,防止并发执行 $updated = ScheduleDemo::where('id', $value->id) ->where('status', 1) ->update(['status' => 3]); // 3代表处理中 if (!$updated) { continue; // 已被其他进程处理,跳过 } // 调用API逻辑 try { $response = $this->client->request('POST', 'https://example.com', [ 'headers' => [ 'Content-Type' => 'application/x-www-form-urlencoded', ], 'form_params' => ['from' => $value->name, 'tag' => $value->tag] ]); $responseData = json_decode($response->getBody()->getContents()); if ($responseData && $responseData->error == 0) { ScheduleDemo::where('id', $value->id)->update(['status' => 2]); // 执行成功 } else { echo "\nFAILED: {$value->name}\n"; ScheduleDemo::where('id', $value->id)->update(['status' => 1]); // 失败重置状态 } } catch (\Exception $e) { echo "\nERROR: {$value->name} - {$e->getMessage()}\n"; ScheduleDemo::where('id', $value->id)->update(['status' => 1]); // 异常重置状态 } }
注意:原代码中json_decode默认返回对象,$response['error']会报错,需改为$responseData->error。
4. 可选:用队列异步处理API调用
如果API耗时较长,建议将执行逻辑放到队列,提升调度器效率:
// 在handle方法中 foreach ($scheduleData as $value) { $updated = ScheduleDemo::where('id', $value->id) ->where('status', 1) ->update(['status' => 3]); if ($updated) { dispatch(new CallScheduleApiJob($value)); // 自定义队列任务 } }
之后创建CallScheduleApiJob类,在handle方法中编写API调用逻辑即可。
内容的提问来源于stack exchange,提问作者Miedkes
相关产品推荐
相关产品推荐

