Pipelining dan transaksi adalah dua teknik Redis yang sama-sama mengelompokkan banyak perintah, tetapi untuk tujuan yang berbeda. Transaksi menjamin serangkaian perintah dijalankan sebagai satu urutan yang tidak disela klien lain, sedangkan pipelining menghemat perjalanan bolak-balik data ke jaringan (round-trip) dengan mengirim banyak perintah sekaligus. Topik ini membahas cara kerja MULTI, EXEC, dan WATCH, perbedaan perintah yang ditolak pada saat diantrikan dan yang gagal pada saat eksekusi, serta penggunaan pipelining pada redis-py. Setelah membacanya, batas jaminan transaksi, perilaku error di dalamnya, dan syarat pipelining memberi manfaat performa dapat diuraikan dengan pasti. Contoh dijalankan pada server Redis 8.0.2 di port 6379 menggunakan key berawalan trx:, sehingga setiap demo dapat dijalankan ulang dengan keluaran yang sama.
Transaksi MULTI dan EXEC#
Sebuah transaksi Redis adalah antrean perintah yang dikumpulkan pada satu koneksi dan dijalankan berurutan tanpa disela. Perintah MULTI mengubah koneksi ke mode antrean, dan setiap perintah berikutnya tidak langsung dieksekusi melainkan dibalas dengan QUEUED. Perintah EXEC menjalankan seluruh antrean dan mengembalikan jawaban tiap perintah dalam urutan yang sama dengan perintahnya. Dari sudut pandang klien lain, seluruh perintah dalam satu EXEC terjadi pada saat yang sama, sehingga tidak ada perintah klien lain yang menyela di tengah antrean.
Demo berikut membentuk transaksi dua perintah untuk mengurangi stok gudang sebanyak tiga unit. Key trx:stok diisi ulang menjadi 10 pada setiap eksekusi, sehingga hasil akhirnya selalu sama.
# transaksi dua perintah: isi ulang stok lalu kurangi tiga unit
redis-cli <<'EOF'
MULTI
SET trx:stok 10
INCRBY trx:stok -3
EXEC
EOFOK
QUEUED
QUEUED
OK
7Keluaran memperlihatkan tahap antrean secara berurutan. MULTI dibalas OK, dua perintah berikutnya dibalas QUEUED karena baru masuk antrean, sedangkan EXEC menjalankan keduanya dan mengembalikan hasilnya secara berurutan, yaitu OK untuk SET dan 7 untuk INCRBY. Nilai akhir pada server dapat diperiksa dengan perintah berikut.
# membaca nilai akhir stok di server
redis-cli GET trx:stok7Perintah yang ditolak dan yang gagal saat eksekusi#
Redis membedakan dua jenis kegagalan di dalam transaksi, dan keduanya berakibat berbeda. Jenis pertama adalah perintah yang tidak dapat diantrikan karena bentuknya salah, misalnya jumlah argumennya kurang. Kesalahan semacam ini terdeteksi seketika pada saat perintah masuk antrean, dan keberadaannya membuat EXEC membatalkan seluruh transaksi dengan error EXECABORT, sehingga tidak satu pun perintah yang dijalankan. Jenis kedua adalah perintah yang sah saat diantrikan tetapi gagal saat eksekusi, misalnya operasi list atas key yang berisi string, dan kegagalan ini tidak menghentikan perintah lain dalam transaksi yang sama.
Demo berikut menyisipkan INCRBY tanpa argumen kedua ke dalam sebuah transaksi. Redis menolak perintah itu pada saat diantrikan, lalu EXEC membatalkan seluruh isinya.
# perintah dengan argumen kurang di dalam transaksi
redis-cli <<'EOF'
MULTI
SET trx:saldo 50
INCRBY trx:saldo
EXEC
EOF
# memastikan key tidak pernah dibuat
redis-cli EXISTS trx:saldoOK
QUEUED
ERR wrong number of arguments for 'incrby' command
EXECABORT Transaction discarded because of previous errors.
0Pesan pada keluaran menunjukkan urutan kejadian tersebut. Redis membalas perintah yang salah bentuk dengan ERR wrong number of arguments sebelum EXEC dikirim, kemudian EXEC dibatalkan dengan EXECABORT. Nilai 0 pada pemeriksaan terakhir memastikan key trx:saldo memang tidak pernah dibuat.
Perilaku jenis kegagalan kedua ditunjukkan demo berikut. Perintah LPUSH atas key string sah secara bentuk, sehingga berhasil masuk antrean, tetapi pasti gagal pada saat eksekusi.
# LPUSH atas key string: sah saat antri, gagal saat eksekusi
redis-cli <<'EOF'
MULTI
SET trx:nama arsip
LPUSH trx:nama item
EXEC
EOF
# membaca nilai key yang tetap berupa string
redis-cli GET trx:namaOK
QUEUED
QUEUED
OK
WRONGTYPE Operation against a key holding the wrong kind of value
arsipHasil eksekusi memperlihatkan konsekuensinya. SET tetap dijalankan, LPUSH dibalas error WRONGTYPE, dan tidak ada pembatalan atas perintah yang sudah berhasil. Redis tidak menyediakan rollback sebagaimana basis data relasional, sehingga transaksi yang salah rancang dapat meninggalkan sebagian perubahan pada data. Penanggulangannya ada pada sisi penulisan, yaitu memastikan tipe key sebelum menulis, memilih operasi atomik tunggal seperti INCRBY bila memungkinkan, atau merancang operasi yang aman untuk dijalankan ulang.
WATCH dan penguncian optimitik#
WATCH memberikan pola check-and-set, yaitu transaksi yang hanya dieksekusi bila key yang diawasi tidak berubah sejak WATCH dikirim. Perintah WATCH menandai satu atau beberapa key untuk koneksi tersebut, dan EXEC mengembalikan nil tanpa menjalankan apa pun apabila ada klien lain yang mengubah salah satu key yang diawasi sebelum EXEC dijalankan. Pada redis-py, keadaan ini muncul sebagai exception WatchError, dan penanganannya berupa perulangan, yaitu baca ulang nilai, hitung ulang, lalu coba eksekusi kembali. Pola ini disebut penguncian optimitik (optimistic locking) karena tidak ada kunci yang dipasang lebih dahulu, dan kegagalan baru diketahui pada saat EXEC.
Skrip berikut memodelkan dua operasi pengurangan atas key yang sama. Fungsi kurangi membaca nilai dalam mode WATCH, menulis pengurangannya dengan EXEC, dan mengulang seluruh proses bila WatchError dilempar. Pemanggilan kedua menyimulasikan penulis lain yang menambah nilai sebesar 100 tepat ketika WATCH masih aktif, sehingga percobaan pertamanya pasti gagal dan mekanisme percobaan ulangnya dapat diamati. Nilai awal ditetapkan tetap di awal skrip agar keluarannya selalu sama.
import redis
r = redis.Redis(host="127.0.0.1", port=6379, decode_responses=True)
KUNCI = "trx:kurs"
# nilai awal tetap agar setiap eksekusi skrip menghasilkan keluaran yang sama
r.set(KUNCI, 100)
def kurangi(jumlah, penulis_lain=False):
# pola check-and-set: amati nilai, tulis dengan EXEC, ulangi bila WATCH mendeteksi perubahan
percobaan = 0
while True:
percobaan += 1
with r.pipeline() as pipe:
try:
pipe.watch(KUNCI)
if penulis_lain:
# simulasi penulis lain yang mengubah key saat WATCH masih aktif
r.set(KUNCI, int(r.get(KUNCI)) + 100)
penulis_lain = False
sisa = int(pipe.get(KUNCI))
if sisa < jumlah:
pipe.unwatch()
return percobaan, None
pipe.multi()
pipe.decrby(KUNCI, jumlah)
pipe.execute()
return percobaan, int(r.get(KUNCI))
except redis.WatchError:
# nilai berubah sebelum EXEC, sehingga seluruh proses diulang dari pembacaan
continue
percobaan_1, sisa_1 = kurangi(30)
print("pengurang pertama:", percobaan_1, "percobaan, sisa", sisa_1)
percobaan_2, sisa_2 = kurangi(30, penulis_lain=True)
print("pengurang kedua:", percobaan_2, "percobaan, sisa", sisa_2)pengurang pertama: 1 percobaan, sisa 70
pengurang kedua: 2 percobaan, sisa 140Keluaran memperlihatkan mekanisme percobaan ulang secara utuh. Pengurang pertama menulis 70 tanpa hambatan karena tidak ada penulis lain. Pengurang kedua kehilangan percobaan pertamanya karena key berubah menjadi 170 di tengah pengawasan, dan tulisan yang dihitung dari nilai lama dibatalkan seluruhnya. Percobaan kedua membaca 170, menguranginya, lalu menulis 140, sehingga hasil akhir mengikuti nilai terbaru dan tidak ada pembaruan yang tertimpa. Untuk perubahan yang dapat dinyatakan sebagai satu perintah atomik seperti DECRBY, WATCH tidak diperlukan, dan pola ini baru relevan bila logika perhitungannya memerlukan beberapa langkah.
Pipelining dan penghematan round-trip#
Pipelining adalah teknik klien untuk mengirim banyak perintah tanpa menunggu jawaban tiap perintah terlebih dahulu, lalu membaca seluruh jawaban secara berurutan. Satu perintah yang dikirim dengan menunggu balasannya membutuhkan satu round-trip jaringan, sehingga seribu perintah kecil membutuhkan seribu round-trip. Dengan pipelining, seribu perintah yang sama dikumpulkan dalam satu pengiriman, dan jumlah perjalanan bolak-balik turun mendekati satu kali. Pipelining mengubah performa ketika beban didominasi banyak perintah kecil, karena waktu tempuh jaringan, bukan waktu eksekusi Redis, menjadi komponen terbesarnya.
Pada redis-py, objek pipeline dibuat dengan r.pipeline(). Secara default objek ini membungkus perintah kirimannya dalam MULTI dan EXEC, sehingga pengirimannya membentuk transaksi, sedangkan argumen transaction=False menonaktifkan pembungkus itu dan menghasilkan pipelining murni. Skrip berikut menulis 1000 key yang sama dua kali, tanpa pipeline dan dengan pipeline, lalu mencetak jumlah key yang berhasil ditulis. Jumlah tersebut mengukur keberhasilan, bukan kecepatan, karena manfaat pipelining terletak pada jumlah round-trip yang dihemat.
import redis
r = redis.Redis(host="127.0.0.1", port=6379, decode_responses=True)
# hapus sisa demo sebelumnya agar penghitungan selalu dimulai dari nol
for k in r.scan_iter("trx:bulk:*"):
r.delete(k)
# menulis 1000 key tanpa pipeline: setiap SET menunggu satu jawaban dari server
berhasil_tanpa = 0
for i in range(1000):
if r.set(f"trx:bulk:{i}", f"nilai-{i}"):
berhasil_tanpa += 1
# menulis 1000 key yang sama dengan pipeline: seribu perintah dikirim sekaligus
pipe = r.pipeline(transaction=False)
for i in range(1000):
pipe.set(f"trx:bulk:{i}", f"nilai-{i}")
jawaban = pipe.execute()
berhasil_dengan = sum(1 for j in jawaban if j)
print("key berhasil ditulis tanpa pipeline:", berhasil_tanpa)
print("key berhasil ditulis dengan pipeline:", berhasil_dengan)
print("jumlah key trx:bulk* saat ini:", len(list(r.scan_iter("trx:bulk:*"))))key berhasil ditulis tanpa pipeline: 1000
key berhasil ditulis dengan pipeline: 1000
jumlah key trx:bulk* saat ini: 1000Kedua cara melaporkan 1000 key berhasil karena semua perintahnya memang berhasil. Perbedaannya berada di jalur jaringan. Versi tanpa pipeline menunggu satu jawaban sebelum mengirim SET berikutnya, sedangkan versi dengan pipeline mengirim seluruh SET lebih dahulu. Perbedaan itu baru terasa pada latensi antara klien dan server, dan penghitungan sengaja dipilih sebagai ukuran agar keluarannya tidak berubah antar eksekusi.
Kesalahan umum#
Tiga kesalahan berikut paling sering muncul ketika transaksi dan pipelining digunakan bersamaan. Masing-masing memiliki penyebab yang jelas dan perbaikan yang sederhana bila diketahui lebih awal.
- Mengharapkan rollback atas perintah yang gagal saat eksekusi. Kebiasaan ini dibawa dari transaksi basis data relasional. Perbaikannya berupa validasi tipe key sebelum menulis atau perancangan operasi yang aman untuk dijalankan ulang.
- Menganggap satu koneksi aman untuk banyak utas sekaligus. MULTI, WATCH, dan antrean pipelining menempel pada koneksi, sehingga perintah dari utas lain yang meminjam koneksi yang sama dapat bersisipan di tengah transaksi. Perbaikannya berupa satu koneksi untuk setiap transaksi atau setiap utas.
- Mengirim pipeline berukuran sangat besar sekaligus. Seluruh perintah dan jawabannya menumpuk di memori server dan klien sampai semuanya selesai dijalankan. Perbaikannya berupa pembagian kirim menjadi kelompok yang berukuran wajar.
Ringkasan#
- MULTI mengubah koneksi ke mode antrean, dan EXEC menjalankan seluruh antrean secara berurutan tanpa disela perintah klien lain.
- Perintah yang bentuknya salah membuat EXEC dibatalkan dengan EXECABORT sehingga tidak ada yang dijalankan, sedangkan perintah yang gagal saat eksekusi tidak memengaruhi perintah lain, dan Redis tidak melakukan rollback.
- WATCH menerapkan pola check-and-set, yaitu EXEC hanya berhasil bila key yang diawasi tidak berubah, dengan penanganan berupa perulangan baca ulang pada redis-py melalui exception
WatchError. - Pipelining menghemat satu round-trip untuk setiap perintah dan memberi manfaat terbesar pada beban yang didominasi perintah kecil, dengan
pipeline(transaction=False)sebagai bentuk murninya pada redis-py.
Referensi#
- Transactions — Redis — Penjelasan resmi transaksi Redis beserta aturan error di dalamnya dan ketiadaan rollback.
- EXEC — Redis — Dokumen perintah EXEC, termasuk pembatalan transaksi dengan EXECABORT bila ada perintah yang ditolak saat antri.
- WATCH — Redis — Dokumen perintah WATCH untuk pola check-and-set beserta perilaku pembatalan EXEC.
- Pipelining — Redis — Penjelasan resmi pipelining beserta penghematan round-trip per perintah.