Cache::lock Laravel: Mencegah Pekerjaan Duplikat yang Berjalan Bersamaan

12 Oktober 2026 · 7 menit baca

Artikel ini dibuat secara otomatis dengan bantuan AI dan telah ditinjau oleh Arya Anjar Ginantara.

Abstract illustration of two concurrent work tokens approaching one shared lock, with one admitted and one waiting.

Bayangkan dua request menerima perintah yang sama hampir pada waktu bersamaan: membuat satu invoice dari pembayaran yang sama. Jika keduanya langsung menjalankan pekerjaan, aplikasi dapat memproses unit kerja yang sama dua kali. Masalahnya bukan sekadar nilai cache yang belum tersedia; dua proses perlu berkoordinasi sebelum masuk ke bagian pekerjaan yang tidak boleh dijalankan bersamaan.

Di Laravel, Cache::lock menyediakan lock berbasis cache untuk membatasi siapa yang boleh memasuki bagian tersebut pada satu waktu. Polanya terdiri dari beberapa keputusan: coba mengambil lock, langsung lanjut atau menunggu sebentar, lepaskan lock setelah selesai, dan tentukan expiry sebagai batas pengaman. Lock membantu mengatur akses bersamaan, tetapi bukan jaminan bahwa seluruh aturan bisnis sudah benar atau bahwa pekerjaan akan terjadi tepat satu kali.

Satu skenario: mencegah invoice diproses bersamaan

Misalnya, endpoint pembayaran memanggil service untuk memproses invoice dengan kunci invoice:process:{id}. Dua proses yang menerima invoice-42 harus memakai nama lock yang sama agar saling melihat kontensi. Nilai kunci tidak boleh dibuat berbeda berdasarkan process ID atau waktu request, karena itu justru membuat setiap proses memiliki lock sendiri.

Contoh berikut menunjukkan pilihan menolak segera ketika lock sedang dipegang proses lain:

<?php

namespace App\Services;

use Illuminate\Support\Facades\Cache;

class ProcessInvoice
{
    /**
     * Documentation-guided illustration; not executed in this article.
     */
    public function handle(string $invoiceId): bool
    {
        $lock = Cache::lock("invoice:process:{$invoiceId}", 30);

        if (! $lock->get()) {
            // Proses lain sedang mengerjakan invoice ini.
            return false;
        }

        try {
            // Jalankan pekerjaan yang memang perlu diserialkan.
            // Sesuaikan dengan aturan idempotensi dan status aplikasi.
            $this->processInvoice($invoiceId);

            return true;
        } finally {
            $lock->release();
        }
    }

    private function processInvoice(string $invoiceId): void
    {
        // Implementasi aplikasi berada di sini.
    }
}

Cache::lock($name, $seconds) membuat lock dengan nama dan durasi tertentu. get() mencoba mengambilnya tanpa menunggu. Jika hasilnya false, service tidak boleh berpura-pura bahwa invoice berhasil diproses; aplikasi perlu memilih respons yang sesuai, misalnya mengembalikan status “sedang diproses”, menjadwalkan percobaan lain, atau mencatat penolakan untuk ditinjau. Pilihan itu adalah aturan aplikasi, bukan keputusan otomatis dari Cache::lock.

finally penting karena lock perlu dilepaskan setelah pekerjaan selesai maupun ketika pekerjaan melempar exception. release() hanya menyelesaikan kepemilikan lock yang sedang dipegang proses ini. Contoh ini tidak membuktikan bahwa model, status invoice, constraint database, atau aturan pembayaran di aplikasi tertentu sudah benar.

Kapan harus menunggu, bukan langsung menolak?

Kadang-kadang proses kedua masih boleh menunggu singkat karena proses pertama diperkirakan segera selesai. Laravel mendokumentasikan block($seconds) untuk mencoba memperoleh lock selama batas waktu tertentu. Jika batas tersebut terlewati, Laravel melempar Illuminate\Contracts\Cache\LockTimeoutException.

use Illuminate\Contracts\Cache\LockTimeoutException;
use Illuminate\Support\Facades\Cache;

$lock = Cache::lock("invoice:process:{$invoiceId}", 30);

try {
    $lock->block(5);

    // Documentation-guided illustration; not executed in this article.
    $this->processInvoice($invoiceId);
} catch (LockTimeoutException $e) {
    // Lock tidak diperoleh dalam waktu tunggu yang ditentukan.
    // Tentukan respons aplikasi: retry terkontrol, status tertunda, atau penolakan.
} finally {
    $lock->release();
}

Angka 5 berarti batas menunggu lima detik pada contoh ini, bukan rekomendasi universal. Waktu tunggu harus dipilih bersama batas waktu request, perkiraan durasi pekerjaan, cara aplikasi menangani timeout, dan kemungkinan proses lain masih berjalan. Jangan mengubah timeout menjadi klaim bahwa pekerjaan pasti selesai atau tidak pernah berulang.

Jika alurnya sederhana, Laravel juga mendokumentasikan bentuk closure yang otomatis melepaskan lock setelah closure selesai:

Cache::lock("invoice:process:{$invoiceId}", 30)->block(5, function () use ($invoiceId) {
    // Documentation-guided illustration; not executed in this article.
    $this->processInvoice($invoiceId);
});

Bentuk closure mengurangi risiko lupa memanggil release, tetapi keputusan saat timeout dan aturan pekerjaan tetap harus dirancang oleh aplikasi. Pastikan penanganan exception dan hasil operasi tidak disembunyikan oleh helper yang terlalu ringkas.

Apa arti expiry pada lock?

Durasi 30 pada contoh adalah masa berlaku lock, bukan durasi maksimum yang menjamin metode selesai dengan aman. Expiry berguna sebagai batas ketika proses pemegang lock berhenti sebelum sempat melepaskannya. Namun, jika pekerjaan lebih lama daripada durasi tersebut, lock dapat kedaluwarsa ketika pekerjaan lama masih berjalan. Proses lain lalu dapat memperoleh lock yang sama.

Karena itu, pilih durasi dengan mempertimbangkan durasi pekerjaan yang sebenarnya, variasinya, dan cara aplikasi menangani pekerjaan yang melampaui batas. Untuk pekerjaan panjang, Laravel juga mendokumentasikan refresh() agar pemilik lock dapat memperpanjang expiry-nya. Itu pun bukan pengganti desain status, idempotensi, atau pemulihan ketika proses terputus.

Jika expiry terlewati, jangan menyimpulkan bahwa pekerjaan sebelumnya pasti gagal atau aman untuk diulang. Lock hanya memberi koordinasi berbasis waktu. Aplikasi tetap memerlukan aturan khusus untuk mengetahui apakah invoice sudah diproses, masih berjalan, atau perlu dipulihkan. Pemeriksaan status, unique constraint, dan idempotency key mungkin relevan, tetapi pemilihannya bergantung pada model data dan kebutuhan bisnis; tidak ada yang otomatis diberikan oleh Cache::lock.

Syarat penting: proses harus berbicara melalui cache yang sama

Atomic lock baru berguna lintas proses atau server jika semua pihak memakai cache yang dapat mereka akses bersama. Dokumentasi Laravel mensyaratkan driver cache yang mendukung atomic locks dan menyatakan bahwa semua server harus berkomunikasi dengan cache server pusat yang sama untuk koordinasi lintas server.

Artinya, mengganti nama lock saja tidak cukup. Dua server yang memakai store berbeda dapat sama-sama merasa berhasil memperoleh invoice:process:42, karena masing-masing tidak melihat lock milik server lain. Verifikasi konfigurasi driver, koneksi, namespace/prefix, dan pembagian cache adalah pekerjaan deployment serta aplikasi yang berada di luar contoh ini. Artikel ini tidak menguji driver atau topologi tertentu.

Driver yang tercantum pada dokumentasi Laravel 13.x untuk atomic locks meliputi memcached, redis, dynamodb, database, file, dan array, dengan syarat dukungan dan konteks penggunaan masing-masing. Daftar tersebut bukan rekomendasi memilih provider. Khusus koordinasi lintas server, pertanyaan utamanya adalah apakah semua proses benar-benar berbicara melalui cache bersama yang sesuai, bukan sekadar apakah nama driver ada di konfigurasi.

Lock bukan transaksi dan bukan idempotensi

Cache::lock mengatur siapa yang masuk ke bagian pekerjaan pada waktu tertentu. Ia tidak mengubahnya menjadi transaksi database, tidak me-rollback perubahan ketika proses gagal, dan tidak menyimpan keputusan bisnis bahwa request tertentu sudah pernah berhasil. Lock juga tidak membuktikan exactly-once execution.

Pisahkan tiga pertanyaan berikut:

  • Koordinasi: apakah dua proses perlu dilarang mengerjakan unit yang sama secara bersamaan?
  • Atomicitas database: apakah beberapa perubahan database harus commit atau rollback sebagai satu unit?
  • Idempotensi bisnis: jika request atau pekerjaan dicoba lagi, bagaimana aplikasi mencegah efek bisnis yang tidak diinginkan?

Satu alur dapat memerlukan lebih dari satu mekanisme, tetapi mekanisme tersebut menyelesaikan masalah yang berbeda. Jangan menggunakan lock sebagai alasan untuk mengabaikan constraint database, status domain, penanganan retry, atau pemeriksaan hasil pekerjaan.

Memilih get() atau block()

Gunakan aturan praktis berikut sebagai titik awal desain, bukan aturan resmi Laravel:

  • Pilih get() ketika proses kedua sebaiknya segera mendapat keputusan bahwa pekerjaan sedang ditangani proses lain.
  • Pilih block() ketika menunggu singkat masuk akal dan aplikasi memiliki respons jelas ketika batas waktu terlewati.
  • Gunakan closure jika bentuk otomatis-release membuat alur lebih mudah dibaca dan penanganan kegagalannya tetap terlihat.
  • Selalu tentukan expiry yang sesuai dengan durasi pekerjaan dan rencana pemulihan; jangan menganggap angka default atau angka contoh cocok untuk semua aplikasi.
  • Pastikan semua proses yang harus berkoordinasi memakai nama lock dan cache bersama yang konsisten.

Sebelum menganggap alur siap digunakan, uji pada project nyata: dua pemanggilan bersamaan, proses yang melempar exception, pekerjaan yang lebih lama dari expiry, cache yang tidak tersedia, dan percobaan ulang setelah timeout. Draft ini tidak menjalankan pengujian tersebut.

Ringkasan

Untuk satu skenario pekerjaan duplikat, mulai dari nama lock yang merepresentasikan resource, lalu pilih apakah kontensi ditolak dengan get() atau ditunggu sebentar dengan block(). Lepaskan lock pada jalur selesai maupun gagal, dan perlakukan expiry sebagai batas pengaman yang harus disesuaikan—bukan bukti keamanan atau exactly-once.

Jika koordinasi melintasi proses atau server, pastikan semuanya memakai cache bersama yang mendukung atomic locks. Setelah itu, tinjau lagi aturan idempotensi dan status bisnis aplikasi, karena lock tidak menggantikan transaksi database maupun desain untuk menangani percobaan ulang.

Bagikan:XLinkedIn

Artikel terkait