Masalah Dual Write
Layanan menyimpan pesanan ke basis data, lalu menerbitkan peristiwa ke Kafka. Dua sistem, dua penulisan, tanpa transaksi bersama.
simpanKeBasisData(pesanan) ← berhasil terbitKeKafka(peristiwa) ← proses mati di sini Hasil: pesanan ada di basis data, tetapi tidak ada yang pernah tahu. Layanan pengiriman tidak pernah menerima peristiwanya.
Menerbitkan ke Kafka lebih dulu menghasilkan kegagalan yang berlawanan tetapi sama buruknya: peristiwa tersebar dan diproses hilir, sementara transaksi basis data gagal — seluruh sistem kini yakin akan pesanan yang tidak pernah ada. Tidak ada urutan yang benar; masalahnya struktural.
Pola Outbox
Tulis peristiwa ke tabel outbox di dalam transaksi basis data yang sama dengan perubahan data. Proses terpisah kemudian mengirimkannya ke Kafka.
BEGIN;
INSERT INTO pesanan (id, total, status) VALUES (123, 250000, 'dibuat');
INSERT INTO outbox (id_peristiwa, agregat, kunci, payload)
VALUES (gen_random_uuid(), 'pesanan', '123', '{...}');
COMMIT; ← atomik: keduanya jadi, atau keduanya batal
Relay (Debezium CDC pada tabel outbox, atau poller terjadwal)
↳ baca baris outbox → kirim ke Kafka → tandai terkirim
| Cara relay | Kelebihan | Kekurangan |
|---|---|---|
| Debezium CDC pada tabel outbox | Latensi rendah, tanpa polling | Perlu menjalankan Connect dan Debezium |
| Poller terjadwal | Sederhana, tanpa infrastruktur baru | Latensi sebesar interval polling; beban kueri berkala |
Relay bisa mati setelah mengirim ke Kafka tetapi sebelum menandai baris terkirim, sehingga peristiwa itu dikirim ulang. Itu sebabnya id_peristiwa disertakan sejak awal — konsumen memakainya untuk deduplikasi.
Konsumen Idempoten
Karena Kafka pada praktiknya memberi at-least-once, pemrosesan harus aman diulang. Ada tiga cara, dari yang paling disukai:
1. Operasi yang secara alami idempoten
UPDATE pesanan SET status='dibayar' WHERE id=123;
→ dijalankan sepuluh kali, hasilnya sama
2. Upsert dengan kunci alami
INSERT INTO ringkasan (id, total) VALUES (123, 250000)
ON CONFLICT (id) DO UPDATE SET total = EXCLUDED.total;
3. Tabel deduplikasi eksplisit
INSERT INTO peristiwa_terproses (id_peristiwa) VALUES ('evt-abc')
ON CONFLICT DO NOTHING RETURNING id_peristiwa;
→ tidak ada baris kembali berarti sudah pernah diproses, lewati
Tabel peristiwa_terproses tumbuh selamanya bila dibiarkan. Simpan hanya jendela waktu yang lebih panjang daripada retensi topik ditambah kemungkinan replay — misalnya 30 hari — dan hapus sisanya secara terjadwal. Beri indeks pada kolom waktu agar pembersihan tidak menjadi kueri berat.
UPDATE saldo SET jumlah = jumlah + 50000 menghasilkan hasil berbeda setiap kali diulang. Operasi akumulatif seperti ini wajib dilindungi kunci deduplikasi — inilah kategori bug yang berakhir sebagai selisih uang, dan biasanya baru ditemukan saat rekonsiliasi.
Retry dan Dead Letter Queue
Kafka tidak punya redelivery per pesan. Bila satu pesan selalu gagal, konsumen yang mengulanginya terus akan memblokir seluruh partisi.
pesan-1 ✓ pesan-2 ✗ gagal terus pesan-3 … pesan-4 …
▲
seluruh partisi berhenti di sini selamanya
Bedakan dua jenis kegagalan:
| Jenis | Contoh | Penanganan |
|---|---|---|
| Sementara | Basis data timeout, layanan hilir 503 | Retry dengan backoff — pesan berikutnya memang harus menunggu |
| Permanen | Pesan rusak, skema tidak valid, id tidak ada | Kirim ke DLQ dan lanjut — retry tidak akan pernah berhasil |
Pola topik retry berjenjang:
pesanan → gagal → pesanan.retry.5m
→ pesanan.retry.30m
→ pesanan.dlq ← perlu campur tangan manusia
Tiap topik retry dikonsumsi oleh worker yang menunda sesuai jenjangnya,
sehingga topik utama tidak pernah tertahan.
Pesan yang masuk DLQ butuh manusia yang melihatnya. Pasang peringatan pada jumlah pesan masuk DLQ, sertakan header diagnostik — pesan galat, jumlah percobaan, topik dan offset asal — dan sediakan cara memasukkan kembali pesan yang sudah diperbaiki.
Daftar Periksa Sebelum Produksi
acks=all,enable.idempotence=true, dan callback galat pada semua produserreplication.factor=3denganmin.insync.replicas=2pada semua topik penting- Kunci pesan dipilih sesuai entitas yang urutannya wajib terjaga
- Auto-commit dimatikan; offset di-commit setelah pemrosesan selesai
- Pemrosesan idempoten atau dilindungi kunci deduplikasi
- DLQ tersedia, terpantau, dan bisa diputar ulang
- Skema terdaftar dengan pemeriksaan kompatibilitas di CI
- Peringatan untuk lag, under-replicated partitions, dan pemakaian disk
auto.create.topics.enable=false, topik dikelola sebagai kode- Prosedur shutdown yang memanggil flush pada produser dan menutup konsumen dengan rapi