Kodeloka
  1. Kodeloka
  2. Kelas Apache Kafka
  3. Materi

2 · Produksi & Konsumsi · Modul 6 dari 13

Semantik Pengiriman & Transaksi Exactly-Once

Exactly-once di Kafka itu nyata, tetapi cakupannya jauh lebih sempit daripada yang dibayangkan kebanyakan orang.

Tiga Semantik

SemantikArtinyaCara mencapainya
At-most-onceBoleh hilang, tidak boleh dobelCommit offset sebelum memproses
At-least-onceTidak boleh hilang, boleh dobelCommit offset sesudah memproses — default yang sehat
Exactly-onceTepat sekaliTransaksi Kafka, atau at-least-once + pemrosesan idempoten

Apa yang Sebenarnya Dijamin Transaksi Kafka

Transaksi Kafka membuat rangkaian baca dari Kafka → proses → tulis ke Kafka → commit offset menjadi satu operasi atomik. Semuanya berhasil, atau semuanya dibatalkan.

produser.initTransactions()

produser.beginTransaction()
    produser.send(topik_keluaran, hasil1)
    produser.send(topik_keluaran, hasil2)
    produser.sendOffsetsToTransaction(offset, metadataGrup)  ← kuncinya di sini
produser.commitTransaction()

Offset konsumen di-commit di dalam transaksi yang sama dengan penulisan hasil. Karena itu tidak mungkin hasil tertulis tanpa offset maju, atau sebaliknya.

🚫 Exactly-once tidak berlaku ke luar Kafka

Batas jaminannya adalah Kafka itu sendiri. Bila pemrosesan Anda menulis ke PostgreSQL, memanggil API pembayaran, atau mengirim email, transaksi Kafka tidak dapat membatalkan efek samping tersebut. Aplikasi bisa crash setelah email terkirim tetapi sebelum transaksi di-commit, dan email itu akan terkirim lagi saat pesan diproses ulang.

Untuk efek samping di luar Kafka, jawabannya bukan transaksi Kafka melainkan pemrosesan idempoten: kunci deduplikasi, upsert, atau tabel pesan-terproses.

Sisi Konsumen: isolation.level

isolation.level=read_uncommitted   ← default: pesan transaksi terlihat sebelum commit
isolation.level=read_committed     ← hanya pesan dari transaksi yang sudah di-commit
⚠️ Transaksi tanpa read_committed tidak ada gunanya

Produser transaksional yang rapi menjadi sia-sia bila konsumen hilirnya memakai default read_uncommitted — konsumen itu tetap membaca pesan dari transaksi yang kemudian dibatalkan. Kedua sisi harus dikonfigurasi bersama.

Biayanya

  • Latensi bertambah — konsumen read_committed hanya boleh membaca sampai batas transaksi yang stabil, sehingga pesan tertahan sampai transaksi ditutup.
  • Throughput turun — tiap transaksi menambah penulisan marker dan koordinasi.
  • Kompleksitas naik — transactional.id harus stabil dan unik per instans produser, termasuk setelah restart dan penjadwalan ulang pod.
💡 Kapan transaksi benar-benar tepat

Transaksi Kafka bersinar pada alur Kafka ke Kafka: agregasi, penggabungan aliran, pengayaan data. Kafka Streams bahkan mengaktifkannya cukup dengan processing.guarantee=exactly_once_v2. Untuk alur yang berujung pada basis data atau API eksternal, rancang idempotensi — itu lebih sederhana dan lebih jujur.

Pola Praktis yang Sering Lebih Baik

At-least-once + kunci deduplikasi di basis data:

  INSERT INTO pesanan_terproses (id_pesan, diproses_pada)
  VALUES ('evt-abc-123', now())
  ON CONFLICT (id_pesan) DO NOTHING
  RETURNING id_pesan;

  -- tidak ada baris kembali → pesan sudah pernah diproses, lewati

Pola ini bekerja lintas sistem, mudah dipahami saat insiden, dan tidak menuntut koordinasi khusus dari Kafka.