Publikasikan event dengan andal
Transactional outbox, change data capture, dan consumer idempoten. Cara menutup celah antara commit database dan publish ke broker.
Pertanyaan
Bagaimana memastikan perubahan di database dan event yang diterbitkan tidak pernah saling berbeda, padahal keduanya sistem terpisah?
Setelah baca ini, kamu bisa menunjukkan dual-write yang bocor, merancang transactional outbox atau CDC sebagai gantinya, dan menyebut apa yang tetap harus ditangani consumer.
Prasyarat dan contoh
Kamu sudah paham transaksi lokal (M05), ack dan at-least-once (M10.02), serta log berpartisi (M10.04).
Layanan pesanan menyimpan pesanan ke database lalu menerbitkan PesananDibuat ke broker:
1. BEGIN; INSERT INTO pesanan ...; COMMIT;
2. broker.publish("PesananDibuat", id)
Crash di antara 1 dan 2: pesanan ada, event tidak pernah terbit. Stok tidak dikurangi, struk tidak terkirim, dan tidak ada error yang tercatat. Kalau urutannya dibalik (publish dulu), crash atau rollback menghasilkan event untuk pesanan yang tidak ada. Menukar urutan hanya menukar jenis kesalahan. Database dan broker tidak bisa dikomit dalam satu transaksi biasa.
Mekanisme
Transactional outbox. Tulis event sebagai baris di tabel outbox dalam transaksi yang
sama dengan perubahan bisnis. Proses terpisah (relay) membaca outbox dan menerbitkan ke broker.
BEGIN;
INSERT INTO pesanan (...);
INSERT INTO outbox (id, topik, key, payload, dibuat_pada, terbit_pada=NULL);
COMMIT;
relay: SELECT ... WHERE terbit_pada IS NULL ORDER BY id LIMIT 100
→ publish → UPDATE outbox SET terbit_pada = now() WHERE id = ...
Atomisitas lokal menjamin: pesanan ada jika dan hanya jika barisnya di outbox ada. Relay bisa
crash setelah publish tapi sebelum menandai terbit, jadi event bisa terbit dua kali. Hasilnya
at-least-once, dan karena itu consumer harus idempoten, dengan outbox.id sebagai kunci
dedup.
Change data capture (CDC). Alih-alih query tabel, relay membaca log perubahan database (WAL atau binlog) dan mengubahnya jadi event. Keuntungan: tidak ada polling, latensi rendah, dan urutan perubahan mengikuti urutan commit. Sering dikombinasikan: outbox sebagai kontrak event, CDC sebagai cara membacanya, sehingga skema internal tabel tidak bocor ke consumer.
| Aspek | Outbox + polling | Outbox + CDC |
| ----------- | --------------------------------- | -------------------------------- |
| Latensi | Interval polling (mis. 200 ms) | Biasanya ratusan ms atau kurang |
| Beban DB | Query berkala dan update tanda | Membaca log, tanpa query tabel |
| Operasi | Satu proses relay sederhana | Konektor, slot replikasi, skema |
| Urutan | Lewat ORDER BY id (hati-hati) | Urutan log commit |
Hati-hati dengan urutan pada polling: id auto-increment bisa dikomit tidak berurutan. Transaksi
A mengambil id 10 dan B mengambil 11, B commit lebih dulu, relay membaca sampai 11, lalu A commit
dengan id 10 dan terlewat. Gunakan penanda yang aman, misalnya menunggu transaksi lama selesai
atau memakai CDC.
Consumer idempoten. Karena duplikat tak terhindarkan, consumer mencatat id event yang sudah diproses (dalam transaksi yang sama dengan efeknya) atau membuat operasinya idempoten secara alami (upsert, "set status = dibayar"). Event juga sebaiknya membawa versi agar consumer bisa menolak event yang lebih tua dari keadaan yang ada.
Isi event. Pilih antara event tipis (ID dan jenis, consumer mengambil data) dan event tebal (membawa data). Event tipis membuat consumer bergantung pada sumber dan membaca nilai yang mungkin sudah berubah. Event tebal lebih mandiri tapi skemanya harus dijaga kompatibel.
Saat rancangan ini gagal
- Dual-write "sementara". Tim menulis ke DB lalu publish langsung, dengan alasan "jarang crash". Dengan 100.000 pesanan per hari dan deploy rutin, beberapa event hilang tiap bulan dan baru ketahuan lewat keluhan.
- Outbox tidak dibersihkan. 20 juta baris terkumpul, query relay melambat. Hapus atau arsipkan baris yang sudah terbit setelah retensi (misalnya 7 hari), dan beri indeks pada kolom filter relay.
- Relay tunggal tanpa pengawasan. Relay mati semalaman, outbox menumpuk 3 juta baris, lalu semuanya dilepas sekaligus pagi hari. Pantau umur baris tertua yang belum terbit, dan pastikan consumer sanggup menyerap lonjakan (M10.03).
- Dua relay tanpa koordinasi. Keduanya membaca baris sama dan menerbitkan ganda secara
sistematis. Gunakan kunci baris (
FOR UPDATE SKIP LOCKED) atau satu pemimpin. - Skema tabel jadi kontrak. Consumer membaca langsung dari CDC tabel
pesanan. Migrasi kolom di tim pesanan merusak tiga consumer. Terbitkan event yang dirancang, bukan baris mentah. - Mengira outbox memberi exactly-once end to end. Outbox hanya menjamin event tidak hilang dari sisi producer. Duplikat dan urutan tetap urusan consumer.
Contoh tandingan: "kalau publish gagal, retry saja". Retry hanya membantu jika proses masih hidup. Kalau crash di antara commit dan publish, tidak ada proses yang tersisa untuk mengulang. Itu sebabnya niat harus tercatat di database.
Latihan
Layanan pembayaran memperbarui saldo lalu harus menerbitkan SaldoBerubah. Desain sekarang:
commit transaksi, lalu publish. Dalam sebulan, 12 dari 3 juta event hilang.
- Jelaskan kejadian yang menyebabkannya.
- Rancang perbaikan dengan outbox, dan sebut apa yang dijamin dan tidak dijamin.
- Consumer menerapkan
SaldoBerubahke ringkasan. Bagaimana mencegah penerapan ganda dan penerapan terbalik urutan?
Lihat pembahasan
- Proses crash (deploy, OOM, putus koneksi) antara
COMMITdanpublish. Saldo sudah berubah, event tidak ada, dan tidak ada proses yang mengingat bahwa event masih harus terbit. Tingkat 12 / 3.000.000 = 0,0004% kecil tapi pasti terjadi. - Dalam transaksi yang sama dengan update saldo,
INSERT INTO outboxdengan id event, key partisiakun_id, dan nomor versi saldo. Relay (polling atau CDC) menerbitkan dan menandai terbit. Dijamin: event dan perubahan saldo ada bersama atau tidak sama sekali, dan event tidak hilang walau relay crash (ia mulai lagi dari baris belum terbit). Tidak dijamin: event terbit tepat sekali (bisa duplikat), dan urutan antar akun. Urutan per akun terjaga bila key partisiakun_iddan relay menerbitkan berurutan. - Simpan
versi_terakhirper akun di ringkasan. Terapkan event hanya jikaversi == terakhir + 1. Duplikat (versi sama atau lebih kecil dari terakhir) diabaikan. Versi lebih tinggi dari yang diharapkan berarti ada celah, jadi tahan atau ambil ulang dari sumber. Update ringkasan dan penanda versi dalam satu transaksi.
Terkait
- M10.02 Tentukan perilaku pengiriman menjelaskan kenapa duplikat tak terhindarkan.
- M10.04 Pertahankan urutan yang diperlukan menjelaskan partition key untuk event yang diterbitkan.
- M10.06 Jadwalkan workflow memakai pola yang sama untuk memicu langkah berikutnya.
- Transaksi lokal dibahas di M05.
Catatan
- Asumsi: satu database yang mendukung transaksi lokal, dan broker yang menerima publish at-least-once. Detail CDC (slot replikasi, format log) bergantung produk.
- Angka 12 dari 3 juta adalah ilustrasi.
- Yang nggak dibahas: saga lintas layanan, transaksi terdistribusi dua fase, dan evolusi skema event secara rinci.
- Terakhir ditinjau: 26 Oktober 2026.