Ringkasan Utama
Distributed locking mencegah dua instance Node.js menyentuh resource yang sama sekaligus. Redis Redlock mengambil lock pada mayoritas master Redis independen dengan TTL dan perpanjangan otomatis. Tetapi lock saja bukan jaminan correctness: Martin Kleppmann menunjukkan fencing token wajib ada, dan advisory lock Postgres sering lebih cocok untuk pekerjaan di satu basis data.
Pertama kali saya menjalankan dua salinan service Node.js yang sama di belakang load balancer, sebuah job terjadwal menyala dua kali dalam detik yang sama. Kedua replika membaca invoice tertunda yang sama, keduanya memposting ke ledger, dan seorang pelanggan tertagih dua kali. Tidak ada yang salah pada kode saya di satu mesin. Bug itu hanya muncul karena kini ada dua mesin, dan tidak ada yang tahu satu sama lain sedang aktif.
Itulah masalah mutual exclusion multi-instance. Sebuah mutex atau flag di dalam proses melindungi satu proses saja; ia tidak berarti apa-apa lintas armada. Agar hanya satu worker bertindak pada satu waktu, Anda butuh lock yang hidup di luar setiap instance, di infrastruktur bersama yang mereka semua percaya. Redis adalah alat yang paling banyak dipakai tim, dan Redlock adalah algoritma yang Redis sendiri usulkan untuk melakukannya dengan aman.
Lock satu instance adalah fondasinya
Sebelum versi terdistribusi, ada baiknya membuat satu node Redis benar dulu, karena Redlock hanyalah pola ini yang diulang di beberapa node. Anda mengambil lock dengan satu perintah atomik: set sebuah key hanya jika belum ada, lampirkan nilai acak unik, dan beri masa kedaluwarsa. Nilai acak inilah bagian yang sering dilewati orang, dan justru bagian yang menjaga keamanan Anda.
Mengapa nilai acak penting: tanpanya, sebuah client yang pekerjaannya melewati masa kedaluwarsa bisa menghapus lock yang sudah sah diambil client kedua. Jadi pelepasan tidak pernah berupa delete biasa. Anda menjalankan compare-and-delete yang menghapus key hanya jika nilai tersimpannya masih sama dengan yang Anda tulis. Dokumentasi resmi menyatakannya sebagai skrip Lua, dan Redis 8.4 menambahkan perintah native untuk pemeriksaan yang sama.
SET resource_name my_random_value NX PX 30000
# Safe release (Redis < 8.4): compare value first, then delete
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
endRedlock: kuorum lintas node independen
Satu node adalah titik kegagalan tunggal, dan setup primary-replica tidak memperbaikinya karena replikasi Redis bersifat asinkron: sebuah primary bisa mengakui sebuah lock, crash sebelum tulisan sampai ke replika, lalu replika yang dipromosikan memberikan lock yang sama ke orang lain. Redlock menghindari ini dengan menjalankan beberapa master yang sepenuhnya independen tanpa replikasi di antaranya. Setup kanoniknya adalah lima node, dan client menelusurinya secara berurutan:
- Catat waktu saat ini, lalu kirim key dan nilai acak yang sama ke seluruh N master secara paralel, masing-masing dengan timeout pendek per node agar satu node mati tidak menghambat seluruh upaya.
- Hitung keberhasilannya. Lock dianggap dipegang hanya jika mayoritas menjawab ya, yang untuk lima node berarti minimal tiga.
- Kurangi waktu yang benar-benar dihabiskan perjalanan bolak-balik dari TTL. Jika sisa masa berlaku positif dan Anda memegang mayoritas, lock menjadi milik Anda untuk jendela yang lebih pendek itu.
- Jika Anda gagal meraih mayoritas atau kehabisan masa berlaku, lepaskan setiap node segera, bahkan node yang Anda kira gagal, agar tidak ada yang menunggu kedaluwarsa.
Aturan mayoritas inilah yang memberi toleransi kegagalan: selama lebih dari separuh master hidup, lock tetap berfungsi, dan client yang butuh waktu lebih lama dari TTL untuk mengumpulkan kuorum harus menganggap lock tidak valid. Masa berlaku efektif selalu TTL dikurangi waktu akuisisi dikurangi margin kecil untuk pergeseran jam, bukan TTL penuh yang Anda minta.
node-redlock: TTL, using(), dan perpanjangan otomatis
Di Node.js, pustaka node-redlock milik mike-marcacci mengimplementasikan semua ini. Anda memberinya sebuah array client, satu per master independen, plus opsi penyetelan: driftFactor untuk margin pergeseran jam, retryCount dan retryDelay dengan retryJitter untuk mendesinkronisasi client yang bersaing, dan automaticExtensionThreshold, yaitu milidetik masa berlaku tersisa sebelum pustaka memperbarui lock sendiri.
import Client from "ioredis";
import Redlock from "redlock";
// Independent masters — no replication between them
const redlock = new Redlock(
[new Client(6379), new Client(6380), new Client(6381)],
{
driftFactor: 0.01, // multiplied by ttl to derive drift
retryCount: 10,
retryDelay: 200, // ms between attempts
retryJitter: 200, // random ms to desynchronise clients
automaticExtensionThreshold: 500, // ms left before auto-extend
}
);
// using(): auto-extends while the routine runs, releases at the end
await redlock.using(["invoice:4821"], 5000, async (signal) => {
const invoice = await loadInvoice("4821");
// If an extension failed, stop before doing damage
if (signal.aborted) throw signal.error;
await postToLedger(invoice);
});
// Manual acquire / extend / release when you need the handle
let lock = await redlock.acquire(["invoice:4821"], 5000);
try {
lock = await lock.extend(5000); // returns a NEW Lock instance
} finally {
await lock.release();
}Titik masuk paling aman adalah using(): Anda memberikan resource, sebuah durasi, dan sebuah routine, lalu pustaka memegang lock persis selama fungsi Anda berjalan, memperpanjangnya di latar belakang sebelum kedaluwarsa dan melepasnya saat Anda kembali. Ia memberi routine Anda sebuah signal, dan Anda wajib memeriksa signal.aborted sebelum langkah apa pun yang tak bisa dibatalkan, karena perpanjangan yang gagal berarti Anda mungkin tak lagi memegang lock meski kode Anda masih berjalan.
Atur TTL sepanjang durasi terlama yang Anda toleransi bila pemegang yang crash memblokir resource, bukan sepanjang durasi job. TTL pendek dengan perpanjangan otomatis jauh lebih aman daripada TTL panjang, karena jika proses mati, lock cepat terbebas, sementara perpanjangan menjaga job panjang yang sehat tetap terlindungi. Jangan pernah berasumsi lock masih milik Anda hanya karena proses Anda masih hidup.
Lock saja bukan correctness
Pada 2016 Martin Kleppmann menerbitkan kritik yang wajib dibaca setiap pengguna Redlock. Inti argumennya adalah sebuah pembedaan: Anda mengunci demi efisiensi, di mana jalan ganda yang jarang hanya membuang usaha, atau demi correctness, di mana jalan ganda merusak data. Redlock, menurutnya, cocok untuk yang pertama dan tidak aman untuk yang kedua, dan alasannya adalah timing. Jeda garbage collection stop-the-world, penundaan jaringan yang panjang, atau lompatan jam dinding bisa membuat client yakin masih memegang lock yang TTL-nya sudah kedaluwarsa.
Solusinya adalah fencing token: setiap akuisisi mengembalikan angka yang hanya pernah membesar, client mengirimnya bersama setiap tulisan, dan resource terlindungi menolak tulisan mana pun yang membawa token lebih rendah dari yang tertinggi pernah dilihatnya. Itu mengubah tulisan terlambat dari pemegang basi menjadi penolakan tak berbahaya. Redlock tidak menghasilkan token semacam itu secara default, dan nilai acak uniknya tidak monotonik, jadi jika butuh correctness Anda harus menambahkan fencing sendiri. Dokumentasi Redis kini merekomendasikan tepat hal ini.
Kapan Postgres mengerjakannya lebih baik
Gunakan Redlock hanya ketika resource yang Anda jaga hidup di luar basis data tunggal mana pun. Jika pekerjaan sudah terjadi di dalam Postgres, Postgres memberi Anda koordinasi gratis tanpa infrastruktur tambahan. Advisory lock adalah lock yang didefinisikan aplikasi, dilacak basis data tetapi tidak melekat pada baris mana pun; varian bercakupan transaksi otomatis terlepas saat transaksi berakhir, sehingga worker yang crash tak pernah meninggalkan lock tersangkut. Untuk menarik job dari tabel antrean, SKIP LOCKED membuat tiap worker mengklaim baris berbeda tanpa saling memblokir.
| Aspek | Redis Redlock | Advisory lock Postgres / SKIP LOCKED | SETNX satu node |
|---|---|---|---|
| Infrastruktur | Lima master Redis independen | Basis data yang sudah Anda jalankan | Satu node Redis |
| Toleransi kegagalan | Bertahan bila minoritas node gagal | Terikat pada ketersediaan basis data | Tidak ada, titik kegagalan tunggal |
| Correctness tanpa fencing | Tak terjamin saat jeda atau jam bergeser | Kuat, lock hidup bersama datanya | Lemah, hanya layak untuk efisiensi |
| Paling cocok | Resource tersebar lintas service | Pekerjaan sudah di dalam satu Postgres | Job satu instance non-kritis |
Advisory lock bercakupan transaksi dan SKIP LOCKED menutup sebagian besar koordinasi yang benar-benar saya butuhkan di backend Node.js, dan keduanya tak pernah menghadirkan datastore kedua yang mode kegagalannya harus saya pikirkan terpisah. Saya baru naik ke Redlock ketika hal yang dikunci benar-benar merentang sistem yang tak berbagi basis data.
Bagaimana saya memutuskan dalam praktik
Aturan saya sederhana. Jika lock demi efisiensi dan semuanya menyentuh satu Postgres, saya pakai advisory lock atau SKIP LOCKED. Jika resource merentang service, saya pakai node-redlock dengan TTL pendek dan using() untuk perpanjangan otomatis. Dan begitu correctness dipertaruhkan, saya berhenti memercayai lock itu sendiri dan menambahkan fencing token, persis seperti yang Kleppmann dan dokumentasi Redis sama-sama tegaskan. Sebuah lock mengurangi peluang tabrakan; sebuah fencing token itulah yang benar-benar membuat tulisan aman.

