前置閱讀:Hyperf/Crontab使用文件
前置閱讀:Hyperf事件機制
寫在開頭
之前做專案用到了Hyperf/Crontab元件來進行秒級的資料清洗,最近又在做定時任務的拆分,於是就打算過一遍元件原始碼加深理解,順便構思一下如何在此基礎上搭建Hyperf/Crontab的任務排程功能。Crontab本質上是一個隨Server啟動的自定義程式,所以接下來我們將從啟動和執行兩個階段來進行介紹。
定時器的啟動
由於Crontab是一個隨著Server啟動的程式,分析他的生命週期肯定會設計到框架的啟動。但本文不是主要介紹Hyperf框架啟動原始碼的,所以我們會簡單的過一下涉及到自定義程式啟動的框架啟動程式碼。
當我們使用命令 php bin/hyperf.php start 啟動hyperf時
$application = $container->get(\Hyperf\Contract\ApplicationInterface::class);
在入口檔案 hyperf.php 框架會例項化一個 Hyperf\Framework\ApplicationFactory 例項,同時會掃描一遍所有帶 @Command 註解或者定義了 commands 配置的地方,例項化一個Symfony\Component\Console\Application物件 (這是一個Symfony的Consoul命令類用於定義命令觸發執行的任務) 。將所有被定義為command的物件類註冊到$application物件中,最後在入口檔案執行
$application->run();
執行所有的Command命令,其中包括 Hyperf\Server\Command\StartServer 服務Server啟動類。在這個類裡面定義瞭如果接收到的命令引數包含 start 那麼就會執行他的邏輯,即根據 config/autoload/server.php 的配置例項化Hyperf\Server\Server類,在這個過程中會觸發BeforeMainServerStart事件,到這裡我們即將進入自定義程式啟動的核心階段。
$serverProcesses = $serverConfig['processes'] ?? [];
$processes = $this->config->get('processes', []);
$annotationProcesses = $this->getAnnotationProcesses();
// Retrieve the processes have been registered.
$processes = array_merge($serverProcesses, $processes, ProcessManager::all(), array_keys($annotationProcesses));
foreach ($processes as $process) {
...
if ($instance instanceof ProcessInterface) {
$instance->isEnable() && $instance->bind($server);
}
}
BootProcessListener監聽到BeforeMainServerStart事件的觸發會拿到所有 @Process 和 定義在processes配置檔案的程式類,執行他們的 isEnable 和 bind 方法。
定時器的執行
在上面我們對自定義程式是如何隨框架啟動的進行了簡單的介紹,接下來我們對本文的主角Crontab自定義程式進行解析。
Hyperf文件介紹在使用Crontab之前需要在 config/autoload/processes.php 內註冊一下 Hyperf\Crontab\Process\CrontabDispatcherProcess 自定義程式。那麼我們就直接來看一下這個process類裡面做了什麼事情。
如果你對 Hyperf/Process 元件使用熟悉的話會知道,一個process類主要執行的邏輯都在 handle() 方法
內。
public function handle(): void
{
$this->event->dispatch(new CrontabDispatcherStarted());
while (true) {
$this->sleep();
$crontabs = $this->scheduler->schedule();
while (! $crontabs->isEmpty()) {
$crontab = $crontabs->dequeue();
$this->strategy->dispatch($crontab);
}
}
}
在handle內首先觸發來一個 CrontabDispatcherStarted 事件,目前這個事件無人監聽。接下來就是一段時間的協程阻塞,阻塞時間第一次為距離下一次整分鐘的秒數,其餘的都是60s。關於為什麼要使用 \Swoole\Coroutine::sleep 而不是直接 sleep() ,是因為自定義程式預設是一個協程的Server。
接下來
$crontabs = $this->scheduler->schedule();
返回一個當前這一分鐘該執行的SplQueue佇列,佇列中的是Crontab物件
object(Hyperf\Crontab\Crontab)#46164 (10) {
["name":protected]=>
string(4) "Foo4"
["type":protected]=>
string(8) "callback"
["rule":protected]=>
string(11) "* * * * * *"
["singleton":protected]=>
bool(false)
["mutexPool":protected]=>
string(7) "default"
["mutexExpires":protected]=>
int(3600)
["onOneServer":protected]=>
bool(true)
["callback":protected]=>
array(2) {
[DEBUG] Event Hyperf\Framework\Event\OnPipeMessage handled by Hyperf\Crontab\Listener\OnPipeMessageListener listener.
[0]=>
string(16) "App\Task\FooTask"
[1]=>
string(7) "execute"
}
["memo":protected]=>
NULL
["executeTime":protected]=>
object(Carbon\Carbon)#46106 (3) {
["date"]=>
string(26) "2020-06-02 14:15:57.000000"
["timezone_type"]=>
int(3)
["timezone"]=>
string(13) "Asia/Shanghai"
}
}
這個物件中記錄了我們目前比較關注的兩個關鍵資訊:
1. 執行callback
2. 執行時間
關於這個crontab物件是如何生成的,這個我們在後面會介紹到。
在我們拿到crontab物件後,我們會把物件傳送給在 config/autoload/dependencies.php 定義好的 StrategyInterface 的實現類來進行dispatch,預設指定的是Worker 程式執行策略。
$server = $this->serverFactory->getServer()->getServer();
if ($server instanceof Server && $crontab->getExecuteTime() instanceof Carbon) {
$workerId = $this->getNextWorkerId($server);
$server->sendMessage(new PipeMessage(
'callback',
[Executor::class, 'execute'],
$crontab
), $workerId);
}
除Coroutine策略外的dispatch方法是相同的,都是程式間輪訓的向WorkID通過 sendMessage 傳送 PipeMessage 物件,同時 sendMessage 方法會觸發 OnPipeMessage 事件。該事件被 OnPipeMessageListener 監聽,會根據 PipeMessage 執行對應的callback函式,即 Executor->execute’ 。在該方法中會根據corntab物件的屬性定義 Swoole\Timer::after ,根據corontab的executeTime屬性定義多少秒後執行callback。
$callback && Timer::after($diff > 0 ? $diff * 1000 : 1, $callback);
這樣基本就完成了我們一個秒級定時任務的執行。
現在我們回到上面那個關於crontab物件是什麼時候生成的問題。我們說過Cornrab本質上就是一個自定義程式,那根據Hyperf/Process的使用說明,所有的自定義程式都繼承了 Hyperf\Process\AbstractProcess ,這個類在啟動SwooleProcess時會觸發 BeforeProcessHandle 事件,在這個事件中會掃描所有的crontab配置和註解,將這些註解進行解析生成crontab物件,儲存在crontabs屬性中。
public function register(Crontab $crontab): bool
{
if (! $this->isValidCrontab($crontab)) {
return false;
}
$this->crontabs[$crontab->getName()] = $crontab;
return true;
}
以上大致就是一個crontab定時任務的執行流程,當然裡面還有很多執行細節和Contab個性化的定義引數由於篇幅有限我們還沒來得及介紹,感興趣的同學可以私下進行閱讀,下面我也附上來Hyperf/Crontab的整體執行流程類之間的關係圖,方便大家對照著閱讀原始碼。
寫在最後
Hyperf 是基於 Swoole 4.4+ 實現的高效能、高靈活性的 PHP 協程框架,內建協程伺服器及大量常用的元件,效能較傳統基於 PHP-FPM 的框架有質的提升,提供超高效能的同時,也保持著極其靈活的可擴充套件性,標準元件均基於 PSR 標準 實現,基於強大的依賴注入設計,保證了絕大部分元件或類都是 可替換 與 可複用 的。
框架元件庫除了常見的協程版的 MySQL 客戶端、Redis 客戶端,還為您準備了協程版的 Eloquent ORM、WebSocket 服務端及客戶端、JSON RPC 服務端及客戶端、GRPC 服務端及客戶端、Zipkin/Jaeger (OpenTracing) 客戶端、Guzzle HTTP 客戶端、Elasticsearch 客戶端、Consul 客戶端、ETCD 客戶端、AMQP 元件、Apollo 配置中心、阿里雲 ACM 應用配置管理、ETCD 配置中心、基於令牌桶演算法的限流器、通用連線池、熔斷器、Swagger 文件生成、Swoole Tracker、Blade 和 Smarty 檢視引擎、Snowflake 全域性ID生成器 等元件,省去了自己實現對應協程版本的麻煩。
Hyperf 還提供了 基於 PSR-11 的依賴注入容器、註解、AOP 面向切面程式設計、基於 PSR-15 的中介軟體、自定義程式、基於 PSR-14 的事件管理器、Redis/RabbitMQ 訊息佇列、自動模型快取、基於 PSR-16 的快取、Crontab 秒級定時任務、Translation 國際化、Validation 驗證器 等非常便捷的功能,滿足豐富的技術場景和業務場景,開箱即用。
本作品採用《CC 協議》,轉載必須註明作者和本文連結