Lock terdistribusi

Lock dengan SET NX EX: akuisisi, pelepasan aman lewat token, dan batas semantik Redlock.

Daftar isi

Topik ini membahas lock terdistribusi di Redis, yaitu mekanisme yang menjamin satu pekerjaan hanya dijalankan oleh satu klien dari banyak proses yang berjalan bersamaan. Pembahasan mencakup konsepnya, akuisisi melalui perintah SET dengan opsi NX dan EX, pelepasan aman melalui token dan skrip Lua, implementasi lengkap dengan redis-py, serta batas jaminan pola ini beserta kesadaran atas Redlock. Seluruh contoh menggunakan awalan key lock: pada satu server Redis, sehingga setiap keluaran dapat direproduksi ulang.

Konsep lock terdistribusi#

Lock adalah hak eksklusif atas satu sumber daya bersama, sedangkan lock terdistribusi adalah lock yang dikelola oleh satu titik yang sama bagi proses-proses yang berjalan di mesin berbeda. Kebutuhan seperti ini muncul ketika satu pekerjaan dapat diambil oleh beberapa instans aplikasi sekaligus, misalnya tugas rekap harian yang dijadwalkan pada dua worker di dua server berbeda. Tanpa koordinasi, kedua worker menjalankan tugas yang sama pada rentang waktu yang berdekatan dan hasilnya menjadi ganda. Situasi semacam ini merupakan contoh race condition, yakni keadaan ketika hasil eksekusi bergantung pada urutan kapan masing-masing proses sampai di titik yang sama.

Semua klien harus menunjuk titik yang sama, dan Redis lazim digunakan untuk peran tersebut karena setiap perintah dijalankan atomik oleh server. Perintah yang atomik tidak dapat diselingi perintah klien lain, sehingga dua klien yang berebut pada saat bersamaan selalu menghasilkan tepat satu pemenang. Diagram berikut memperlihatkan perebutan lock oleh dua klien yang memanggil perintah yang sama pada key yang sama.

Klien Aberhasil: OKKlien Bditolak: nilSET NX EX 10SET NX EX 10Server Redislock:tugas-harian
Dua klien berebut lock pada satu server Redis

Nilai yang disimpan pada key lock berfungsi sebagai token pemilik, yaitu penanda identitas yang digunakan kembali pada saat pelepasan. Pemeriksaan ketersediaan key dan penulisannya harus terjadi dalam satu langkah, karena urutan pemeriksaan lalu penulisan yang dilakukan dari sisi klien menyisakan celah di antara keduanya. Perintah SET dengan opsi NX menggabungkan kedua langkah tersebut di server.

Akuisisi dengan SET NX EX#

Akuisisi lock dilakukan dengan satu perintah SET yang membawa opsi NX dan EX sekaligus. Opsi NX menyimpan nilai hanya apabila key belum ada, sedangkan opsi EX memasang masa hidup dalam satuan detik. Server menjawab OK apabila key berhasil ditulis dan nil apabila key sudah ada, sehingga keberhasilan akuisisi langsung terbaca dari balasan perintah. redis-py menerjemahkan kedua balasan tersebut menjadi True dan None.

Pola lama memisahkan langkah yang sama menjadi dua perintah, yaitu SETNX diikuti EXPIRE. Jeda di antara keduanya menyisakan risiko yang serius apabila proses terputus tepat setelah SETNX berhasil tetapi sebelum EXPIRE dijalankan. Key lock tertinggal tanpa masa hidup, tidak pernah terbuka lagi, dan seluruh klien berikutnya ditolak selamanya, keadaan yang disebut deadlock dalam konteks lock. Menggabungkan pemeriksaan dan pemasangan masa hidup dalam satu perintah SET menghilangkan jeda tersebut karena server menjalankannya sebagai satu langkah.

Demo berikut memperlihatkan semantik balasan tersebut langsung di redis-cli. Key dibersihkan terlebih dahulu, klien A mengunci key yang belum ada, lalu klien B mencoba mengunci key yang sudah terkunci.

bash
# menghapus sisa key dari percobaan sebelumnya
redis-cli DEL lock:tugas-harian > /dev/null
# klien A mencoba mengunci key yang belum ada
redis-cli SET lock:tugas-harian "token-42" NX EX 10
# klien B mencoba mengunci key yang sudah terkunci
redis-cli SET lock:tugas-harian "token-77" NX EX 10
# sisa masa hidup lock milik klien A
redis-cli TTL lock:tugas-harian
text Keluaran
OK

10

Balasan pertama OK menandakan klien A memegang lock, sedangkan baris kosong setelahnya adalah tampilan redis-cli untuk balasan nil, yaitu penolakan NX terhadap klien B. Nilai 10 pada TTL menunjukkan masa hidup terpasang penuh karena pembacaan dilakukan seketika setelah penguncian. Masa hidup inilah jaring pengaman pola ini, karena key lock tetap hilang dengan sendirinya apabila pemiliknya berhenti sebelum sempat melepas lock.

Pelepasan aman dengan token#

Pelepasan lock tidak boleh dilakukan sembarang klien, karena perintah DEL yang dijalankan klien yang salah dapat mencabut lock milik klien lain. Skenarionya seperti berikut. Klien A masih menjalankan pekerjaan ketika masa hidup locknya habis, kemudian klien B mengunci key yang sama. Apabila klien A yang baru selesai menghapus key dengan DEL, lock milik klien B ikut tercabut, sehingga klien C pun dapat mengunci dan dua pekerjaan berjalan bersamaan.

Pelepasan yang aman melekatkan token, yaitu nilai unik yang dihasilkan pemilik pada saat akuisisi dan disimpan sebagai nilai key. Pelepasan menghapus key hanya apabila nilai yang tersimpan cocok dengan token yang dikirim. Perbandingan dan penghapusan tidak boleh dijalankan sebagai dua perintah terpisah, karena jeda di antara keduanya dapat diisi penguncian baru oleh klien lain. Skrip Lua menjalankan kedua langkah secara atomik di server melalui perintah EVAL, dan Redis menangguhkan perintah klien lain selama skrip berjalan.

lua
-- tidak-dijalankan
-- skrip pelepasan lock: perbandingan token dan penghapusan berjalan sebagai satu langkah atomik
if redis.call('get', KEYS[1]) == ARGV[1] then
    return redis.call('del', KEYS[1])
else
    return 0
end

Skrip tersebut membaca nilai key melalui redis.call('get', KEYS[1]) dan menghapusnya melalui redis.call('del', KEYS[1]) hanya apabila nilainya sama dengan ARGV[1], yaitu token yang dikirim pemilik. Server menjawab 1 apabila lock berhasil dihapus dan 0 apabila token tidak cocok atau key sudah tidak ada. Skrip ini digunakan kembali oleh implementasi lengkap pada bagian berikutnya.

Implementasi lengkap dengan redis-py#

Berikut adalah lock.py, satu berkas utuh yang menggabungkan akuisisi dan pelepasan aman dalam satu skrip. Fungsi akuisisi membungkus SET dengan nx=True dan ex=10, sedangkan fungsi lepas mengirim skrip pelepasan melalui r.eval. Urutan demonstrasi membersihkan key terlebih dahulu, lalu dua klien mengunci, klien B mencoba melepas dengan token miliknya, klien A melepas, dan pelepasan yang sama diulang.

python lock.py
import redis

r = redis.Redis(host="127.0.0.1", port=6379, decode_responses=True)
KEY = "lock:tugas-harian"
TTL = 10

# skrip pelepasan: key dihapus hanya bila nilai key cocok dengan token pemilik
SKRIP_LEPAS = """
if redis.call('get', KEYS[1]) == ARGV[1] then
    return redis.call('del', KEYS[1])
else
    return 0
end
"""


def akuisisi(token):
    """Mengunci KEY dengan SET NX EX dan mengembalikan True bila berhasil."""
    # redis-py memberi True bila key ditulis dan None bila NX ditolak
    return bool(r.set(KEY, token, nx=True, ex=TTL))


def lepas(token):
    """Melepas KEY hanya bila token cocok, dievaluasi atomik oleh server."""
    # server memberi 1 bila lock dihapus dan 0 bila token tidak cocok
    return r.eval(SKRIP_LEPAS, 1, KEY, token) == 1


# membersihkan key demo agar urutan hasil selalu sama
r.delete(KEY)

print(f"klien A mengunci: {akuisisi('token-42')}")
print(f"klien B mengunci: {akuisisi('token-77')}")
print(f"klien B mencoba melepas: {lepas('token-77')}")
print(f"klien A melepas: {lepas('token-42')}")
print(f"klien A melepas ulang: {lepas('token-42')}")
text Keluaran
klien A mengunci: True
klien B mengunci: False
klien B mencoba melepas: False
klien A melepas: True
klien A melepas ulang: False

Keluaran mengikuti urutan yang diharapkan dan setiap nilainya dapat dijelaskan dari mekanismenya. Klien A memperoleh True karena key belum ada, sedangkan klien B memperoleh False karena NX menolak penulisan pada key yang sudah terkunci. Pelepasan klien B bernilai False karena token-77 tidak cocok dengan nilai yang tersimpan, pelepasan klien A bernilai True karena token-42 adalah pemegang key tersebut, dan pelepasan ulang bernilai False karena key sudah tidak ada. Token pada contoh dibuat tetap agar keluarannya dapat direproduksi, sedangkan pada kebutuhan nyata token dihasilkan unik untuk setiap pemegang.

Batasan jaminan dan kesadaran Redlock#

Lock di Redis adalah jaminan sisi klien dan bukan transaksi server. Server hanya menahan key sampai masa hidupnya habis atau dihapus, dan tidak ada pekerjaan yang dihentikan ketika status lock berubah. Apabila masa hidup habis ketika pemilik masih bekerja, pekerjaan tersebut berlanjut tanpa lock, sehingga klien lain dapat mengunci dan menjalankan pekerjaan yang sama. Konsekuensinya, desain pekerjaan perlu mengantisipasi eksekusi ganda. Antisipasi yang lazim adalah idempotensi (idempotency), yakni sifat pekerjaan yang hasil akhirnya tidak berubah meskipun dijalankan ulang, atau fencing token (fencing token), yaitu nomor yang naik setiap kali lock diberikan dan diperiksa sistem penyimpanan sebelum hasil pekerjaan ditulis.

Apabila satu server tidak memadai, dokumentasi Redis menyediakan algoritme Redlock, yaitu pola lock yang diperoleh pada beberapa server Redis yang berdiri sendiri dan dianggap berhasil bila diperoleh pada mayoritas server dalam rentang waktu singkat. Redlock mengurangi ketergantungan pada satu server, tetapi menambah kompleksitas operasi dan tetap bersandar pada masa hidup yang diukur oleh jam pada tiap mesin. Keandalan algoritme ini pernah menjadi perdebatan terbuka di komunitas karena perilaku jam dan kedaluwarsa yang berbeda antar mesin, sehingga penerapannya perlu disesuaikan dengan kebutuhan sistem. Untuk mayoritas kebutuhan, lock satu server dengan akuisisi atomik, pelepasan berbasis token, dan masa hidup yang tepat sudah memadai, sedangkan keputusan yang berisiko tinggi tetap memerlukan pengaman berupa fencing token atau idempotensi.

Ringkasan#

  • Akuisisi lock dilakukan dengan satu perintah SET key token NX EX ttl yang atomik, sehingga pemeriksaan ketersediaan dan pemasangan masa hidup tidak dapat dipisahkan oleh jeda.
  • Masa hidup menjadi jaring pengaman ketika pemilik lock mati, dan nilai yang tepat sebanding dengan durasi pekerjaan yang dijamin.
  • Pelepasan dilakukan hanya oleh pemegang token melalui skrip Lua yang membandingkan nilai key dengan token dan menghapus key secara atomik melalui EVAL.
  • Lock di Redis adalah jaminan sisi klien dan bukan transaksi server, sehingga pekerjaan kritis tetap memerlukan idempotensi atau fencing token, sedangkan Redlock ditambahkan hanya apabila lock antar banyak server benar-benar dibutuhkan.

Referensi#

  • SET — Redis — Dokumentasi opsi NX dan EX beserta perilaku atomik yang menjadi dasar akuisisi lock.
  • SETNX — Redis — Perintah pemeriksaan ketersediaan key yang pemasangan masa hidupnya dulu dilakukan terpisah.
  • EVAL — Redis — Eksekusi skrip Lua di server secara atomik yang digunakan untuk pelepasan berbasis token.
  • Distributed Locks with Redis — Redis — Pola resmi lock terdistribusi beserta algoritme Redlock dan pembahasan batas keandalannya.