Redis locking untuk worker queue dipakai ketika Anda perlu memastikan hanya satu worker yang boleh memproses pekerjaan tertentu pada saat yang sama. Ini penting untuk mencegah job ganda, race condition, dan inkonsistensi data saat queue melakukan retry, duplicate delivery, atau ketika beberapa job menyentuh resource yang sama.

Namun lock bukan jawaban untuk semua kasus. Banyak sistem justru lebih aman jika mengutamakan idempotency: job boleh terkirim atau dieksekusi lebih dari sekali, tetapi hasil akhirnya tetap benar. Artikel ini membahas kapan Redis lock memang diperlukan, kapan cukup mengandalkan idempotency, bagaimana merancang lock key, TTL, renew/heartbeat, timeout, penanganan dead worker, serta pola observability dan checklist produksi.

Kapan Redis locking dibutuhkan, dan kapan cukup idempotency?

Gunakan lock jika ada eksklusivitas proses

Lock diperlukan ketika dua eksekusi paralel terhadap job yang sama atau resource yang sama dapat menimbulkan efek yang salah. Contoh umum:

  • Dua worker memproses pembayaran untuk order yang sama pada waktu bersamaan.
  • Dua job melakukan sinkronisasi ke API pihak ketiga yang tidak mendukung deduplikasi.
  • Satu resource harus dimutasi secara serial, misalnya rebuild cache untuk tenant tertentu, rekalkulasi saldo, atau generate invoice final.
  • Queue bisa mengirim ulang job sebelum eksekusi sebelumnya benar-benar selesai.

Cukup idempotency jika hasil bisa diulang dengan aman

Idempotency lebih sederhana dan sering kali lebih tahan terhadap kegagalan terdistribusi. Cocok jika:

  • Job menulis data dengan upsert atau kondisi unik di database.
  • Efek samping bisa dikenali dengan idempotency key.
  • Eksekusi ulang tidak menyebabkan duplikasi output, misalnya mengirim event internal yang punya dedupe key.

Contoh: job send email receipt lebih aman menggunakan tabel atau penyimpanan status seperti email_receipts(sent_key) dengan constraint unik dibanding hanya mengandalkan lock Redis. Jika worker mati setelah email terkirim tetapi sebelum lock dilepas, lock saja tidak cukup untuk menjamin email tidak terkirim dua kali pada retry berikutnya.

Prinsip praktis: gunakan idempotency untuk menjamin hasil akhir tetap benar, dan gunakan locking untuk mencegah eksekusi paralel yang tidak boleh terjadi. Pada sistem produksi, keduanya sering dipakai bersama.

Masalah yang ingin diselesaikan pada worker queue

Duplicate delivery

Banyak sistem queue menerapkan model at-least-once delivery: job bisa diterima lebih dari sekali. Penyebabnya bisa berupa retry, timeout, koneksi worker putus, atau kegagalan saat ACK. Karena itu asumsi “satu job pasti hanya dikirim sekali” tidak aman.

Visibility timeout dan job diproses ulang

Jika queue memiliki konsep visibility timeout, job yang sedang diproses akan disembunyikan sementara. Bila worker tidak selesai atau gagal meng-ACK sebelum batas waktu, job bisa terlihat lagi dan diambil worker lain. Di sinilah lock bisa mencegah dua worker mengerjakan resource yang sama secara bersamaan.

Dead worker

Worker bisa crash setelah mengambil lock, atau berhenti saat proses belum selesai. Tanpa TTL, lock dapat bocor dan memblokir job berikutnya tanpa batas. Karena itu lock di Redis hampir selalu harus punya masa berlaku.

Konsep inti Redis lock untuk queue

Lock key

Lock key harus mewakili unit eksklusivitas yang benar. Ini bagian yang paling sering salah. Jangan membuat key terlalu umum, tetapi juga jangan terlalu sempit.

  • Baik: lock:order:123 jika semua job yang menyentuh order 123 harus serial.
  • Baik: lock:tenant:45:billing-sync jika hanya sinkronisasi billing tenant itu yang perlu eksklusif.
  • Kurang baik: lock:job:uuid bila duplicate delivery bisa hadir dengan UUID berbeda tetapi menyentuh resource yang sama.

Pertanyaan kuncinya: apa yang tidak boleh diproses paralel? Jawabannya itulah yang membentuk lock key.

TTL

TTL mencegah lock hidup selamanya ketika worker mati. Nilainya harus cukup panjang agar mayoritas eksekusi normal selesai, tetapi tidak terlalu lama hingga membuat sistem lambat pulih saat worker crash.

Aturan praktis:

  • TTL awal lebih panjang dari durasi proses normal.
  • Jika durasi job bisa sangat bervariasi, gunakan mekanisme renew atau heartbeat.
  • Jangan mengandalkan TTL yang sangat pendek tanpa heartbeat untuk job yang durasinya tidak pasti.

Owner token

Saat mengambil lock, simpan nilai unik sebagai pemilik lock, misalnya UUID acak. Ini penting agar hanya pemilik lock yang boleh memperpanjang atau melepas lock. Tanpa token, worker lain bisa tanpa sengaja menghapus lock yang bukan miliknya.

Renew atau heartbeat

Untuk job panjang, worker perlu memperpanjang TTL secara periodik selama masih sehat dan masih memegang lock. Biasanya dilakukan setiap sebagian kecil dari TTL, misalnya sekitar sepertiga atau setengah TTL. Jika worker mati, heartbeat berhenti dan lock akan kadaluarsa sendiri.

Timeout

Perlu dibedakan antara:

  • Lock acquire timeout: berapa lama worker mau menunggu sebelum menyerah mendapatkan lock.
  • Job execution timeout: berapa lama job boleh berjalan sebelum dianggap gagal.
  • Queue visibility timeout: kapan job bisa muncul kembali bila belum di-ACK.

Tiga timeout ini harus konsisten. Jika visibility timeout terlalu pendek dibanding durasi kerja aktual, duplicate delivery akan lebih sering terjadi.

Pola implementasi Redis locking yang aman

Akuisisi lock atomik

Pola paling umum adalah menyimpan key hanya jika belum ada, sekaligus memberi TTL. Secara konsep:

  1. Worker membentuk lock key dari resource target.
  2. Worker membuat owner token acak.
  3. Worker mencoba set key jika belum ada, dengan TTL.
  4. Jika berhasil, worker memproses job.
  5. Jika gagal, worker memutuskan: skip, retry nanti, atau requeue dengan backoff.

Contoh pseudocode generik:

lockKey = "lock:order:" + orderId
owner = randomUUID()
lockTTL = 30000 // ms

acquired = redis.set(lockKey, owner, NX=true, PX=lockTTL)
if !acquired:
  requeueWithBackoff(job)
  return

startHeartbeat(lockKey, owner, every=10000, ttl=lockTTL)

try:
  processJob(job)
  ack(job)
finally:
  stopHeartbeat()
  releaseLockIfOwner(lockKey, owner)

Release lock hanya jika masih owner

Ini bagian penting. Jangan lakukan DEL lockKey secara buta. Skenario berbahaya:

  1. Worker A memegang lock.
  2. TTL habis karena proses lama atau heartbeat gagal.
  3. Worker B mengambil lock yang baru.
  4. Worker A selesai dan memanggil DEL.
  5. Lock milik B ikut terhapus, lalu worker C bisa masuk.

Karena itu pelepasan lock harus mengecek owner token terlebih dahulu, idealnya atomik di server Redis.

// pseudocode release atomik
if redis.get(lockKey) == owner:
  redis.del(lockKey)

Dalam implementasi nyata, pengecekan dan penghapusan perlu dilakukan atomik, misalnya dengan script server-side, agar tidak ada celah antara GET dan DEL.

Renew lock hanya jika masih owner

Hal yang sama berlaku untuk heartbeat. Perpanjangan TTL harus memverifikasi bahwa nilai key masih sama dengan owner token. Kalau tidak, worker seharusnya menghentikan proses secepat mungkin atau menandai eksekusi sebagai tidak lagi eksklusif.

// pseudocode renew atomik
if redis.get(lockKey) == owner:
  redis.pexpire(lockKey, lockTTL)
  return true
return false

Jika renew gagal, itu pertanda lock sudah hilang atau diambil pihak lain. Lanjut memproses job dalam kondisi ini berisiko menghasilkan race condition.

Alur implementasi generik untuk Laravel, Node.js, atau Go

1. Tentukan unit eksklusivitas

Misalnya semua job yang memutasi satu order harus serial. Berarti lock key dibangun dari order_id, bukan dari ID job queue.

2. Pisahkan deduplikasi dari eksklusivitas

Gunakan dua lapisan jika perlu:

  • Idempotency key untuk memastikan hasil akhir tidak diterapkan dua kali.
  • Redis lock untuk mencegah dua worker berjalan bersamaan pada resource yang sama.

Contohnya pada refund:

  • lock:payment:123 mencegah dua worker melakukan refund paralel.
  • refund_request_id atau constraint unik di database mencegah refund yang sama tercatat dua kali.

3. Atur urutan kerja secara konsisten

  1. Ambil job dari queue.
  2. Coba ambil lock.
  3. Jika lock gagal, jangan paksa proses. Requeue dengan backoff atau tandai sebagai ditunda.
  4. Jika lock berhasil, mulai heartbeat bila job berpotensi lama.
  5. Lakukan operasi bisnis.
  6. Simpan status akhir/idempotency marker secepat mungkin.
  7. ACK job setelah operasi inti berhasil.
  8. Lepas lock jika masih owner.

4. Sesuaikan dengan retry

Retry harus aman baik saat lock masih ada maupun sudah hilang. Jika retry datang ketika lock masih aktif, worker lain seharusnya gagal mengambil lock dan menunda eksekusi. Jika lock sudah hilang tetapi operasi bisnis sebagian sudah terjadi, idempotency akan mencegah efek samping ganda.

Contoh pseudocode yang lebih lengkap

function handle(job):
  resourceId = job.orderId
  lockKey = "lock:order:" + resourceId
  owner = randomUUID()
  lockTTL = 60000

  if !acquireLock(lockKey, owner, lockTTL):
    requeueWithBackoff(job)
    return

  heartbeat = startHeartbeat(lockKey, owner, interval=20000, ttl=lockTTL)

  try:
    if alreadyProcessed(job.idempotencyKey):
      ack(job)
      return

    result = executeBusinessOperation(job)
    markProcessed(job.idempotencyKey, result)
    ack(job)
  catch err:
    failOrRetry(job, err)
  finally:
    heartbeat.stop()
    releaseLockIfOwner(lockKey, owner)

Pseudocode ini dapat diterapkan dalam berbagai runtime. Pada Laravel, Node.js, atau Go, detail API berbeda, tetapi prinsipnya sama: set if not exists + TTL + owner token + atomic renew/release.

Menentukan TTL, heartbeat, dan visibility timeout

TTL terlalu pendek

Risikonya lock habis saat job masih berjalan. Akibatnya worker lain bisa masuk dan memproses resource yang sama. Ini salah satu penyebab race condition paling sulit dideteksi karena hanya muncul pada job lambat.

TTL terlalu panjang

Risikonya pemulihan lambat saat worker mati. Job lain harus menunggu terlalu lama walaupun pemilik lock sudah tidak hidup.

Panduan praktis

  • Jika job relatif singkat dan durasinya stabil, TTL tetap bisa cukup.
  • Jika job bisa memanggil API eksternal, memproses batch, atau durasinya sangat bervariasi, gunakan heartbeat.
  • Visibility timeout queue sebaiknya lebih panjang dari waktu kerja normal, termasuk margin untuk jitter jaringan dan shutdown lambat.
  • Jangan set heartbeat terlalu jarang. Jika interval terlalu dekat ke TTL, lock bisa kedaluwarsa karena GC pause, CPU stall, atau latensi sesaat.

Praktik yang aman: perlakukan TTL sebagai mekanisme pemulihan dari crash, bukan sebagai penentu utama akhir pekerjaan. Hasil akhir tetap harus diamankan oleh idempotency atau transaksi data yang tepat.

Dead worker, lock bocor, dan cara menghadapinya

Dead worker

Jika worker mati mendadak, lock akan tetap ada sampai TTL habis. Ini normal. Karena itu observability harus bisa membedakan lock aktif sehat dan lock yang hanya menunggu TTL berakhir.

Lock bocor

Istilah ini biasanya merujuk pada lock yang tidak terlepas sesuai harapan. Penyebab umum:

  • TTL tidak dipasang.
  • Heartbeat berhenti tetapi proses masih lanjut.
  • Release dilakukan tanpa verifikasi owner.
  • Jam kerja sistem tidak menjadi masalah utama untuk TTL Redis, tetapi jeda proses yang lama, freeze, atau event loop macet bisa membuat renew terlambat.

Mitigasi

  • Semua lock wajib punya TTL.
  • Gunakan owner token untuk release dan renew.
  • Buat metrik jumlah lock acquisition gagal, renew gagal, dan lock yang bertahan terlalu lama.
  • Sediakan prosedur operasional untuk menginspeksi key lock yang menumpuk.

Pola operasional dan observability

Metrik yang perlu dikumpulkan

  • Jumlah percobaan acquire lock.
  • Persentase acquire berhasil vs gagal.
  • Waktu tunggu sebelum lock didapat atau job direqueue.
  • Jumlah renew berhasil vs gagal.
  • Durasi job saat memegang lock.
  • Jumlah retry yang terjadi karena lock contention.
  • Jumlah duplicate delivery yang terdeteksi oleh idempotency key.

Logging yang berguna

Masukkan informasi berikut ke log terstruktur:

  • job_id
  • resource_id atau entitas yang dikunci
  • lock_key
  • lock_owner
  • lock_ttl
  • hasil acquire, renew, release
  • alasan retry atau requeue

Dengan data itu, Anda bisa menelusuri apakah job gagal karena kontensi lock, worker mati, atau idempotency marker sudah ada.

Alerting

Buat alert jika:

  • Acquire gagal melonjak tajam.
  • Renew gagal meningkat.
  • Job tertahan jauh lebih lama dari biasanya pada resource tertentu.
  • Jumlah key lock aktif tumbuh terus tanpa turun.

Dashboard yang disarankan

Satu dashboard sederhana sering cukup:

  • grafik kontensi lock per jenis job,
  • grafik retry rate,
  • grafik durasi job p95 atau p99,
  • jumlah lock aktif per prefix key,
  • jumlah duplicate yang berhasil diblok oleh idempotency.

Kesalahan umum yang sering terjadi

1. Lock key salah sasaran

Mengunci berdasarkan ID job, bukan berdasarkan resource yang benar-benar konflik. Akibatnya job berbeda tetapi menyentuh resource yang sama tetap bisa balapan.

2. Hanya pakai lock tanpa idempotency

Ini sering gagal saat worker crash setelah efek samping sudah terjadi tetapi sebelum status berhasil tersimpan. Retry berikutnya bisa mengulangi efek yang sama.

3. Release lock dengan DEL biasa

Tanpa verifikasi owner, worker lama bisa menghapus lock worker baru.

4. TTL tidak sesuai realita produksi

TTL diambil dari pengujian lokal yang terlalu optimistis. Di produksi, latensi eksternal, beban puncak, atau stop-the-world pause bisa membuat job lebih lama.

5. Tidak menangani acquire gagal

Jika lock gagal lalu job dianggap error permanen, Anda justru kehilangan pekerjaan yang valid. Biasanya lebih tepat melakukan retry dengan backoff dan jitter.

6. Heartbeat tidak dipantau

Heartbeat yang diam-diam gagal bisa lebih berbahaya daripada tidak ada heartbeat sama sekali, karena sistem tampak sehat padahal eksklusivitas sudah hilang.

7. Visibility timeout lebih pendek dari durasi kerja

Ini memicu duplicate delivery berulang, lalu lock contention tampak seperti masalah utama padahal akar masalahnya ada pada konfigurasi queue.

Strategi debugging saat job tetap ganda atau balapan

  1. Periksa apakah duplicate delivery memang terjadi dari queue.
  2. Audit pembentukan lock key. Pastikan dua job yang berkonflik menghasilkan key yang sama.
  3. Cek TTL dan durasi nyata job pada beban produksi.
  4. Periksa log renew: apakah lock sempat kadaluarsa di tengah proses?
  5. Pastikan release dan renew memverifikasi owner token.
  6. Telusuri apakah efek samping eksternal diamankan oleh idempotency key atau constraint unik.
  7. Lihat apakah ACK dilakukan terlalu awal atau terlalu lambat.

Sering kali masalah bukan pada Redis-nya, tetapi pada kombinasi lock key yang salah, timeout yang tidak serasi, dan ketiadaan idempotency di layer bisnis.

Kapan tidak perlu Redis locking?

Anda tidak selalu perlu lock. Hindari menambah kompleksitas bila salah satu kondisi ini terpenuhi:

  • Operasi sepenuhnya idempotent dan aman dijalankan paralel.
  • Database sudah menyediakan kontrol konkurensi yang cukup, misalnya constraint unik, optimistic locking, atau update bersyarat.
  • Job hanya membaca data dan tidak menimbulkan efek samping.
  • Kontensi sangat rendah dan biaya retry yang aman lebih kecil daripada biaya koordinasi lock.

Lock adalah alat koordinasi. Jika konsistensi sudah bisa dijaga di layer data atau lewat desain idempotent, gunakan solusi yang lebih sederhana.

Checklist produksi Redis locking untuk worker queue

  • Unit eksklusivitas sudah jelas dan tercermin pada lock key.
  • Semua lock memiliki TTL.
  • Set lock dilakukan atomik dengan syarat key belum ada.
  • Lock value berisi owner token unik.
  • Release lock hanya jika owner masih cocok, secara atomik.
  • Renew/heartbeat hanya memperpanjang lock milik owner yang sama.
  • Acquire gagal ditangani dengan retry, backoff, atau reschedule yang jelas.
  • Visibility timeout queue selaras dengan durasi job aktual.
  • Idempotency key atau constraint data tersedia untuk efek samping penting.
  • Log terstruktur mencatat lock key, owner, TTL, dan hasil acquire/renew/release.
  • Metrik dan alert tersedia untuk contention, renew failure, retry rate, dan lock aktif abnormal.
  • Ada prosedur operasional untuk investigasi lock tertinggal dan job tertahan.

Penutup

Redis locking untuk worker queue efektif untuk mencegah job ganda dan race condition, tetapi hanya jika dirancang di level resource yang tepat dan dilengkapi TTL, owner token, serta heartbeat untuk job panjang. Lock membantu menjaga eksklusivitas eksekusi, sedangkan idempotency menjaga hasil akhir tetap benar saat retry atau duplicate delivery tidak bisa dihindari.

Jika Anda ingin desain yang aman untuk produksi, jangan memilih salah satu secara mutlak. Untuk banyak sistem queue, kombinasi terbaik adalah: lock untuk mencegah proses paralel yang berbahaya, idempotency untuk menahan efek samping ganda, dan observability untuk mendeteksi saat asumsi Anda mulai tidak berlaku.