Topik ini mengimplementasikan kontrak data proyek papan peringkat (leaderboard) kuis kelas menjadi satu skrip Python utuh bernama papan_skor.py. Skrip mengelola dua key sesuai kontrak, yaitu sorted set kuis:papan:kuis-01 untuk skor kumulatif peserta dan hash kuis:meta:kuis-01 untuk metadata kuis. Seluruh skenarionya bernilai tetap, sehingga setiap eksekusi menghasilkan keluaran yang persis sama. Penanganan kegagalan koneksi disediakan oleh fungsi pembungkus (wrapper) coba_ulang yang mengulang operasi tulis paling banyak tiga kali. Contoh dijalankan pada server Redis 8.0.2 di port 6379 menggunakan redis-py dengan key berawalan kuis:, sehingga skrip aman dijalankan berulang pada server pengembangan yang digunakan bersama.
Kontrak data yang diterjemahkan menjadi kode#
Kontrak data proyek menetapkan dua key dengan nama dan peran yang pasti. Sorted set kuis:papan:kuis-01 menyimpan empat peserta sebagai anggota, yaitu Ali, Budi, Citra, dan Dewi, sedangkan skornya berupa nilai kumulatif hasil jawaban benar. Setiap jawaban benar bernilai 10 poin dan dicatat dengan ZINCRBY karena skor selalu berubah secara relatif terhadap nilai semula. Papan lengkap dibaca dengan ZREVRANGE beserta opsi WITHSCORES, sedangkan posisi satu peserta dibaca dengan ZREVRANK.
Hash kuis:meta:kuis-01 menyimpan tiga field, yaitu judul, jumlah_soal, dan status. Field status bernilai berjalan selama kuis berlangsung dan berubah menjadi selesai pada akhir skenario. Kontrak melarang masa hidup (time to live, TTL) pada kedua key selama kuis berjalan, sehingga skrip tidak memasang TTL sama sekali dan hash meta bertahan sebagai catatan setelah kuis usai. Kontrak juga menetapkan ZREVRANGE sebagai perintah papan meskipun perintah itu tercatat legacy karena setara dengan ZRANGE REV, dan penamaannya tetap mengikuti kontrak demi keselarasan dengan rancangan data.
| Kebutuhan | Perintah Redis | Method redis-py | Method PapanSkor |
|---|---|---|---|
| Mengisi metadata kuis | HSET | hset(mapping=…) | daftarkan_meta |
| Mencatat satu jawaban benar | ZINCRBY | zincrby | catat_skor |
| Membaca papan lengkap | ZREVRANGE WITHSCORES | zrevrange(withscores=True) | papan |
| Membaca posisi satu peserta | ZREVRANK | zrevrank | posisi |
| Menutup kuis | HSET field status | hset | akhiri |
Skenario lima ronde yang bernilai tetap#
Skenario kuis terdiri atas lima ronde, dan setiap ronde mewakili satu soal yang dapat dijawab benar oleh lebih dari satu peserta. Daftar jawaban benar per ronde ditulis sebagai nilai tetap di dalam skrip, bukan dihasilkan secara acak, sehingga jalannya skenario dapat diikuti langsung dari kodenya. Kelima ronde tersebut adalah sebagai berikut.
- Ronde pertama dijawab benar oleh Ali.
- Ronde kedua dijawab benar oleh Budi.
- Ronde ketiga dijawab benar oleh Ali dan Citra.
- Ronde keempat dijawab benar oleh Citra dan Dewi.
- Ronde kelima dijawab benar oleh Ali, Budi, dan Citra.
Ali dan Citra masing-masing menjawab benar tiga kali sehingga mencapai 30, Budi menjawab benar dua kali sehingga mencapai 20, sedangkan Dewi menjawab benar sekali sehingga mencapai 10. Field jumlah_soal pada hash meta diisi 5 sesuai banyaknya ronde. Kombinasi ini sengaja menghadirkan dua peserta berskor sama di puncak papan agar perilaku Redis atas skor yang sama ikut teramati.
Setiap eksekusi dimulai dengan pembersihan key milik proyek sendiri. Skrip memindai key berpola kuis:* menggunakan SCAN lalu menghapus hasilnya, sehingga sisa data dari eksekusi sebelumnya tidak memengaruhi keluaran. Cara ini menyentuh key berawalan kuis: saja dan tidak menyentuh key milik aplikasi lain pada server yang sama. Karena daftar ronde beserta nilainya tetap, skrip selalu mencetak keluaran yang sama tanpa melibatkan waktu maupun angka acak.
Kelas PapanSkor dan pembungkus coba_ulang#
Kelas PapanSkor menerima objek klien redis-py beserta dua nama key pada konstruktor, sehingga kelas yang sama dapat digunakan untuk kuis lain cukup dengan mengganti nama key-nya. Lima method kelas dipetakan satu banding satu dengan baris pada tabel kontrak di atas. Method pembaca dijalankan langsung karena kegagalannya tidak meninggalkan jejak pada data, sedangkan ketiga method penulis dibungkus dengan coba_ulang.
Fungsi coba_ulang menangani exception redis.exceptions.ConnectionError, yaitu kegagalan yang muncul ketika klien tidak dapat mencapai server, misalnya saat redis-server baru saja dimulai ulang. Fungsi ini menjalankan operasi yang diberikan, menunggu jeda tetap setengah detik apabila koneksi gagal, lalu mencoba lagi paling banyak tiga kali. Apabila percobaan ketiga juga gagal, error dinaikkan kembali ke pemanggil agar kegagalan tidak berhenti tanpa pesan. Nilai kembalian operasi langsung diteruskan, sehingga jalur sukses skrip tidak tersentuh mekanisme pengulangan ini.
import time
import redis
# key proyek mengikuti kontrak data pada tahap desain
KUNCI_PAPAN = "kuis:papan:kuis-01"
KUNCI_META = "kuis:meta:kuis-01"
# jawaban benar tiap ronde, satu jawaban benar bernilai 10 poin
RONDE = [
("Ali",),
("Budi",),
("Ali", "Citra"),
("Citra", "Dewi"),
("Ali", "Budi", "Citra"),
]
def coba_ulang(operasi, maksimal=3, jeda=0.5):
# jalankan operasi tulis dan ulangi saat koneksi terputus, maksimal tiga kali
for percobaan in range(1, maksimal + 1):
try:
return operasi()
except redis.exceptions.ConnectionError:
if percobaan == maksimal:
raise
time.sleep(jeda)
class PapanSkor:
def __init__(self, klien, kunci_papan, kunci_meta):
self.klien = klien
self.kunci_papan = kunci_papan
self.kunci_meta = kunci_meta
def daftarkan_meta(self, judul, jumlah_soal):
# isi hash meta kuis, tanpa TTL selama kuis berjalan
def tulis():
return self.klien.hset(self.kunci_meta, mapping={
"judul": judul,
"jumlah_soal": jumlah_soal,
"status": "berjalan",
})
return coba_ulang(tulis)
def catat_skor(self, peserta, poin):
# ZINCRBY menaikkan skor kumulatif peserta secara relatif
return coba_ulang(lambda: self.klien.zincrby(self.kunci_papan, poin, peserta))
def papan(self):
# ZREVRANGE WITHSCORES mengembalikan papan urut menurun beserta skornya
return self.klien.zrevrange(self.kunci_papan, 0, -1, withscores=True)
def posisi(self, peserta):
# ZREVRANK mengembalikan posisi mulai dari nol dalam urutan menurun
return self.klien.zrevrank(self.kunci_papan, peserta)
def akhiri(self):
# tutup kuis dengan mengubah field status pada hash meta
return coba_ulang(lambda: self.klien.hset(self.kunci_meta, "status", "selesai"))
def bersihkan_key_proyek(klien):
# SCAN lalu delete hanya atas key berawalan kuis: agar skrip idempotent
for key in klien.scan_iter("kuis:*"):
klien.delete(key)
def utama():
klien = redis.Redis(host="127.0.0.1", port=6379, decode_responses=True)
bersihkan_key_proyek(klien)
papan = PapanSkor(klien, KUNCI_PAPAN, KUNCI_META)
papan.daftarkan_meta("Kuis Matematika Kelas", 5)
for nomor, jawaban_benar in enumerate(RONDE, start=1):
for peserta in jawaban_benar:
papan.catat_skor(peserta, 10)
print(f"ronde {nomor}: jawaban benar {', '.join(jawaban_benar)}")
print()
print("Papan skor akhir")
print("----------------")
for peringkat, (peserta, skor) in enumerate(papan.papan(), start=1):
print(f"{peringkat}. {peserta:<6} {int(skor):>3}")
posisi_citra = papan.posisi("Citra")
print()
print(f"Posisi Citra: ZREVRANK {posisi_citra}, peringkat {posisi_citra + 1}")
papan.akhiri()
print("Status kuis pada hash meta: selesai")
if __name__ == "__main__":
utama()ronde 1: jawaban benar Ali
ronde 2: jawaban benar Budi
ronde 3: jawaban benar Ali, Citra
ronde 4: jawaban benar Citra, Dewi
ronde 5: jawaban benar Ali, Budi, Citra
Papan skor akhir
----------------
1. Citra 30
2. Ali 30
3. Budi 20
4. Dewi 10
Posisi Citra: ZREVRANK 0, peringkat 1
Status kuis pada hash meta: selesaiKeluaran memperlihatkan alur lengkap dari ronde pertama sampai status akhir. Ali dan Citra sama-sama mencapai 30, dan pada papan urut menurun anggota berskor sama diurutkan menurut byte namanya secara terbalik, sehingga Citra tampil sebelum Ali. ZREVRANK Citra mengembalikan 0 karena posisi dihitung mulai dari nol, sedangkan nomor peringkat pada tabel dihitung sebagai posisi ditambah satu. Baris terakhir mengonfirmasi field status pada hash meta sudah berubah menjadi selesai.
Menjalankan skrip dan memeriksa keadaan akhir#
Jalankan skrip dua kali berturut-turut dari folder tempat papan_skor.py berada. Skrip kedua bekerja pada keadaan server yang sudah berisi data dari skrip pertama, sehingga kedua eksekusi itu membuktikan sifat idempotent skrip, yaitu eksekusi ulang yang tidak mengubah hasilnya. Kedua pemanggilan sengaja ditulis dalam satu blok agar keluarannya dapat dibandingkan langsung.
# menjalankan skrip pertama kali
python3 papan_skor.py
# menjalankan ulang skrip untuk membuktikan keluarannya identik
python3 papan_skor.pyronde 1: jawaban benar Ali
ronde 2: jawaban benar Budi
ronde 3: jawaban benar Ali, Citra
ronde 4: jawaban benar Citra, Dewi
ronde 5: jawaban benar Ali, Budi, Citra
Papan skor akhir
----------------
1. Citra 30
2. Ali 30
3. Budi 20
4. Dewi 10
Posisi Citra: ZREVRANK 0, peringkat 1
Status kuis pada hash meta: selesai
ronde 1: jawaban benar Ali
ronde 2: jawaban benar Budi
ronde 3: jawaban benar Ali, Citra
ronde 4: jawaban benar Citra, Dewi
ronde 5: jawaban benar Ali, Budi, Citra
Papan skor akhir
----------------
1. Citra 30
2. Ali 30
3. Budi 20
4. Dewi 10
Posisi Citra: ZREVRANK 0, peringkat 1
Status kuis pada hash meta: selesaiKedua keluaran identik baris demi baris karena daftar ronde, nilai skornya, dan pembersihan awal selalu sama. Pembersihan SCAN pada awal eksekusi itulah penyebabnya, bukan keadaan server yang kebetulan kosong. Sifat ini membuat skrip layak digunakan sebagai demo yang dapat dijalankan ulang kapan pun tanpa perlu memeriksa keadaan server terlebih dahulu.
Keadaan akhir pada server juga dapat diperiksa langsung dengan redis-cli. Perintah HGETALL menampilkan seluruh field hash meta beserta nilainya, sedangkan perintah TTL membaca sisa masa hidup key. Kedua perintah mengasumsikan skrip sebelumnya baru saja dijalankan, sehingga keadaan yang diperiksa adalah keadaan yang baru dibentuk.
# membaca seluruh field hash meta setelah kuis selesai
redis-cli HGETALL kuis:meta:kuis-01
# memastikan hash meta tidak memiliki masa hidup
redis-cli TTL kuis:meta:kuis-01judul
Kuis Matematika Kelas
jumlah_soal
5
status
selesai
-1HGETALL menampilkan tiga field sesuai kontrak dengan selesai sebagai nilai akhir field status. TTL melaporkan -1, artinya key tidak memiliki masa hidup dan tidak akan kedaluwarsa dengan sendirinya. Hash meta yang bertahan ini menjadi catatan hasil kuis yang dapat dibaca aplikasi lain setelah skrip berakhir.
Kesalahan umum#
Tiga kesalahan berikut paling sering muncul ketika pola pada topik ini dipraktikkan. Masing-masing disertai penyebab dan perbaikannya.
Kesalahan pertama adalah menghitung skor baru di sisi aplikasi lalu menimpanya dengan ZADD. Cara ini membaca skor lama, menambahkannya di memori aplikasi, dan menulis hasilnya kembali, sehingga tulisan dari proses lain yang terjadi di tengah proses tersebut dapat tertimpa. Perbaikannya berupa ZINCRBY yang menghitung kenaikannya di server secara atomik.
Kesalahan kedua adalah menganggap coba_ulang menjamin setiap tulisan diterapkan tepat satu kali. Pembungkus ini hanya memulihkan kegagalan sebelum perintah mencapai server, sedangkan apabila koneksi terputus setelah server menerima tulisan, pengulangan dapat menerapkan operasi yang sama dua kali. Selain itu, hanya ConnectionError yang diulang, sedangkan error lain seperti WRONGTYPE langsung dinaikkan karena mencoba ulang atas error yang pasti muncul kembali hanya membuang waktu. Perbaikannya berupa pemilihan operasi yang aman diulang atau pemeriksaan keadaan sebelum mencoba lagi.
Kesalahan ketiga adalah membersihkan data uji dengan FLUSHDB atau FLUSHALL karena kelihatannya lebih singkat. Perintah itu menghapus seluruh key pada server tanpa membedakan pemiliknya, sedangkan KEYS tanpa pola penyaring juga membebani server pada kumpulan key yang besar. Perbaikannya sudah diterapkan skrip, yaitu SCAN dengan pola kuis:* yang hanya menghapus key milik proyek sendiri.
Ringkasan#
- Kontrak data dipetakan langsung ke kelas PapanSkor, yaitu ZINCRBY untuk pencatatan skor, ZREVRANGE WITHSCORES untuk papan, ZREVRANK untuk posisi, serta HSET untuk hash meta judul, jumlah_soal, dan status tanpa masa hidup.
- Skenario lima ronde bernilai tetap menghasilkan papan akhir Citra 30, Ali 30, Budi 20, dan Dewi 10 pada setiap eksekusi karena key
kuis:*dibersihkan dengan SCAN di awal. coba_ulangmengulang operasi tulis paling banyak tiga kali dengan jeda tetap setengah detik saat ConnectionError, lalu menaikkan kembali error bila percobaan terakhir tetap gagal.- Anggota berskor sama diurutkan menurut byte nama secara terbalik pada papan menurun, dan nomor peringkat dihitung sebagai ZREVRANK ditambah satu.
Referensi#
- Sorted sets — Redis — Dokumentasi tipe data sorted set beserta perilaku urutan anggota berskor sama.
- ZINCRBY — Redis — Penambahan skor relatif pada sorted set beserta balasan skor terbarunya.
- ZREVRANGE — Redis — Papan urut menurun dengan opsi WITHSCORES beserta status legacy perintahnya.
- ZREVRANK — Redis — Posisi anggota pada urutan menurun yang dihitung mulai dari nol.
- HSET — Redis — Pengisian satu atau beberapa field hash dalam satu perintah.
- redis-py documentation — redis-py — Dokumentasi klien redis-py yang mencakup kelas Redis dan exception ConnectionError.