Workerman定时器通过Timer::add()方法实现高精度、事件循环内的周期或延时任务,支持毫秒级调度,与Cron相比精度更高、性能更好,但依赖进程存活。为避免阻塞,应拆分任务、使用Task Worker或消息队列异步处理。定时任务默认不持久化,需结合数据库或Redis存储配置,并在onWorkerStart中重新注册以实现持久化。多实例部署时,通过Redis分布式锁防止重复执行,确保高可用。混合使用Workerman定时器与Cron可兼顾实时性与系统级任务调度。

Workerman实现定时器主要通过其内置的
Timer
Timer::add()
在Workerman中,实现定时器主要依赖于
WorkermanLibTimer
add()
del()
1. 注册一个定时器:Timer::add(float $interval, callable $callback, array $args = [], bool $persistent = true)
$interval
$callback
$args
$persistent
true
false
Timer::add()
示例代码:
<?php
use WorkermanWorker;
use WorkermanLibTimer;
require_once __DIR__ . '/vendor/autoload.php';
$task = new Worker();
$task->onWorkerStart = function($worker) {
// 每2.5秒执行一次,打印当前时间
$timer_id_periodic = Timer::add(2.5, function() {
echo "周期性任务执行了,当前时间:" . date('H:i:s') . "
";
// 假设这里执行一些数据清理、状态检查等任务
});
echo "注册了一个周期性定时器,ID: " . $timer_id_periodic . "
";
// 5秒后执行一次,然后自动停止
$timer_id_once = Timer::add(5, function() {
echo "单次任务执行了,只执行一次,当前时间:" . date('H:i:s') . "
";
// 比如,延时发送一个通知,或者在某个条件满足后执行一次特定操作
}, [], false); // 注意这里的 false,表示非持久化
echo "注册了一个单次定时器,ID: " . $timer_id_once . "
";
// 假设我们想在某个时刻手动删除一个定时器
// 比如,10秒后删除上面注册的周期性定时器
Timer::add(10, function() use ($timer_id_periodic) {
if (Timer::del($timer_id_periodic)) {
echo "周期性定时器 " . $timer_id_periodic . " 已被手动删除。
";
} else {
echo "尝试删除定时器 " . $timer_id_periodic . " 失败或已不存在。
";
}
}, [], false);
};
Worker::runAll();2. 删除一个定时器:Timer::del(int $timer_id)
$timer_id
Timer::add()
true
false
通过这种方式,你可以在Workerman的任何Worker进程中灵活地创建和管理定时任务。需要注意的是,这些定时器是与当前的Worker进程绑定的,如果Worker进程重启,所有未持久化到外部存储的定时器都会丢失。
在我看来,Workerman的定时器和传统的Linux Cron任务,虽然都能实现“定时执行”的目的,但它们的设计哲学和适用场景却有着本质的区别。理解这些差异,对于我们选择合适的工具来解决具体问题至关重要。
Workerman定时器:
传统Cron任务:
我该如何选择?
在我看来,选择哪种方式,关键在于你的任务性质和对系统稳定性的要求。
这是一个非常关键的问题,也是我在实际开发中经常遇到的挑战。Workerman的Worker进程是单线程的(在PHP层面),这意味着在一个Worker进程中,任何一个任务如果执行时间过长,都会阻塞整个事件循环,导致该Worker无法处理其他客户端请求或执行其他定时任务,进而影响整个服务的响应速度和用户体验。
要避免这种阻塞,我总结了几种行之有效的方法:
1. 任务拆分与分批处理:
如果你的长时间任务可以被分解成多个小任务,那么这就是一个很好的解决方案。例如,你需要处理100万条数据,不要在一个定时器回调中一次性处理完。你可以:
示例(伪代码):
// 假设有一个全局变量或存储来记录处理进度
$currentOffset = 0;
$batchSize = 1000;
Timer::add(1, function() use (&$currentOffset, $batchSize) {
// 从数据库中获取一批数据
$data = getDataFromDB($currentOffset, $batchSize);
if (empty($data)) {
// 数据处理完毕,可以停止定时器或重置
echo "所有数据处理完毕。
";
// Timer::del($currentTimerId); // 如果需要停止
$currentOffset = 0; // 重置以便下次重新开始
return;
}
foreach ($data as $item) {
// 处理单条数据,确保这里的处理是快速的
processSingleItem($item);
}
$currentOffset += count($data);
echo "已处理到偏移量: " . $currentOffset . "
";
});2. 任务委托给独立的Worker进程(Task Worker):
Workerman本身提供了
Task
TaskWorker
TaskWorker
TaskWorker
TaskWorker
3. 引入消息队列(Message Queue):
这是处理长时间任务和高并发场景的黄金法则。当定时任务触发时,它仅仅是将一个“任务消息”推送到消息队列(如Redis List、RabbitMQ、Kafka)中,然后立即返回。接着,由独立的消费者进程(可以是另一个Workerman Worker,也可以是其他语言编写的服务)从队列中拉取消息并执行实际的耗时操作。
示例(使用Redis作为消息队列):
// 在定时器回调中
Timer::add(60, function() use ($redis) { // 假设 $redis 是一个 Redis 客户端实例
$taskData = [
'type' => 'heavy_report_generation',
'params' => ['user_id' => 123, 'date' => date('Y-m-d')]
];
$redis->rPush('heavy_task_queue', json_encode($taskData));
echo "已将报告生成任务推送到队列。
";
});
// 在另一个独立的消费者Worker进程中(或一个独立的PHP脚本)
// 循环从 'heavy_task_queue' 中 lPop 消息并处理4. pcntl_fork()
对于CPU密集型任务,理论上可以使用
pcntl_fork()
在我看来,对于大多数场景,任务拆分、Task Worker和消息队列是更安全、更推荐的解决方案。它们能有效避免Workerman主进程的阻塞,同时提供良好的可扩展性和稳定性。
Workerman的
Timer
Timer::add()
要解决这两个问题,我们需要跳出Workerman进程本身,引入外部存储和分布式协调机制。
1. 持久化定时任务:
核心思想是:将定时任务的配置信息存储在外部,并在Workerman启动时重新加载和注册。
onWorkerStart
Timer::add()
示例(概念性代码):
// 假设有一个函数从数据库加载任务
function loadScheduledTasksFromDB() {
// 模拟从数据库加载任务列表
return [
['interval' => 10, 'callback' => 'App\Tasks\CleanCache::run', 'args' => [], 'persistent' => true],
['interval' => 300, 'callback' => 'App\Tasks\GenerateReport::run', 'args' => ['type' => 'daily'], 'persistent' => true],
];
}
$worker = new Worker();
$worker->onWorkerStart = function($worker) {
$tasks = loadScheduledTasksFromDB();
foreach ($tasks as $taskConfig) {
$callback = $taskConfig['callback'];
// 这里需要动态解析回调函数,例如通过反射或简单的类方法调用
$callable = function() use ($callback, $taskConfig) {
list($className, $methodName) = explode('::', $callback);
(new $className())->$methodName(...$taskConfig['args']);
};
Timer::add($taskConfig['interval'], $callable, [], $taskConfig['persistent']);
echo "从数据库注册了任务: " . $callback . "
";
}
};2. 实现高可用(避免重复执行与单点故障):
当你有多个Workerman进程(甚至多个服务器上的Workerman实例)都在运行相同的定时任务时,就可能出现重复执行的问题。高可用性要求即使某个进程或服务器挂掉,任务也能被其他健康的实例接管。
SETNX
示例(Redis分布式锁):
use WorkermanLibTimer;
use Redis; // 假设你已经配置好了 Redis 客户端
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
// ... 在 Worker::onWorkerStart 中注册定时器
Timer::add(60, function() use ($redis) {
$lockKey = 'lock:task:generate_report';
$lockValue = uniqid(); // 唯一的锁值,用于防止误删
$expireTime = 55; // 锁的过期时间,略小于定时器间隔
// 尝试获取锁:SET lock_key unique_value NX EX expire_time
if ($redis->set($lockKey, $lockValue, ['nx', 'ex' => $expireTime])) {
echo "成功获取锁,开始执行报告生成任务...
";
try {
// 这里执行实际的耗时任务
// generateDailyReport();
sleep(10); // 模拟任务执行
echo "报告生成任务完成。
";
} catch (Exception $e) {
echo "任务执行失败: " . $e->getMessage() . "
";
} finally {
// 确保只有自己设置的锁才能被自己释放
if ($redis->get($lockKey) === $lockValue) {
$redis->del($lockKey);
echo "锁已释放。
";
}
}
} else {
echo "未能获取锁,任务已被其他实例执行或正在执行中。
";
}
});以上就是Workerman如何实现定时器?Workerman定时任务怎么写?的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号