<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	xmlns:series="https://publishpress.com/"
	>

<channel>
	<title>cronjob Archives - Tomoshare</title>
	<atom:link href="https://blog.tomosia.com.vn/tag/cronjob/feed/" rel="self" type="application/rss+xml" />
	<link>https://blog.tomosia.com.vn/tag/cronjob/</link>
	<description>Kênh chia sẻ kiến thức Tomosia Việt Nam</description>
	<lastBuildDate>Thu, 25 Dec 2025 09:28:55 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9.4</generator>

<image>
	<url>https://blog.tomosia.com.vn/wp-content/uploads/2023/09/cropped-icon-32x32.png</url>
	<title>cronjob Archives - Tomoshare</title>
	<link>https://blog.tomosia.com.vn/tag/cronjob/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Laravel Scheduler: Tư duy thiết kế, trade-off và best practices cho hệ thống lớn</title>
		<link>https://blog.tomosia.com.vn/laravel-scheduler-tu-duy-thiet-ke-trade-off-va-best-practices-cho-he-thong-lon/</link>
					<comments>https://blog.tomosia.com.vn/laravel-scheduler-tu-duy-thiet-ke-trade-off-va-best-practices-cho-he-thong-lon/#respond</comments>
		
		<dc:creator><![CDATA[hoa nguyen]]></dc:creator>
		<pubDate>Thu, 25 Dec 2025 09:26:41 +0000</pubDate>
				<category><![CDATA[PHP]]></category>
		<category><![CDATA[php]]></category>
		<category><![CDATA[Laravel]]></category>
		<category><![CDATA[Scheduler]]></category>
		<category><![CDATA[cronjob]]></category>
		<guid isPermaLink="false">https://blog.tomosia.com.vn/?p=3967</guid>

					<description><![CDATA[<p>Nếu bạn đã đi làm vài năm, bạn sẽ thấy &#8220;Scheduler&#8221; không phải chuyện&#160;viết vài dòng&#160;dailyAt(). Nó là&#160;bài&#8230;</p>
<p>The post <a href="https://blog.tomosia.com.vn/laravel-scheduler-tu-duy-thiet-ke-trade-off-va-best-practices-cho-he-thong-lon/">Laravel Scheduler: Tư duy thiết kế, trade-off và best practices cho hệ thống lớn</a> appeared first on <a href="https://blog.tomosia.com.vn">Tomoshare</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>Nếu bạn đã đi làm vài năm, bạn sẽ thấy &#8220;Scheduler&#8221; không phải chuyện&nbsp;<em>viết vài dòng&nbsp;<code>dailyAt()</code></em>. Nó là&nbsp;<strong>bài toán reliability + performance + observability</strong>. Sai một ly là: trùng charge, spam notify, kẹt DB lúc 9h, hoặc &#8220;đêm qua không chạy mà không ai biết&#8221;.</p>



<p>Bài này chia sẻ theo góc nhìn hệ thống lớn: <strong>ra quyết định kỹ thuật</strong> khi số job tăng nhanh, dữ liệu lớn, nhiều worker, CI/CD liên tục, thậm chí realtime.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="1-nguyên-tắc-số-1-scheduler-phải-nhẹ--việc-nặng-đẩy-qua-queue"><span id="1-nguyen-tac-so-1-scheduler-phai-nhe-viec-nang-day-qua-queue">1. Nguyên tắc số 1: Scheduler phải &#8220;nhẹ&#8221; — việc nặng đẩy qua queue</span></h2>



<p><strong>Tư duy</strong>: Scheduler là &#8220;orchestrator&#8221;, không phải &#8220;executor&#8221;.</p>



<p>Scheduler chỉ làm 2 việc:</p>



<ul class="wp-block-list">
<li>Quyết định &#8220;đúng giờ thì trigger cái gì&#8221;</li>



<li>Dispatch nhanh, ghi dấu vết (batch metadata)</li>
</ul>



<p>Việc nặng (DB scan, gọi API, gửi notify hàng loạt, reconcile, billing…) → chạy trong&nbsp;<strong>queued jobs</strong>&nbsp;+ nhiều worker.</p>



<p><strong>Trade-off</strong>:</p>



<ul class="wp-block-list">
<li>✅ Ưu: Scale bằng worker, retry/backoff chuẩn, tách tải khỏi scheduler</li>



<li>⚠️ Nhược: Job có thể chạy trễ → phải thiết kế theo &#8220;batch time&#8221; (xem phần 4)</li>
</ul>



<p><strong>Sai lầm thường gặp</strong>: Nhét logic nặng vào scheduler (closure/command) rồi &#8220;đỡ nghẽn&#8221; bằng cách tăng CPU. Đến lúc 100 lịch/ngày thì scheduler thành single-point bottleneck.</p>



<p><strong>Ví dụ</strong>:</p>



<pre class="wp-block-code"><code>&lt;?php

// routes/console.php
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schedule;

// ❌ SAI: Logic nặng trực tiếp trong scheduler
Schedule::call(function () {
    DB::table('recent_users')->chunk(100, function ($users) {
        foreach ($users as $user) {
            // Process payment, send email, update DB...
            // Nếu có 1000 users, scheduler sẽ bị block rất lâu
        }
    });
})->dailyAt('09:00');

// ✅ ĐÚNG: Scheduler chỉ dispatch job
Schedule::job(new ProcessRecentUsersJob)
    ->dailyAt('09:00');</code></pre>



<p>Scheduler chỉ trigger đúng giờ, không bao giờ chạy logic business trực tiếp. Worker sẽ xử lý job trong queue.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="2-đúng-một-lần-theo-nghĩa-business--đúng-một-lần-theo-nghĩa-kỹ-thuật"><span id="2-dung-mot-lan-theo-nghia-business-dung-mot-lan-theo-nghia-ky-thuat">2. &#8220;Đúng một lần&#8221; theo nghĩa business &gt; &#8220;đúng một lần&#8221; theo nghĩa kỹ thuật</span></h2>



<p>Ở hệ thống lớn, &#8220;at least once&#8221; là mặc định. Scheduler/queue/worker/retry/deploy đều có thể làm job chạy lại.</p>



<p><strong>Best practice</strong>: Thiết kế job theo&nbsp;<strong>idempotent</strong>:</p>



<ul class="wp-block-list">
<li>Chạy 2 lần vẫn ra kết quả đúng</li>



<li>Side-effect (charge/send/update) phải có khóa logic hoặc unique key</li>
</ul>



<p>Dùng lock ở&nbsp;<strong>đúng tầng</strong>:</p>



<ul class="wp-block-list">
<li>Tầng scheduler: Chống chồng / chạy một server (khi HA)</li>



<li>Tầng job/business: Lock theo entity/time-window (mới là thứ bảo vệ cuối)</li>
</ul>



<p><strong>Trade-off</strong>:</p>



<ul class="wp-block-list">
<li>Lock nhiều quá → Giảm throughput, dễ nghẽn nếu lock sai granularity</li>



<li>Lock ít quá → Trùng dữ liệu/side-effect</li>
</ul>



<p><strong>Sai lầm thường gặp</strong>: Tin tuyệt đối vào &#8220;chống chồng&#8221; của scheduler rồi bỏ qua idempotency. Một ngày deploy &#8220;đúng lúc&#8221;, bạn sẽ hiểu.</p>



<p><strong>Ví dụ</strong>:</p>



<p>Job có idempotency key dựa trên&nbsp;<code>entity_id + batch_time</code>. Concept này áp dụng cho mọi job có side-effect quan trọng. Trong scheduler, bạn chỉ dispatch job với metadata:</p>



<pre class="wp-block-code"><code>&lt;?php

// routes/console.php
use Illuminate\Support\Facades\Schedule;

Schedule::job(new ProcessOrdersJob)
    ->dailyAt('09:00');</code></pre>



<p>Job sẽ tự implement idempotency check trong&nbsp;<code>handle()</code>&nbsp;method để đảm bảo không xử lý trùng.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="3-khi-nào-dùng-runinbackground-và-khi-nào-bắt-buộc-dùng-queue"><span id="3-khi-nao-dung-runinbackground-va-khi-nao-bat-buoc-dung-queue">3. Khi nào dùng&nbsp;<code>runInBackground()</code>&nbsp;và khi nào &#8220;bắt buộc&#8221; dùng queue?</span></h2>



<p>Tôi coi&nbsp;<code>runInBackground()</code>&nbsp;là&nbsp;<strong>giải pháp tactical</strong>, không phải chiến lược lâu dài.</p>



<p><strong>Dùng&nbsp;<code>runInBackground()</code>&nbsp;khi</strong>:</p>



<ul class="wp-block-list">
<li>Task là command/exec tương đối ngắn</li>



<li>Không cần scale theo tải</li>



<li>Bạn chỉ cần tránh scheduler bị giữ process quá lâu</li>
</ul>



<p><strong>Bắt buộc đưa qua queue khi</strong>:</p>



<ul class="wp-block-list">
<li>Task có thể chạy vài chục giây/phút</li>



<li>Task có thể &#8220;nở&#8221; theo dữ liệu (data grows)</li>



<li>Task có side-effect quan trọng (payment/notify hàng loạt)</li>



<li>Cần retry/backoff, rate-limit, phân ưu tiên</li>
</ul>



<p><strong>Sai lầm thường gặp</strong>: &#8220;Task đang lâu → thêm&nbsp;<code>runInBackground()</code>&#8221; và coi như xong. Thực tế bạn chỉ chuyển nghẽn từ scheduler sang CPU/DB/connections của cùng máy.</p>



<p><strong>Ví dụ</strong>:</p>



<pre class="wp-block-code"><code>&lt;?php

// routes/console.php
use Illuminate\Support\Facades\Schedule;

// ✅ Dùng runInBackground cho command nhẹ, không cần scale
Schedule::command('cache:clear')
    ->hourly()
    ->runInBackground();

Schedule::command('emails:send')
    ->dailyAt('02:00')
    ->runInBackground();

// ✅ Bắt buộc dùng queue cho job có side-effect quan trọng
Schedule::job(new SendOrderConfirmationJob)
    ->dailyAt('09:00'); // Không có runInBackground, vì đã là queued job</code></pre>



<p>Chỉ có 2 loại command dùng&nbsp;<code>runInBackground()</code>:</p>



<ol class="wp-block-list">
<li><strong>Maintenance commands</strong> (cleanup, cache) — không ảnh hưởng business</li>



<li><strong>Monitoring commands</strong> — cần chạy nhanh, không cần retry</li>
</ol>



<p>Tất cả job business đều qua queue để có retry/backoff/scale.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="4-bài-toán-lớn-nhất-của-hệ-queue-job-chạy-trễ--lệch-cửa-sổ-dữ-liệu"><span id="4-bai-toan-lon-nhat-cua-he-queue-job-chay-tre-%e2%86%92-lech-cua-so-du-lieu">4. Bài toán lớn nhất của hệ queue: Job chạy trễ → lệch cửa sổ dữ liệu</span></h2>



<p>Đây là điểm phân biệt hệ &#8220;chạy được&#8221; và hệ &#8220;chạy đúng&#8221;.</p>



<p><strong>Vấn đề</strong>:</p>



<ul class="wp-block-list">
<li>Scheduler trigger 09:00, worker chạy 09:04</li>



<li>Nếu job query theo <code>now()</code> lúc 09:04, bạn vừa:
<ul class="wp-block-list">
<li>Miss dữ liệu 09:00–09:04 (tùy window)</li>



<li>Hoặc duplicate với batch trước</li>
</ul>
</li>
</ul>



<p><strong>Giải pháp</strong>: Scheduler&nbsp;<strong>đóng dấu batch time</strong>&nbsp;ngay lúc dispatch (tôi hay gọi&nbsp;<code>scheduledAt</code>/<code>batchAt</code>). Job luôn dùng&nbsp;<code>scheduledAt</code>&nbsp;để:</p>



<ul class="wp-block-list">
<li>Tính window dữ liệu</li>



<li>Log/audit theo batch</li>



<li>Tạo idempotency key</li>
</ul>



<p><strong>Trade-off</strong>:</p>



<ul class="wp-block-list">
<li>Tốn thêm metadata, thêm &#8220;kỷ luật&#8221; trong code</li>



<li>Đổi lại: Batch chạy trễ vẫn <strong>đúng cửa sổ</strong>, dễ đối soát, dễ replay theo batch</li>
</ul>



<p><strong>Ví dụ — Pattern batch timestamp</strong>:</p>



<p>Concept: Scheduler đóng dấu batch time ngay lúc dispatch. Job nhận batch time và dùng để tính time window, không dùng&nbsp;<code>now()</code>.</p>



<p><strong>Sử dụng trong Laravel 12</strong>:</p>



<pre class="wp-block-code"><code>&lt;?php

// routes/console.php
use Illuminate\Support\Facades\Schedule;

Schedule::job(new ProcessOrdersJob)
    ->dailyAt('09:00');</code></pre>



<p>Job sẽ nhận&nbsp;<code>scheduledAt</code>&nbsp;từ scheduler event và dùng để query đúng time window.</p>



<p><strong>Ví dụ với sub-minute scheduling trong Laravel 12</strong>:</p>



<p>Theo&nbsp;<a href="https://laravel.com/docs/12.x/scheduling#sub-minute-scheduled-tasks">Laravel 12.x Sub-Minute Scheduled Tasks</a>:</p>



<pre class="wp-block-code"><code>&lt;?php

// routes/console.php
use Illuminate\Support\Facades\Schedule;

// Laravel 12 hỗ trợ sub-minute scheduling
Schedule::call(function () {
    // Process tasks every 30 seconds
})->everyThirtySeconds();</code></pre>



<p><strong>Lợi ích</strong>:</p>



<ul class="wp-block-list">
<li>Job chạy trễ 3 phút vẫn query đúng cửa sổ 09:00–09:01</li>



<li>Dễ đối soát: &#8220;Batch 09:00 đã xử lý bao nhiêu records?&#8221;</li>



<li>Dễ replay: &#8220;Chạy lại batch 09:00&#8221; → chỉ cần tạo job với <code>jobCreatedAt = 09:00</code></li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="5-tách-workload-shape-phân-queue-theo-ưu-tiên-tránh-job-rác-ăn-tài-nguyên-job-sống-còn"><span id="5-tach-workload-shape-phan-queue-theo-uu-tien-tranh-job-rac-an-tai-nguyen-job-song-con">5. Tách &#8220;workload shape&#8221;: Phân queue theo ưu tiên, tránh job &#8220;rác&#8221; ăn tài nguyên job &#8220;sống còn&#8221;</span></h2>



<p>Một hệ thống lớn sẽ có job kiểu:</p>



<ul class="wp-block-list">
<li><strong>Critical</strong>: Billing, payment, cancel, inventory, security</li>



<li><strong>Default</strong>: Vận hành</li>



<li><strong>Low</strong>: Marketing, remind, digest</li>
</ul>



<p><strong>Best practice</strong>: Tách queue theo priority, scale worker theo queue. Đặt rule: Job low không bao giờ được làm chậm job critical.</p>



<p><strong>Trade-off</strong>:</p>



<ul class="wp-block-list">
<li>Nhiều queue → Config vận hành phức tạp hơn (supervisor/horizon)</li>



<li>Nhưng nếu không tách, bạn sẽ gặp kiểu &#8220;19h campaign bắn notify&#8221; làm nghẽn payment</li>
</ul>



<p><strong>Lưu ý</strong>: Trong scheduler, bạn có thể chỉ định queue khi schedule job:</p>



<p>Theo&nbsp;<a href="https://laravel.com/docs/12.x/scheduling#scheduling-queued-jobs">Laravel 12.x Scheduling Queued Jobs</a>, method&nbsp;<code>job()</code>&nbsp;nhận queue name làm tham số thứ 2:</p>



<pre class="wp-block-code"><code>&lt;?php

// routes/console.php
use Illuminate\Support\Facades\Schedule;

Schedule::job(new SendOrderConfirmationJob, 'critical')
    ->dailyAt('09:00');</code></pre>



<p>Hoặc nếu job class đã có property&nbsp;<code>$queue</code>, scheduler sẽ tự động dùng queue đó.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="6-thundering-herd-lúc-tròn-giờ-kẻ-thù-của-db-và-api"><span id="6-thundering-herd-luc-tron-gio-ke-thu-cua-db-va-api">6. &#8220;Thundering herd&#8221; lúc tròn giờ: Kẻ thù của DB và API</span></h2>



<p>Hệ thống lớn rất hay bị &#8220;dồn lịch&#8221; vào 00:00, 09:00, 12:00, 19:00…</p>



<p><strong>Best practice</strong>:</p>



<ul class="wp-block-list">
<li><strong>Stagger</strong>: Dàn đều theo phút/giây</li>



<li>Nếu bắt buộc tròn giờ: Chia nhỏ thành nhiều job chunk, có rate-limit/backoff</li>



<li>Không quét toàn bảng: Dùng cursor/chunk theo index, watermark theo batch</li>
</ul>



<p><strong>Sai lầm thường gặp</strong>: Một job &#8220;tổng hợp cuối ngày&#8221; chạy 00:00 quét cả bảng 50 triệu dòng. Đêm đó DB chết, sáng không ai biết vì không có alert.</p>



<p><strong>Ví dụ — Stagger pattern trong scheduler</strong>:</p>



<pre class="wp-block-code"><code>&lt;?php

// routes/console.php
use Illuminate\Support\Facades\Schedule;

// Dàn đều theo phút để tránh thundering herd
$times = &#91;'9:03', '10:03', '11:03', '12:03', '13:03', '14:03', '15:03', '16:03', '17:03', '18:03', '19:03', '20:03', '21:03'];

foreach ($times as $time) {
    Schedule::job(new ProcessHourlyTaskJob)
        ->at($time);
}</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="7-observability-là-bắt-buộc-không-ai-biết-scheduler-fail--thất-bại-hệ-thống"><span id="7-observability-la-bat-buoc-khong-ai-biet-scheduler-fail-that-bai-he-thong">7. Observability là bắt buộc: &#8220;Không ai biết scheduler fail&#8221; = thất bại hệ thống</span></h2>



<p>Scheduler fail nguy hiểm vì nó&nbsp;<strong>âm thầm</strong>.</p>



<p><strong>Best practice</strong>:</p>



<ul class="wp-block-list">
<li>Log theo batch (scheduledAt, job name, duration, result)</li>



<li>Có alert khi fail (hook/event/ping tùy hệ)</li>



<li>Có &#8220;schedule inventory&#8221;: Định kỳ audit lịch chạy (ít nhất kiểm lịch sau deploy)</li>
</ul>



<p><strong>Trade-off</strong>:</p>



<ul class="wp-block-list">
<li>Thêm log/metrics → Tốn công + tốn storage</li>



<li>Nhưng thiếu nó thì khi sự cố xảy ra, bạn mất nhiều giờ &#8220;đào mộ&#8221;</li>
</ul>



<p><strong>Ví dụ — Scheduler hooks trong Laravel 12</strong>:</p>



<p>Theo&nbsp;<a href="https://laravel.com/docs/12.x/scheduling#task-hooks">Laravel 12.x Task Hooks</a>:</p>



<pre class="wp-block-code"><code>&lt;?php

// routes/console.php
use Illuminate\Support\Facades\Schedule;

Schedule::command('emails:send')
    ->daily()
    ->before(function () {
        // The task is about to execute...
    })
    ->after(function () {
        // The task has executed...
    })
    ->onSuccess(function () {
        // The task succeeded...
    })
    ->onFailure(function () {
        // The task failed...
    });</code></pre>



<p><strong>Scheduler setup</strong>:</p>



<pre class="wp-block-code"><code># Cron entry
* * * * * cd /path-to-app &amp;&amp; php artisan schedule:run >> /dev/null 2>&amp;1</code></pre>



<p><strong>Kiểm tra lịch chạy</strong>:</p>



<pre class="wp-block-code"><code># Laravel 12: Xem tất cả scheduled tasks
php artisan schedule:list</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="8-cicd--deploy-đừng-để-deploy-tạo-ra-double-run--half-run"><span id="8-ci-cd-deploy-dung-de-deploy-tao-ra-double-run-half-run">8. CI/CD &amp; Deploy: Đừng để deploy tạo ra double-run / half-run</span></h2>



<p>Ở môi trường deploy liên tục, scheduler/worker cần &#8220;graceful&#8221;:</p>



<p><strong>Best practice</strong>:</p>



<ul class="wp-block-list">
<li>Tránh chạy song song 2 version job logic cho cùng một batch</li>



<li>Worker restart có kiểm soát; job quan trọng có idempotency key để chịu được retry/duplicate</li>



<li>Nếu hệ HA nhiều scheduler node: Phải có chiến lược &#8220;one server&#8221; + shared lock store</li>
</ul>



<p><strong>Sai lầm thường gặp</strong>: Deploy xong &#8220;mọi thứ xanh&#8221;, nhưng lịch 09:00 bị chạy 2 lần vì 2 scheduler node cùng tick.</p>



<p><strong>Ví dụ — Laravel 12&nbsp;<code>onOneServer()</code></strong>:</p>



<p>Theo&nbsp;<a href="https://laravel.com/docs/12.x/scheduling#running-tasks-on-one-server">Laravel 12.x Running Tasks on One Server</a>:</p>



<pre class="wp-block-code"><code>&lt;?php

// routes/console.php
use Illuminate\Support\Facades\Schedule;

// Nếu có nhiều scheduler server, dùng onOneServer() để chạy chỉ trên 1 server
Schedule::command('emails:send')
    ->daily()
    ->onOneServer(); // Chỉ chạy trên 1 server, dùng cache để lock</code></pre>



<p><strong>Lưu ý</strong>: Job vẫn phải idempotent vì:</p>



<ul class="wp-block-list">
<li>Retry khi fail</li>



<li>Deploy có thể làm job chạy lại</li>



<li>Worker restart có thể làm job chạy lại</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="9-tổ-chức-code-khi-kernelphp-phình-to"><span id="9-to-chuc-code-khi-kernel-php-phinh-to">9. Tổ chức code: Khi Kernel.php &#8220;phình to&#8221;</span></h2>



<p>Khi số lượng lịch tăng,&nbsp;<code>Kernel.php</code>&nbsp;sẽ rất dài (có thể lên 300+ lines).</p>



<p><strong>Best practice — Laravel 12</strong>: Tách helper functions theo domain để dễ maintain:</p>



<p>Theo&nbsp;<a href="https://laravel.com/docs/12.x/scheduling#defining-schedules">Laravel 12.x Defining Schedules</a>, bạn có thể định nghĩa trong&nbsp;<code>routes/console.php</code>&nbsp;và tách thành helper functions:</p>



<pre class="wp-block-code"><code>&lt;?php

// routes/console.php
use Illuminate\Support\Facades\Schedule;

// Tách theo domain để dễ maintain
schedulePaymentTasks();
scheduleNotificationTasks();
scheduleCleanupTasks();

function schedulePaymentTasks(): void
{
    Schedule::command('payments:process')
        ->dailyAt('09:00');

    Schedule::command('payments:retry')
        ->dailyAt('14:00');
}

function scheduleNotificationTasks(): void
{
    Schedule::command('notifications:send')
        ->hourly();

    Schedule::command('notifications:digest')
        ->dailyAt('20:00');
}

function scheduleCleanupTasks(): void
{
    Schedule::command('cache:clear')
        ->hourly()
        ->runInBackground();
}</code></pre>



<p>Hoặc dùng&nbsp;<code>withSchedule</code>&nbsp;trong&nbsp;<code>bootstrap/app.php</code>&nbsp;và tách thành methods:</p>



<pre class="wp-block-code"><code>&lt;?php

// bootstrap/app.php
use Illuminate\Console\Scheduling\Schedule;

->withSchedule(function (Schedule $schedule) {
    schedulePaymentTasks($schedule);
    scheduleNotificationTasks($schedule);
    scheduleCleanupTasks($schedule);
});

function schedulePaymentTasks(Schedule $schedule): void
{
    $schedule->command('payments:process')->dailyAt('09:00');
    $schedule->command('payments:retry')->dailyAt('14:00');
}</code></pre>



<p><strong>Lợi ích</strong>:</p>



<ul class="wp-block-list">
<li>Dễ review: Mỗi function = 1 domain</li>



<li>Dễ test: Có thể test từng domain riêng</li>



<li>Dễ ownership: Team có thể own từng domain</li>



<li>Tránh Kernel.php phình to: Code nằm trong <code>routes/console.php</code> hoặc helper functions</li>
</ul>



<p><strong>Schedule Groups — Laravel 12</strong>:</p>



<p>Theo&nbsp;<a href="https://laravel.com/docs/12.x/scheduling#schedule-groups">Laravel 12.x Schedule Groups</a>, bạn có thể nhóm các tasks có cùng cấu hình để tránh lặp lại code:</p>



<pre class="wp-block-code"><code>&lt;?php

// routes/console.php
use Illuminate\Support\Facades\Schedule;

// ❌ SAI: Lặp lại cấu hình cho từng task
Schedule::command('emails:send')
    ->daily()
    ->onOneServer()
    ->timezone('America/New_York');

Schedule::command('emails:prune')
    ->daily()
    ->onOneServer()
    ->timezone('America/New_York');

// ✅ ĐÚNG: Dùng Schedule Groups để nhóm tasks có cùng cấu hình
Schedule::daily()
    ->onOneServer()
    ->timezone('America/New_York')
    ->group(function () {
        Schedule::command('emails:send');
        Schedule::command('emails:prune');
    });</code></pre>



<p><strong>Lợi ích của Schedule Groups</strong>:</p>



<ul class="wp-block-list">
<li><strong>Giảm lặp lại code</strong>: Không cần lặp lại <code>onOneServer()</code>, <code>timezone()</code>, <code>withoutOverlapping()</code> cho từng task</li>



<li><strong>Dễ maintain</strong>: Thay đổi cấu hình một lần áp dụng cho cả nhóm</li>



<li><strong>Tăng tính nhất quán</strong>: Đảm bảo tất cả tasks trong nhóm có cùng cấu hình</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="10-timezone-handling-luôn-nhất-quán"><span id="10-timezone-handling-luon-nhat-quan">10. Timezone handling: Luôn nhất quán</span></h2>



<p><strong>Best practice</strong>: Cố định timezone theo business, không dùng server timezone.&nbsp;<strong>Nguyên tắc</strong>: Tất cả scheduled tasks phải dùng cùng một business timezone để đảm bảo tính nhất quán.</p>



<p><strong>Ví dụ — Laravel 12 timezone</strong>:</p>



<p>Theo&nbsp;<a href="https://laravel.com/docs/12.x/scheduling#timezones">Laravel 12.x Scheduling Timezones</a>, có 2 cách set timezone:</p>



<p><strong>Cách 1: Set timezone trong config (khuyến nghị)</strong>:</p>



<p>Khi tất cả tasks dùng cùng business timezone, thêm&nbsp;<code>schedule_timezone</code>&nbsp;vào&nbsp;<code>config/app.php</code>:</p>



<pre class="wp-block-code"><code>&lt;?php

// config/app.php
return &#91;
    'timezone' => 'UTC', // Application timezone

    'schedule_timezone' => 'Asia/Tokyo', // Business timezone cho scheduled tasks
];</code></pre>



<p>Với cấu hình này, tất cả scheduled tasks sẽ tự động dùng timezone&nbsp;<code>Asia/Tokyo</code>&nbsp;trừ khi được ghi đè bởi method&nbsp;<code>timezone()</code>&nbsp;trong từng task cụ thể.</p>



<p><strong>Cách 2: Set timezone cho từng task riêng</strong>:</p>



<p>Chỉ định timezone cho task cụ thể bằng method&nbsp;<code>timezone()</code>:</p>



<pre class="wp-block-code"><code>&lt;?php

// routes/console.php
use Illuminate\Support\Facades\Schedule;

// Set timezone cho task cụ thể (ghi đè config)
Schedule::command('report:generate')
    ->timezone('America/New_York')
    ->at('2:00');</code></pre>



<p><strong>Lưu ý quan trọng về DST (Daylight Saving Time)</strong>:</p>



<p>Theo&nbsp;<a href="https://laravel.com/docs/12.x/scheduling#timezones">Laravel 12.x Scheduling Timezones</a>, một số timezone sử dụng DST có thể ảnh hưởng đến thời gian thực thi. Khi DST thay đổi, task có thể chạy 2 lần hoặc không chạy.&nbsp;<strong>Nên tránh dùng timezone có DST khi có thể</strong>.</p>



<p><strong>Nguyên tắc áp dụng</strong>:</p>



<ul class="wp-block-list">
<li>✅ <strong>Nên</strong>: Set timezone một lần trong <code>config/app.php</code> với key <code>schedule_timezone</code> nếu tất cả tasks dùng cùng business timezone</li>



<li>⚠️ <strong>Tránh</strong>: Set timezone cho từng task riêng lẻ → dễ quên, dễ sai, khó maintain</li>



<li>⚠️ <strong>Tránh</strong>: Dùng timezone có DST → có thể ảnh hưởng đến thời gian thực thi khi DST thay đổi</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="kết-luận-framework-cho-quyết-định-kỹ-thuật"><span id="ket-luan-framework-cho-quyet-dinh-ky-thuat">Kết luận: Framework cho quyết định kỹ thuật</span></h2>



<p>Khi thêm một scheduled task mới, tôi tự hỏi 6 câu:</p>



<ol class="wp-block-list">
<li><strong>Task này có side-effect quan trọng không?</strong> → Nếu có → Idempotent + audit batch</li>



<li><strong>Dữ liệu tăng thì task tăng theo tuyến nào?</strong> → Nếu O(N) theo bảng lớn → Phải có watermark/chunk/index</li>



<li><strong>Chạy trễ có làm sai cửa sổ dữ liệu không?</strong> → Nếu có → Cần <code>scheduledAt/batchAt</code></li>



<li><strong>Có được phép chạy song song không?</strong> → Nếu không → Overlap guard + lock theo business key</li>



<li><strong>Nó thuộc priority nào?</strong> → Nếu không critical → Đừng để nó chạy chung queue với critical</li>



<li><strong>Fail thì ai biết, trong bao lâu?</strong> → Nếu câu trả lời mơ hồ → Bổ sung observability trước</li>
</ol>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="checklist-áp-dụng"><span id="checklist-ap-dung">Checklist áp dụng</span></h2>



<ul class="wp-block-list">
<li> Scheduler chỉ dispatch queued jobs, không chạy logic nặng</li>



<li> Tất cả job có side-effect đều idempotent</li>



<li> Job dùng <code>batchTime/scheduledAt</code> để tính time window, không dùng <code>now()</code></li>



<li> Job dùng <code>chunkById</code> hoặc cursor pagination cho data lớn</li>



<li> Job có stagger/rate-limit khi gọi external API</li>



<li> Job log theo batch time để dễ đối soát</li>



<li> Scheduler có helper methods theo domain, không phình to <code>schedule()</code></li>



<li> Timezone nhất quán: Business timezone, không dùng server timezone</li>



<li> Queue được phân priority (critical/default/low)</li>



<li> Có alert khi scheduler/job fail</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p><strong>Tài liệu tham khảo</strong>:&nbsp;<a href="https://laravel.com/docs/12.x/scheduling#main-content">Laravel 12.x Task Scheduling</a></p>
<p>The post <a href="https://blog.tomosia.com.vn/laravel-scheduler-tu-duy-thiet-ke-trade-off-va-best-practices-cho-he-thong-lon/">Laravel Scheduler: Tư duy thiết kế, trade-off và best practices cho hệ thống lớn</a> appeared first on <a href="https://blog.tomosia.com.vn">Tomoshare</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.tomosia.com.vn/laravel-scheduler-tu-duy-thiet-ke-trade-off-va-best-practices-cho-he-thong-lon/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
