SupportWriter space ↗
← Back to the journal
Laravel

Mencegah Cache Stampede di Laravel dengan Atomic Lock

Batasi penghitungan laporan mahal ketika cache kedaluwarsa lewat double-check, Redis lock, dan fallback terkendali.

DAdministrator · 13 Oct 2026 · 1 min read

This article is not yet available in your selected language. Showing the original.

Laporan penjualan disimpan di cache selama satu menit. Ketika cache habis, seratus request datang bersamaan dan semuanya menjalankan agregasi mahal. Database menerima seratus pekerjaan yang seharusnya cukup dilakukan sekali. Situasi ini dikenal sebagai cache stampede.

Cache::remember() memudahkan penyimpanan hasil, tetapi callback pada cache miss tidak otomatis diserialkan untuk seluruh request. Studi kasus Laravel 13 ini menambahkan atomic lock dan pemeriksaan cache kedua. Semua server web menggunakan Redis yang sama, termasuk konfigurasi prefix key yang konsisten.

Pisahkan cache data dan lock

Misalkan laporan per tim hanya menghitung total order berstatus paid. Tabel orders memiliki team_id, status, dan total_cents integer. Laporan boleh tertinggal paling lama 60 detik, sehingga cache adalah optimasi dengan batas kesegaran yang jelas.

Buat layanan berikut melalui php artisan make:class Services/SalesSummary --no-interaction. Otorisasi keanggotaan tim dilakukan controller sebelum memanggil layanan; ID tim tetap dimasukkan ke cache key agar laporan satu tim tidak dipakai tim lain.

<?php

namespace App\Services;

use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;

final class SalesSummary
{
    /** @return array{orders: int, revenue_cents: int} */
    public function forTeam(int $teamId): array
    {
        $cache = Cache::store('redis');
        $key = "sales-summary:v1:team:{$teamId}";
        $cached = $cache->get($key);

        if (is_array($cached)) {
            return $cached;
        }

        return $cache->lock($key.':lock', 30)->block(3, function () use ($cache, $key, $teamId): array {
            $cached = $cache->get($key);
            if (is_array($cached)) {
                return $cached;
            }

            $row = DB::table('orders')
                ->where('team_id', $teamId)
                ->where('status', 'paid')
                ->selectRaw('COUNT(*) AS order_count, COALESCE(SUM(total_cents), 0) AS revenue_cents')
                ->first();

            $summary = [
                'orders' => (int) $row->order_count,
                'revenue_cents' => (int) $row->revenue_cents,
            ];

            $cache->put($key, $summary, now()->addSeconds(60));

            return $summary;
        });
    }
}

Pemeriksaan kedua penting: request dapat menunggu lock ketika request pertama sedang menghitung. Saat akhirnya memperoleh lock, cache mungkin sudah tersedia. Tanpa pemeriksaan kedua, setiap request yang sempat antre tetap menjalankan agregasi ulang.

Durasi lock bukan waktu tunggu

Angka 30 adalah umur lock, sedangkan block(3, ...) menunggu maksimal tiga detik untuk mendapatkannya. Laravel melepas lock ketika callback selesai. Jika query berjalan lebih dari 30 detik, lock bisa kedaluwarsa saat callback masih aktif sehingga request lain mulai menghitung bersamaan.

Tentukan umur lock berdasarkan waktu terburuk yang diukur, pasang timeout query yang masuk akal, dan hindari operasi jaringan tak terbatas di dalam callback. Lock ini mengurangi beban; ia bukan mekanisme yang tepat untuk menjamin transaksi finansial atau mencegah overselling.

Tentukan respons ketika antrean terlalu lama

block() melempar Illuminate\Contracts\Cache\LockTimeoutException jika batas tunggu habis. Controller dapat menangkap exception tersebut dan mengembalikan 503 dengan Retry-After pendek, atau menampilkan laporan lama yang disimpan pada key terpisah. Jangan menjalankan query mahal sebagai fallback untuk seluruh request yang timeout karena stampede akan muncul lagi.

Redis yang tidak tersedia adalah kegagalan berbeda dari lock timeout. Tentukan kebijakan aplikasi: laporan opsional dapat sementara tidak ditampilkan; endpoint kritis dapat memberikan respons terkontrol. Mencampur cache lokal pada masing-masing server sebagai pengganti lock pusat akan kehilangan koordinasi antarserver.

Invalidasi dan perubahan filter

Key contoh menyertakan versi format dan ID tim. Jika laporan memiliki periode, mata uang, atau filter tambahan, normalisasikan nilai tersebut dan masukkan ke key. Jangan mengambil key dari input bebas tanpa batas karena pengguna dapat membuat banyak entry.

Menghapus key saat order berubah bisa berlomba dengan perhitungan yang sedang berjalan: perhitungan lama dapat menulis hasil setelah invalidasi. Untuk laporan yang toleran keterlambatan, TTL 60 detik merupakan batas yang dapat diterima. Jika bisnis menuntut invalidasi lebih ketat, gunakan strategi generasi key atau data ringkasan yang diperbarui atomik; sekadar forget() tidak menjamin kesegaran absolut.

Uji cache hit, cold miss, dua request bersamaan, lock timeout, dan isolasi dua tim. Untuk konkurensi nyata gunakan Redis bersama, bukan driver array yang hanya hidup pada proses pengujian.

Rujukan API: Atomic locks.

Comments 0

Share your questions, experiences, or feedback about this article.

No comments yet. Be the first to share your feedback.

← Explore more notes