<?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>hoa nguyen, Author at Tomoshare</title>
	<atom:link href="https://blog.tomosia.com.vn/author/hoa-nguyen/feed/" rel="self" type="application/rss+xml" />
	<link>https://blog.tomosia.com.vn/author/hoa-nguyen/</link>
	<description>Kênh chia sẻ kiến thức Tomosia Việt Nam</description>
	<lastBuildDate>Fri, 17 Apr 2026 07:08:02 +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>hoa nguyen, Author at Tomoshare</title>
	<link>https://blog.tomosia.com.vn/author/hoa-nguyen/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Tối ưu Slow Query PostgreSQL: Khi một dòng code cứu cả hệ thống</title>
		<link>https://blog.tomosia.com.vn/toi-uu-slow-query-postgresql-khi-mot-dong-code-cuu-ca-he-thong/</link>
					<comments>https://blog.tomosia.com.vn/toi-uu-slow-query-postgresql-khi-mot-dong-code-cuu-ca-he-thong/#respond</comments>
		
		<dc:creator><![CDATA[hoa nguyen]]></dc:creator>
		<pubDate>Fri, 17 Apr 2026 07:07:57 +0000</pubDate>
				<category><![CDATA[PHP]]></category>
		<category><![CDATA[database]]></category>
		<category><![CDATA[Laravel]]></category>
		<guid isPermaLink="false">https://blog.tomosia.com.vn/?p=4257</guid>

					<description><![CDATA[<p>Tối ưu Slow Query PostgreSQL: Khi một dòng code cứu cả hệ thống Bối cảnh Trong một dự&#8230;</p>
<p>The post <a href="https://blog.tomosia.com.vn/toi-uu-slow-query-postgresql-khi-mot-dong-code-cuu-ca-he-thong/">Tối ưu Slow Query PostgreSQL: Khi một dòng code cứu cả hệ thống</a> appeared first on <a href="https://blog.tomosia.com.vn">Tomoshare</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 id="toi-uu-slow-query-postgresql-khi-mot-dong-code-cuu-ca-he-thong" class="wp-block-heading">Tối ưu Slow Query PostgreSQL: Khi một dòng code cứu cả hệ thống</h1>



<h2 id="boi-canh" class="wp-block-heading">Bối cảnh</h2>



<p>Trong một dự án e-commerce gần đây, tôi gặp một bài toán rất quen thuộc:<br>trên trang chi tiết đơn hàng, cần có nút:</p>



<ul class="wp-block-list">
<li>“Đơn tiếp theo” (Next)</li>



<li>“Đơn trước đó” (Previous)</li>
</ul>



<p>Yêu cầu tưởng đơn giản, nhưng có một constraint quan trọng:<br><strong>không được load toàn bộ danh sách lên RAM</strong>.</p>



<p>Giải pháp hiển nhiên là dùng <strong>Keyset Pagination</strong> (cursor-based), thay vì offset pagination.</p>



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



<h2 id="cach-trien-khai-ban-dau" class="wp-block-heading">Cách triển khai ban đầu</h2>



<p>Dữ liệu được sort theo:</p>



<ul class="wp-block-list">
<li><code>paid_at</code> (thời điểm thanh toán)</li>



<li><code>id</code> (để break tie)</li>
</ul>



<p>Code Laravel Eloquent ban đầu của tôi cho “Next Order” trông như sau:</p>



<pre class="wp-block-preformatted">-&gt;where(function (Builder $q) use ($order) {<br>    $q-&gt;where('paid_at', '&gt;', $order-&gt;paid_at)<br>        -&gt;orWhere(function (Builder $q1) use ($order) {<br>            $q1-&gt;where('paid_at', $order-&gt;paid_at)<br>                -&gt;where('id', '&gt;', $order-&gt;id);<br>        });<br>})<br>-&gt;orderBy('paid_at')<br>-&gt;orderBy('id')</pre>



<p>Nếu đọc bằng “tiếng người”, logic này hoàn toàn đúng:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Lấy các đơn có <code>paid_at</code> lớn hơn,<br>hoặc nếu cùng thời điểm thì <code>id</code> phải lớn hơn.</p>
</blockquote>



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



<h2 id="van-de-thuc-te-xay-ra" class="wp-block-heading">Vấn đề thực tế xảy ra</h2>



<p>Khi dữ liệu tăng lên vài triệu bản ghi, hệ thống bắt đầu:</p>



<ul class="wp-block-list">
<li>xuất hiện <strong>slow query</strong></li>



<li>query plan chuyển sang:
<ul class="wp-block-list">
<li><code>Bitmap Heap Scan</code></li>



<li>hoặc tệ hơn là <code>Seq Scan</code></li>
</ul>
</li>
</ul>



<p>Trong khi đó, database đã có <strong>composite index</strong>:</p>



<pre class="wp-block-preformatted">(paid_at, id)</pre>



<p>=&gt; Về lý thuyết phải chạy rất nhanh.</p>



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



<h2 id="nguyen-nhan-cot-loi" class="wp-block-heading">Nguyên nhân cốt lõi</h2>



<p>Thủ phạm nằm ở <strong>mệnh đề <code>OR</code></strong>.</p>



<p>PostgreSQL Query Planner khi gặp <code>OR</code> thường:</p>



<ul class="wp-block-list">
<li>tách điều kiện thành nhiều nhánh</li>



<li>không tận dụng được composite index một cách hiệu quả</li>



<li>dẫn tới scan nhiều hơn cần thiết</li>
</ul>



<p>Nói đơn giản:<br><strong>Index có, nhưng không được dùng đúng cách.</strong></p>



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



<h2 id="cach-giai-quyet-va-la-diem-aha-moment" class="wp-block-heading">Cách giải quyết (và là điểm “aha moment”)</h2>



<p>Thay vì viết điều kiện dạng boolean phức tạp,<br>ta sử dụng <strong>Tuple Comparison (Row Value Syntax)</strong> của PostgreSQL.</p>



<h3 id="thay-doi-duy-nhat" class="wp-block-heading">Thay đổi duy nhất:</h3>



<pre class="wp-block-preformatted">-&gt;whereRaw('(paid_at, id) &gt; (?, ?)', [$order-&gt;paid_at, $order-&gt;id])</pre>



<p>Với “Previous”:</p>



<pre class="wp-block-preformatted">-&gt;whereRaw('(paid_at, id) &lt; (?, ?)', [$order-&gt;paid_at, $order-&gt;id])</pre>



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



<h2 id="vi-sao-cach-nay-hieu-qua" class="wp-block-heading">Vì sao cách này hiệu quả?</h2>



<p>Composite index <code>(paid_at, id)</code> trong PostgreSQL được sắp xếp theo kiểu:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>từ điển (lexicographical order)</p>
</blockquote>



<p>Tức là:</p>



<ol class="wp-block-list">
<li>So sánh <code>paid_at</code></li>



<li>Nếu bằng nhau → so sánh <code>id</code></li>
</ol>



<p>Khi bạn viết:</p>



<pre class="wp-block-preformatted">(paid_at, id) &gt; (x, y)</pre>



<p>PostgreSQL có thể:</p>



<ul class="wp-block-list">
<li><strong>map trực tiếp vào B-Tree index</strong></li>



<li>nhảy đúng vị trí <code>(x, y)</code></li>



<li>thực hiện <strong>Index Scan tuyến tính</strong></li>
</ul>



<p>=&gt; Không cần phân nhánh logic như <code>OR</code> nữa.</p>



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



<h2 id="ket-qua" class="wp-block-heading">Kết quả</h2>



<p>Sau khi thay đổi:</p>



<ul class="wp-block-list">
<li>Query Plan chuyển thành <strong>Index Scan</strong></li>



<li>Slow query biến mất</li>



<li>Thời gian response ổn định lại</li>
</ul>



<p>Ngoài ra còn một lợi ích “không ngờ”:</p>



<h3 id="code-gon-hon-rat-nhieu" class="wp-block-heading">Code gọn hơn rất nhiều</h3>



<p>Từ:</p>



<ul class="wp-block-list">
<li>nhiều closure lồng nhau</li>



<li>logic khó đọc</li>
</ul>



<p>=&gt; còn:</p>



<pre class="wp-block-preformatted">-&gt;whereRaw('(paid_at, id) &gt; (?, ?)', [...])</pre>



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



<h2 id="bai-hoc-rut-ra" class="wp-block-heading">Bài học rút ra</h2>



<h3 id="1-tranh-or-khi-lam-keyset-pagination" class="wp-block-heading">1. Tránh <code>OR</code> khi làm keyset pagination</h3>



<p>Đặc biệt khi:</p>



<ul class="wp-block-list">
<li>sort theo nhiều cột</li>



<li>đã có composite index</li>
</ul>



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



<h3 id="2-hieu-cach-database-to-chuc-index-quan-trong-hon-orm" class="wp-block-heading">2. Hiểu cách database tổ chức index quan trọng hơn ORM</h3>



<p>ORM giúp code “đẹp”, nhưng:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>không đảm bảo query tối ưu</p>
</blockquote>



<p>Đôi khi, quay về <strong>RAW SQL đúng chỗ</strong> lại là lựa chọn tốt hơn.</p>



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



<h3 id="3-tan-dung-tuple-comparison-trong-postgresql" class="wp-block-heading">3. Tận dụng Tuple Comparison trong PostgreSQL</h3>



<p>Đây là một feature rất mạnh nhưng ít được dùng:</p>



<pre class="wp-block-preformatted">(col1, col2) &gt; (val1, val2)</pre>



<p>Rất phù hợp cho:</p>



<ul class="wp-block-list">
<li>keyset pagination</li>



<li>multi-column sorting</li>
</ul>



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



<h2 id="ket-luan" class="wp-block-heading">Kết luận</h2>



<p>Một thay đổi nhỏ:</p>



<ul class="wp-block-list">
<li>bỏ <code>OR</code></li>



<li>dùng tuple comparison</li>
</ul>



<p>=&gt; có thể:</p>



<ul class="wp-block-list">
<li>giảm load database</li>



<li>tránh slow query</li>



<li>đơn giản hoá code</li>
</ul>



<p>Đây là một ví dụ điển hình cho việc:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p><strong>tối ưu performance không phải lúc nào cũng cần rewrite lớn — đôi khi chỉ cần hiểu đúng cách database hoạt động.</strong></p>
</blockquote>



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



<p></p>
<p>The post <a href="https://blog.tomosia.com.vn/toi-uu-slow-query-postgresql-khi-mot-dong-code-cuu-ca-he-thong/">Tối ưu Slow Query PostgreSQL: Khi một dòng code cứu cả hệ thống</a> appeared first on <a href="https://blog.tomosia.com.vn">Tomoshare</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.tomosia.com.vn/toi-uu-slow-query-postgresql-khi-mot-dong-code-cuu-ca-he-thong/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<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>
		<item>
		<title>Hành Trình Debug Lỗi Redis OOM Trong Laravel: Tìm Ra Hơn 2.8 Triệu Keys Và Cách Khắc Phục Triệt Để</title>
		<link>https://blog.tomosia.com.vn/hanh-trinh-debug-loi-redis-oom-trong-laravel-tim-ra-hon-2-8-trieu-keys-va-cach-khac-phuc-triet-de/</link>
					<comments>https://blog.tomosia.com.vn/hanh-trinh-debug-loi-redis-oom-trong-laravel-tim-ra-hon-2-8-trieu-keys-va-cach-khac-phuc-triet-de/#comments</comments>
		
		<dc:creator><![CDATA[hoa nguyen]]></dc:creator>
		<pubDate>Sun, 07 Dec 2025 16:10:14 +0000</pubDate>
				<category><![CDATA[Chưa phân loại]]></category>
		<guid isPermaLink="false">https://blog.tomosia.com.vn/?p=3737</guid>

					<description><![CDATA[<p>Có những ngày làm kỹ thuật trôi qua rất yên bình, nhưng cũng có những ngày bắt đầu&#8230;</p>
<p>The post <a href="https://blog.tomosia.com.vn/hanh-trinh-debug-loi-redis-oom-trong-laravel-tim-ra-hon-2-8-trieu-keys-va-cach-khac-phuc-triet-de/">Hành Trình Debug Lỗi Redis OOM Trong Laravel: Tìm Ra Hơn 2.8 Triệu Keys Và Cách Khắc Phục Triệt Để</a> appeared first on <a href="https://blog.tomosia.com.vn">Tomoshare</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>Có những ngày làm kỹ thuật trôi qua rất yên bình, nhưng cũng có những ngày bắt đầu bằng một tiếng ping Slack “<strong>Queue đứng rồi anh ơi!</strong>”, &#8220;<strong>Server không vào được nữa anh ơi!</strong>&#8220;<br>Và đây là một trong những ca sự cố mà tôi nhớ rất rõ — bởi vì nó dẫn tôi đến việc tìm thấy <strong>hơn 2.8 triệu Redis keys</strong> chỉ trong môi trường phát triển.</p>



<p>Đó là lúc tôi buộc phải lôi toàn bộ “đồ nghề” ra: từ metrics, redis-cli, bash script, đến việc ngồi soi từng pattern key một. Và những gì tôi học được trong hành trình đó, tôi muốn chia sẻ lại ở đây — để nếu bạn cũng dùng Redis giống tôi, bạn sẽ tránh được một ngày làm việc dở khóc dở cười.</p>



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



<h1 id="1-dau-hieu-ban-dau-khi-slack-bao-dong-khong-ngung" class="wp-block-heading"><strong>1. Dấu hiệu ban đầu – khi slack  báo động</strong> <strong>không ngừng</strong></h1>



<p>Sự cố được phát hiện qua lỗi sau trên slack:</p>



<pre class="wp-block-code"><code>OOM command not allowed when used memory > 'maxmemory'.
RedisException: OOM command not allowed when used memory > 'maxmemory'. in /var/www/app/vendor/laravel/framework/src/Illuminate/Redis/Connections/PhpRedisConnection.php:405
</code></pre>



<p>Nếu bạn đã từng gặp lỗi này, bạn biết nó nguy hiểm cỡ nào:</p>



<ul class="wp-block-list">
<li>Redis <strong>từ chối mọi lệnh ghi</strong></li>



<li>Queue dừng</li>



<li>Horizon đứng hình</li>



<li>Laravel không thể push thêm job</li>



<li>Toàn bộ request liên quan Redis gần như fail đồng loạt</li>
</ul>



<p>Điều đầu tiên tôi làm là mở AWS ElastiCache lên. Và cảnh tượng trước mắt:</p>



<ul class="wp-block-list">
<li>Memory đã <strong>100%</strong></li>



<li>Biểu đồ tăng đều, gần như hoàn hảo theo dạng <em>diagonal slope</em></li>



<li>Không hề có eviction (vì đang dùng <code>volatile-lru</code> — mặc định của AWS ElastiCache)</li>
</ul>



<p>Đây là dấu hiệu kinh điển của <strong>memory leak</strong> trong Redis.</p>



<p>Giờ thì không còn đường lui nữa: phải lội vào trong Redis để xem nó đang chứa cái gì.</p>



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



<h1 id="2-bat-dau-dieu-tra-mo-mam-tung-dau-vet-trong-redis" class="wp-block-heading"><strong>2. Bắt đầu điều tra – mò mẫm từng dấu vết trong Redis</strong></h1>



<p>Tôi ssh vào ECS container chạy Laravel (Alpine), cài nhanh redis-cli:</p>



<pre class="wp-block-code"><code>apk add redis vim</code></pre>



<p>Rồi truy cập Redis database mà Laravel đang dùng:</p>



<pre class="wp-block-code"><code>redis-cli -h &lt;redis-host> -n 1 --scan</code></pre>



<p>Mục tiêu của tôi:</p>



<p><strong>Tìm ra nhóm keys nào đã ăn hết RAM của Redis.</strong></p>



<p>Bởi Redis mà đầy RAM thì chỉ có 1 trong 2 nguyên nhân:</p>



<ol class="wp-block-list">
<li><strong>Key quá to</strong></li>



<li><strong>Too many keys</strong></li>
</ol>



<p>Và theo trực giác, tôi nghiêng về <strong>too many keys</strong>.</p>



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



<h1 id="3-nhan-dien-pattern-la-cuoc-dieu-tra-bat-dau-co-hy-vong" class="wp-block-heading"><strong>3. Nhận diện pattern lạ – cuộc điều tra bắt đầu có hy vọng</strong></h1>



<p>Trong dự án <code>laravel_dev</code>, tôi lướt qua các module có khả năng sinh nhiều keys.<br>Sau một hồi đọc log và grep source, tôi để ý đến pattern:</p>



<pre class="wp-block-code"><code>laravel_dev_cache_:App\\\\Jobs*</code></pre>



<p>Đây là nhóm keys do một service lưu metadata job, kiểu dạng log hoặc tracking.</p>



<p>Tôi quyết định scan thử:</p>



<pre class="wp-block-code"><code>redis-cli -h &lt;redis-host> -n 1 --scan --pattern "laravel_dev_cache_:App\\\\Jobs*" | wc -l</code></pre>



<p>Redis trả về:</p>



<p><strong>≈ 2.800.000 keys</strong></p>



<p>Tôi ngồi im vài giây.</p>



<p>Đây chính là thủ phạm.</p>



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



<h1 id="4-dao-sau-hon-khong-chi-nhieu-keys-ma-con-bat-tu" class="wp-block-heading"><strong>4. Đào sâu hơn – không chỉ nhiều keys mà còn “bất tử”</strong></h1>



<p>Tôi kiểm tra TTL của một vài key:</p>



<pre class="wp-block-code"><code>redis-cli -h &lt;redis-host> -n 1 TTL "laravel_dev_cache_:App\\Jobs\\JobName:1234567890"</code></pre>



<p>Kết quả:</p>



<pre class="wp-block-code"><code>-1</code></pre>



<p>TTL = -1 nghĩa là:</p>



<p><strong>Key không bao giờ tự hết hạn.</strong></p>



<p>Vậy là rõ:<br>Code đang sinh keys mỗi ngày, mỗi giờ, mỗi phút → và không bao giờ xoá.<br>Một kiểu memory leak rất phổ biến khi dùng Redis sai mục đích.</p>



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



<h1 id="5-van-de-kho-nhan-xoa-2-8-trieu-keys-sao-cho-khong-lam-redis-chet-tiep" class="wp-block-heading"><strong>5. Vấn đề khó nhằn: Xoá 2.8 triệu keys sao cho không làm Redis chết tiếp</strong></h1>



<p>Không thể dùng <code>KEYS</code> — vì:</p>



<ul class="wp-block-list">
<li>Nó block Redis</li>



<li>Khi memory đang căng → khả năng treo Redis rất cao</li>



<li>AWS ElastiCache có thể tự động failover</li>
</ul>



<p>Chỉ có một cách an toàn: <strong>SCAN + DEL dần dần</strong>.</p>



<p>Tôi viết một đoạn bash loop:</p>



<pre class="wp-block-code"><code>while IFS= read -r key; do
  echo "Deleting key: '$key'"
  redis-cli -h &lt;redis-host> -n 1 DEL "$key"
done &lt; &lt;(redis-cli -h &lt;redis-host> -n 1 --scan --pattern "laravel_dev_cache_:App\\\\Jobs*")</code></pre>



<p>Mất khoảng 30–40 phút, lượng keys giảm rõ rệt.<br>Biểu đồ memory từ <strong>100%</strong> rớt xuống <strong>10%</strong>.<br>Horizon hồi sinh. Queue bắt đầu đẩy job trở lại. Cache tiếp tục hoạt động</p>



<p>Nếu muốn chạy background mà không cần giữ terminal:</p>



<pre class="wp-block-code"><code>nohup sh -c '
while IFS= read -r key; do
  echo "Deleting key: '\''$key'\''"
  redis-cli -h &lt;redis-host> -n 1 DEL "$key"
done &lt; &lt;(redis-cli -h &lt;redis-host> -n 1 --scan --pattern "laravel_dev_cache_:App\\\\Jobs*")
' > /tmp/redis_delete.log 2>&amp;1 &amp;</code></pre>



<p>Một cảm giác rất “đã” — giống như nới được một cái van áp lực.</p>



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



<h1 id="6-tim-hieu-nguyen-nhan-trong-code-thu-pham-thuc-su-nam-o-logic" class="wp-block-heading"><strong>6. Tìm hiểu nguyên nhân trong code – thủ phạm thực sự nằm ở logic</strong></h1>



<p>Tôi trace lại luồng xử lý và phát hiện:</p>



<ul class="wp-block-list">
<li>Mỗi job đều sinh ra 1 metadata key khi kết hợp với Laravel Scheduler chưa đúng cách/</li>



<li>Key lưu dạng JSON với prefix <code>laravel_dev_cache_:App\\Jobs:...</code></li>



<li>Không có TTL</li>



<li>Không có cơ chế cleanup</li>



<li>Job chạy nhiều hơn dự kiến → số lượng keys tăng <em>âm thầm</em> một cách đều đặn <em>hàng phút.</em></li>
</ul>



<p>Đây là kiểu bug không gây lỗi ngay lập tức, nhưng sẽ phá hủy hệ thống sau vài tuần hoặc vài tháng — nên rất khó phát hiện nếu không quan sát metrics.</p>



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



<h1 id="7-fix-triet-de-de-su-co-nay-khong-bao-gio-quay-lai" class="wp-block-heading"><strong>7. Fix triệt để – để sự cố này không bao giờ quay lại</strong></h1>



<p>Tôi áp dụng 3 thay đổi lớn.</p>



<h2 id="7-1-bat-buoc-gan-ttl-cho-moi-key" class="wp-block-heading"><strong>7.1 Bắt buộc gán TTL cho mọi key</strong></h2>



<p>Trong Laravel:</p>



<pre class="wp-block-code"><code>Cache::put($key, $value, now()-&gt;addMinutes(60));
</code></pre>



<p>Trong Redis thuần:</p>



<pre class="wp-block-code"><code>Redis::setex($key, 3600, $value);</code></pre>



<p>Nếu bạn để key tồn tại mãi mãi → bạn đang đặt bom hẹn giờ trong hệ thống.</p>



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



<h2 id="7-2-gom-tat-ca-vao-hash-thay-vi-tao-hang-trieu-keys-roi-rac" class="wp-block-heading"><strong>7.2 Gom tất cả vào Hash thay vì tạo hàng triệu keys rời rạc</strong></h2>



<p>Từ:</p>



<pre class="wp-block-code"><code>laravel_dev_cache_:App\\Jobs:&lt;jobId></code></pre>



<p>Chuyển sang:</p>



<pre class="wp-block-code"><code>HSET laravel_dev:jobs:&lt;date> &lt;jobId> &lt;data>
EXPIRE laravel_dev:jobs:&lt;date> 86400</code></pre>



<p>Thay vì 1.8M keys → chỉ còn &lt; 30 keys/ngày.</p>



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



<h2 id="7-3-gioi-han-dung-luong-log" class="wp-block-heading"><strong>7.3 Giới hạn dung lượng log</strong></h2>



<p>Giữ 200 job gần nhất chẳng hạn:</p>



<pre class="wp-block-code"><code>Redis::ltrim('laravel_dev:jobs_log', -200, -1);</code></pre>



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



<h1 id="8-bai-hoc-sau-su-co-nhung-thu-toi-uoc-gi-minh-biet-som-hon" class="wp-block-heading"><strong>8. Bài học sau sự cố – những thứ tôi ước gì mình biết sớm hơn</strong></h1>



<p>Sau ca này, tôi rút ra những kinh nghiệm rất thực chiến:</p>



<h3 id="8-1-redis-khong-phai-database" class="wp-block-heading"><strong>8.1. Redis không phải database.</strong></h3>



<p>Dùng Redis để lưu dữ liệu dạng “log” là tự sát.</p>



<h3 id="8-2-moi-key-phai-co-ttl-luon-luon" class="wp-block-heading"><strong>8.2. Mọi key phải có TTL — luôn luôn.</strong></h3>



<p>Không trừ trường hợp đặc biệt.</p>



<h3 id="8-3-so-luong-keys-cung-la-chi-so-quan-trong-nhu-memory" class="wp-block-heading"><strong>8.3. Số lượng keys cũng là chỉ số quan trọng như memory.</strong></h3>



<p>Rất ít đội monitor key-count, nhưng nên làm.</p>



<h3 id="8-4-can-co-co-che-alert-som" class="wp-block-heading"><strong>8.4. Cần có cơ chế alert sớm.</strong></h3>



<ul class="wp-block-list">
<li>Memory > 80%</li>



<li>Key-count tăng nhanh</li>



<li>Horizon queue lag</li>
</ul>



<h3 id="8-5-khi-redis-day-dung-hoang-scan-va-xoa-dung-cach-la-cuu-duoc" class="wp-block-heading"><strong>8.5. Khi Redis đầy, đừng hoảng — scan và xoá đúng cách là cứu được.</strong></h3>



<p>Không được dùng <code>KEYS</code>.</p>



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



<h1 id="9-ket-luan-mot-ca-su-co-dang-nho-va-bai-hoc-cho-nhung-lan-sau" class="wp-block-heading"><strong>9. Kết luận – Một ca sự cố đáng nhớ, và bài học cho những lần sau</strong></h1>



<p>Sự cố Redis OOM trong dự án <code>laravel_dev</code> không chỉ là một lỗi kỹ thuật.<br>Nó nhắc tôi rằng:</p>



<ul class="wp-block-list">
<li>Hệ thống có thể chạy rất mượt trước khi đột ngột “bung lụa”.</li>



<li>Metrics là thứ không bao giờ được xem nhẹ.</li>



<li>Redis mạnh, nhưng dùng sai cách thì nguy hiểm như bất kỳ database nào.</li>
</ul>



<p>Hy vọng hành trình này giúp bạn tránh được một ngày làm việc “chữa cháy” như tôi.</p>



<p></p>
<p>The post <a href="https://blog.tomosia.com.vn/hanh-trinh-debug-loi-redis-oom-trong-laravel-tim-ra-hon-2-8-trieu-keys-va-cach-khac-phuc-triet-de/">Hành Trình Debug Lỗi Redis OOM Trong Laravel: Tìm Ra Hơn 2.8 Triệu Keys Và Cách Khắc Phục Triệt Để</a> appeared first on <a href="https://blog.tomosia.com.vn">Tomoshare</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.tomosia.com.vn/hanh-trinh-debug-loi-redis-oom-trong-laravel-tim-ra-hon-2-8-trieu-keys-va-cach-khac-phuc-triet-de/feed/</wfw:commentRss>
			<slash:comments>4</slash:comments>
		
		
			</item>
	</channel>
</rss>
