Ketika Laravel menjalankan banyak worker queue yang berinteraksi dengan layanan eksternal, race condition bisa menyebabkan duplikasi atau inkonsistensi data. Solusi praktisnya adalah memastikan hanya satu worker yang memproses job tertentu melalui cache lock Redis sebelum akses sumber daya kritis. Pendekatan ini mengurangi konflik sekaligus mempertahankan throughput.

Mengapa Cache Lock Redis Penting untuk Job yang Menulis ke Layanan Eksternal

Tanpa lock, dua worker bisa saja mulai menulis ke layanan eksternal pada saat sama. Contoh sederhana: job untuk mengirim invoice ke penyedia tagihan, lalu worker lain mengirim nomor yang sama karena tidak mengetahui job sudah diproses. Cache lock Redis (biasanya menggunakan SETNX + TTL) memastikan hanya job yang memegang lock yang boleh mengeksekusi bagian kritis.

Membandingkan Pendekatan Tanpa Lock dan dengan Cache Lock

Pendekatan tanpa lock

Tanpa lock, Laravel hanya bergantung pada retry/timeout job. Ketika job gagal karena timeout, worker lain mengambil job yang sama. Ini aman jika operasi idempotent, namun operasi yang tidak idempotent bisa menggandakan efek (misalnya membuat invoice ganda). Monitoring menjadi sulit karena tidak ada proteksi logis terhadap eksekusi paralel.

Pendekatan dengan cache lock

Dengan lock, job yang hendak mengakses layanan eksternal akan mencoba memperoleh lock terlebih dahulu. Jika lock sudah dipegang worker lain, job bisa diretry nanti atau langsung gagal dengan alasan "resource busy". Lock ini memiliki TTL untuk menghindari deadlock saat worker mati mendadak. Strategi ini cocok ketika akses eksternal tidak bisa diulang tanpa efek samping.

Implementasi Praktis: Struktur Job dan Middleware Lock

Contoh job:

class SendInvoiceJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public function handle(CacheManager $cache)
    {
        $lockKey = 'invoice:' . $this->invoiceId;
        $lock = $cache->store('redis')->lock($lockKey, 30); // TTL 30 detik

        if (! $lock->get()) {
            $this->release(10); // coba lagi setelah delay
            return;
        }

        try {
            // akses layanan eksternal di sini
            $this->sendToBillingProvider();
        } finally {
            $lock->release();
        }
    }
}

Jika Anda menggunakan custom middleware, struktur lock bisa di-enkapsulasi:

class CacheLockMiddleware
{
    public function handle($job, $next)
    {
        $lockKey = $job->lockKey ?? null;
        if (! $lockKey) {
            return $next($job);
        }

        $lock = Cache::store('redis')->lock($lockKey, $job->lockTimeout ?? 30);

        if (! $lock->get()) {
            $job->release($job->lockRetryDelay ?? 10);
            return;
        }

        try {
            $next($job);
        } finally {
            $lock->release();
        }
    }
}

Job bisa men-set properti lockKey dan lockTimeout untuk middleware, sehingga logika locking terpusat dan reusable.

Konfigurasi cache redis (config/cache.php)

'redis' => [
    'driver' => 'redis',
    'connection' => 'default',
],

'connections' => [
    'default' => [
        'host' => env('REDIS_HOST', '127.0.0.1'),
        'password' => env('REDIS_PASSWORD', null),
        'port' => env('REDIS_PORT', 6379),
        'database' => 0,
    ],
],

Lock otomatis menggunakan TTL. Pastikan TTL cukup panjang untuk job tetapi tidak terlalu lama agar lock tidak menghalangi worker lain saat job gagal.

Handling Retry, Failure, dan Recovery dari Deadlock

Pastikan job yang gagal karena tidak mendapatkan lock mengeluarkan log jelas dan menggunakan $job->release() untuk jadwal ulang. Jika Anda menggunakan retryUntil, tambahkan batas maksimum attempt untuk mencegah job stuck.

Deadlock recovery

  • Gunakan ttl yang masuk akal (misalnya 30-60 detik) agar lock otomatis terlepas bila worker mati.
  • Implementasikan alat monitoring lock Redis (key pattern lock:invoice:*) untuk mendeteksi lock yang tersisa lama.
  • Kalau ditemukan lock lama, pertimbangkan skrip yang memeriksa apakah job terkait benar-benar masih berjalan sebelum memaksa merilisnya.

Observability: Latency, Lock Leak, dan Dashboard

Checklist observability:

  • Metric: total job queue, success/failure rate, dan lock acquire latency. Gunakan Laravel Telescope atau Prometheus exporter untuk queue throughput.
  • Log: log ketika job tidak memperoleh lock, ketika job melepaskan lock, dan saat lock dilepas karena timeout.
  • Dashboard: tampilkan rata-rata waktu pengambilan lock dan jumlah job yang retry karena lock busy. Panel lock leak bisa memantau key Redis yang tetap ada di atas TTL normal.

Laporkan juga waktu end-to-end job (dari dispatch sampai selesai) karena locking bisa menambah latensi jika ada banyak kontensi.

Trade-off dan Pemilihan Pendekatan

Locking memperbesar jaminan konsistensi tapi menambah latensi serta menurunkan concurrency. Gunakan lock hanya untuk job yang menulis resource tunggal atau tidak idempotent. Untuk operasi idempotent, cukup pastikan job handling bersifat safe dan rely pada retry default Laravel.

Best Practice untuk Throughput dan Reliability Tanpa Kontensi Berlebih

  1. Segmentasikan resource key: gunakan lock per invoice/customer/tenant sehingga tidak semua job saling menunggu.
  2. Gunakan job middleware untuk centralisasi logika lock agar konsistensi dapat diaudit dan diperbarui tanpa merombak job.
  3. Sesuaikan jumlah worker dan queue per resource untuk menghindari bottleneck. Jika job sering gagal karena lock busy, tambahkan delay retry yang meningkat (exponential backoff).
  4. Monitor lock leakage dan pastikan TTL sinkron dengan ekspektasi durasi job.
  5. Dokumentasikan flow lock di tim dan tambahkan alert jika job retry karena tidak mendapat lock melebihi threshold.

Dengan kombinasi cache lock Redis, middleware terstruktur, monitoring latency dan lock leak, serta strategi observability yang matang, Laravel dapat menjaga konsistensi queue tanpa menurunkan throughput secara signifikan. Pendekatan ini juga memudahkan diagnosis saat terjadi kontensi atau perilaku tak terduga di production.