Queue vs Topic di JMS: Point-to-Point atau Publish-Subscribe

Pilih queue bila setiap pesan cukup diproses satu kali oleh salah satu pekerja, dan pilih topic bila setiap pesan harus diterima oleh semua pihak yang berlangganan. Queue adalah model point-to-point: pesan dibagi di antara consumer. Topic adalah model publish-subscribe: pesan disalin ke setiap subscriber.

Keputusan ini terlihat sederhana, tetapi dua fitur topic sering membingungkan: durable subscription dan shared subscription. Keduanya menentukan apakah pesan bisa hilang saat subscriber mati dan apakah beban satu subscription bisa dibagi ke beberapa instance aplikasi.

Queue: membagi pekerjaan

Dalam queue, broker menyimpan pesan sampai ada consumer yang mengambil dan mengakuinya (acknowledge). Jika ada tiga instance layanan yang mendengarkan queue pesanan.baru, broker membagi pesan di antara ketiganya. Pesan pertama mungkin ke instance A, pesan kedua ke instance B, dan seterusnya. Dalam kondisi normal, tidak ada pesan yang diproses dua instance sekaligus.

Sifat penting queue:

  • Pesan tetap tersimpan walau belum ada consumer sama sekali.
  • Menambah consumer berarti menambah daya proses (scaling horizontal).
  • Broker menjaga urutan pesan di dalam queue, tetapi begitu beberapa consumer bekerja paralel, urutan pemrosesan tidak lagi terjamin.

Contoh tugas yang cocok: memproses pembayaran, membuat label pengiriman, mengirim email transaksi, atau mengompres gambar.

Topic: menyiarkan kejadian

Dalam topic, pengirim menerbitkan pesan tanpa tahu siapa saja yang mendengarkan. Setiap subscriber mendapat salinannya sendiri. Misalnya event produk.harga-berubah diterbitkan sekali, lalu layanan pencarian memperbarui indeks, layanan cache menghapus data lama, dan layanan notifikasi memberi tahu pengguna yang memasang alarm harga.

Yang sering dilupakan: subscriber biasa (non-durable) hanya menerima pesan selama ia terhubung. Bila layanan pencarian sedang deploy ulang selama dua menit, pesan yang terbit di dua menit itu tidak akan pernah sampai kepadanya.

Durable subscription

Durable subscription meminta broker menyimpan pesan untuk subscriber tertentu selama ia offline. Subscription ini punya nama, dan broker terus mengumpulkan pesan atas nama tersebut sampai subscription dihapus secara eksplisit. Di JMS 2.0 ke atas, Anda membuatnya dengan createDurableConsumer:

try (JMSContext context = cf.createContext()) {
    context.setClientID("layanan-pencarian");
    Topic topic = context.createTopic("produk.harga-berubah");
    JMSConsumer consumer = context.createDurableConsumer(topic, "indeks-harga");
    String body = consumer.receiveBody(String.class, 5000);
    // proses body; null berarti tidak ada pesan dalam 5 detik
}

Durable subscription yang tidak di-share terikat pada kombinasi client ID dan nama subscription, dan hanya boleh punya satu consumer aktif pada satu waktu. Itulah sebabnya client ID wajib di-set. Masalah muncul ketika Anda menjalankan dua instance layanan pencarian dengan client ID yang sama: instance kedua akan ditolak broker.

Hal lain yang perlu diperhatikan: durable subscription yang ditinggalkan akan terus menimbun pesan. Jika suatu layanan dimatikan permanen, hapus subscription-nya dengan context.unsubscribe("indeks-harga") (setelah consumer-nya ditutup) agar penyimpanan broker tidak penuh.

Shared subscription

JMS 2.0 menambahkan shared subscription untuk menyelesaikan masalah scaling tadi. Beberapa consumer boleh memakai nama subscription yang sama, dan pesan untuk subscription itu dibagi di antara mereka, persis seperti queue. Ada dua varian:

  • createSharedConsumer(topic, nama): shared non-durable. Pesan dibagi di antara instance yang aktif, tetapi pesan yang terbit saat semua instance mati tidak disimpan.
  • createSharedDurableConsumer(topic, nama): shared dan durable. Pesan dibagi di antara instance dan tetap disimpan bila semua instance offline.
// Dijalankan di setiap instance layanan pencarian
JMSConsumer consumer =
        context.createSharedDurableConsumer(topic, "indeks-harga");
consumer.setMessageListener(message -> {
    // perbarui indeks pencarian
});

Untuk shared subscription, client ID bersifat opsional. Jika tidak di-set, subscription tidak terikat pada client ID tertentu. Dengan pola ini, satu topic bisa melayani beberapa kelompok subscriber, dan masing-masing kelompok bisa diskalakan secara terpisah. Catatan kecil: setMessageListener tidak boleh dipakai di dalam container web atau EJB Jakarta EE; di sana gunakan message-driven bean, atau anotasi listener dari framework seperti Spring.

Menyaring pesan dengan selector

Baik queue maupun topic mendukung message selector, yaitu filter berbasis header dan property, misalnya wilayah = 'jawa' AND prioritas > 5. Pada topic, selector membuat subscription hanya menerima pesan yang relevan. Pada queue, selector perlu dipakai hati-hati: pesan yang tidak cocok dengan selector consumer mana pun tetap tertahan di antrean dan bisa menumpuk tanpa disadari. Jika Anda butuh banyak filter berbeda, sering kali lebih rapi membuat destination terpisah daripada mengandalkan selector yang rumit.

Tabel perbandingan

Kebutuhan Pilihan Catatan
Satu pesan diproses satu pekerja Queue Tambah consumer untuk scaling
Banyak layanan perlu tahu kejadian yang sama Topic Pengirim tidak perlu tahu daftar penerima
Subscriber boleh ketinggalan pesan Topic, non-durable Misalnya tampilan dashboard real time
Subscriber tidak boleh ketinggalan pesan Topic, durable Hapus subscription yang tidak dipakai
Subscriber penting yang dijalankan beberapa instance Topic, shared durable Tersedia sejak JMS 2.0

Studi kasus singkat: event pesanan dibuat

Misalkan toko online menerbitkan event “pesanan dibuat”. Tiga hal harus terjadi: stok dikurangi, email konfirmasi dikirim, dan data masuk ke gudang data untuk laporan. Jika memakai satu queue, hanya satu dari tiga layanan yang mendapat pesan, jadi itu salah. Jika memakai topic dengan subscriber non-durable, layanan email yang sedang restart akan melewatkan sebagian pesanan. Pilihan yang tepat adalah topic dengan tiga shared durable subscription: stok, email, dan laporan. Masing-masing layanan bisa menjalankan berapa pun instance tanpa saling mengganggu.

Sebagian tim memilih pola lain: satu topic yang diteruskan ke beberapa queue di sisi broker. ActiveMQ Artemis, misalnya, memodelkan semuanya sebagai address dengan satu atau beberapa queue di belakangnya, sementara RabbitMQ memakai exchange yang di-bind ke beberapa queue. Hasilnya mirip, hanya konfigurasinya berbeda. Perbedaan model antarbroker ini saya bandingkan di ActiveMQ Artemis, RabbitMQ, dan Kafka untuk pemula.

Kesalahan yang sering terjadi

  1. Memakai queue untuk siaran. Gejalanya, sebagian layanan “kadang-kadang” tidak menerima event, karena pesan dibagi, bukan disalin.
  2. Client ID yang sama di beberapa instance. Instance kedua gagal terhubung. Pakai shared subscription atau client ID unik per instance.
  3. Durable subscription yatim. Layanan lama sudah dimatikan, tetapi subscription-nya masih menimbun pesan berbulan-bulan.
  4. Mengandalkan urutan dengan banyak consumer. Jika urutan penting, misalnya perubahan status pesanan, kelompokkan pesan per kunci (fitur message grouping di beberapa broker) atau tangani urutan di logika aplikasi dengan nomor versi.

Apa pun pilihannya, queue maupun topic durable sama-sama memberi jaminan at-least-once. Satu pesan bisa sampai dua kali ketika consumer mati sebelum sempat mengakui pesan, jadi rancang pemrosesan Anda agar aman terhadap duplikat sejak awal, bukan setelah insiden pertama terjadi.

Hendra Kurniawan

Oleh Hendra Kurniawan

Pengembang backend Java yang lama menangani integrasi sistem dan message broker. Menulis catatan praktis tentang JMS, API, dan arsitektur aplikasi.