Lompat ke konten
19 menit bacamateri 75 dari 96

Kirim pesan chat

ID pesan dari klien, urutan per percakapan dari server, ack setelah tulis tahan lama, dan kursor baca. Itu yang bikin chat nggak kehilangan atau menggandakan pesan.

Ditinjau 26 Oktober 2026

Pertanyaan

Bagaimana memastikan pesan chat sampai sekali, berurutan, dan tetap ada walau penerimanya offline?

Setelah baca ini, kamu bisa merancang ID pesan, cakupan urutan, alur acknowledgment, pengiriman saat offline, dan status belum dibaca tanpa menyimpan satu baris per pesan per penerima.

Prasyarat dan contoh

Kamu sudah paham idempotensi dan retry (M03), transaksi dasar (M05), dan partisi (M07). Koneksi yang bisa putus dibahas di M13.02.

Budi mengirim "oke, transfer sekarang" lewat jaringan seluler yang lemah. Aplikasi menunggu ack, nggak datang dalam 5 detik, lalu mengirim ulang. Server sebenarnya sudah menyimpan pesan pertama, cuma ack-nya hilang. Tanpa desain yang benar, Sinta melihat pesan itu dua kali. Dengan desain yang salah di arah sebaliknya (nggak ada retry), pesan hilang diam-diam.

Mekanisme

Dua ID yang beda tugasnya.

  • client_msg_id: UUID yang dibuat klien sebelum kirim. Tugasnya idempotensi: kirim ulang dengan ID yang sama nggak membuat pesan baru. Pasang unique constraint pada (conversation_id, client_msg_id).
  • seq: bilangan bulat yang diberikan server, naik satu per percakapan (1, 2, 3, ...). Tugasnya urutan dan deteksi celah.

Cakupan urutan per percakapan, bukan global. Urutan global menuntut satu penulis buat seluruh sistem, jadi bottleneck. Per percakapan, yang butuh urutan cuma peserta percakapan itu. Tetapkan seq dengan satu penulis per percakapan, misalnya partisi berdasarkan conversation_id (M07), atau di basis data dengan UPDATE conversation SET last_seq = last_seq + 1 ... RETURNING dalam transaksi yang sama dengan insert pesan. Konsekuensinya: urutan antar percakapan berbeda nggak didefinisikan, dan itu memang nggak perlu.

Alur kirim.

  1. Klien menyimpan pesan lokal dengan status "mengirim", lalu kirim client_msg_id + isi.
  2. Server menulis pesan ke penyimpanan tahan lama (dengan seq), baru membalas ack berisi client_msg_id dan seq.
  3. Klien menandai "terkirim". Kalau ack nggak datang, klien kirim ulang dengan client_msg_id yang sama. Server melihat duplikat, nggak menulis lagi, dan membalas ack dengan seq lama.

Pengiriman ke klien itu at-least-once, deduplikasi pakai ID bikin efeknya sekali. Ack dikirim setelah tulis tahan lama, bukan sebelum, supaya "terkirim" artinya benar-benar tersimpan.

Offline dan celah. Server nggak perlu tahu apakah penerima online. Pesan sudah tersimpan dengan seq. Saat klien nyambung, ia kirim last_seq per percakapan, dan server membalas semua pesan dengan seq lebih besar. Kalau klien menerima seq 41 lalu 43, ia tahu 42 hilang dan meminta ulang. Jalur real-time hanya mempercepat, jalur sinkronisasi menjamin.

Status belum dibaca. Jangan simpan flag per pesan per penerima. Di grup 500 anggota, itu 500 baris untuk satu pesan. Simpan satu kursor per anggota: last_read_seq. Jumlah belum dibaca = last_seq percakapan dikurangi last_read_seq anggota. Satu update kecil saat membaca, dan satu angka murah buat badge.

Hitung. Percakapan aktif 30 pesan per menit, dengan 1 juta percakapan aktif itu sekitar 500.000 pesan per detik di puncak ekstrem. Dengan partisi per percakapan, beban tersebar, dan tiap partisi hanya melayani penulisan berurutan buat percakapan miliknya.

Saat rancangan ini gagal

  • Ack sebelum tulis. Server membalas ack lalu crash sebelum menyimpan. Klien yakin sudah terkirim, pesan hilang. Ack harus mengikuti durabilitas.
  • Urutan pakai waktu klien atau server. Jam berbeda, dua pesan di milidetik yang sama, urutan bolak-balik di dua perangkat. seq dari satu penulis menghindarinya.
  • Tanpa deduplikasi. Retry setelah ack hilang membuat pesan ganda.
  • Celah nggak terdeteksi. Real-time dikirim tanpa seq, pesan hilang di tengah, dan klien nggak punya cara tahu.
  • Percakapan panas. Grup dengan 100.000 anggota dan ribuan pesan per detik membuat satu penulis jadi bottleneck, dan fan-out ke 100.000 koneksi butuh strategi sendiri (mirip M13.05).

Contoh tandingan: chat internal dengan 30 pengguna nggak perlu sistem partisi, cukup satu tabel dan satu counter per percakapan. Mekanisme ID dan ack tetap dipakai karena murah.

Latihan

Klien mengirim pesan dengan client_msg_id = a1, server menyimpannya sebagai seq = 52, lalu ack hilang. Klien mengirim ulang.

  1. Apa yang dilakukan server pada kiriman kedua, dan apa yang dibalas?
  2. Sinta offline, menerima sampai seq = 50. Saat nyambung, apa yang terjadi?
  3. Grup 200 anggota, last_seq = 300. Tiga anggota punya last_read_seq 300, 295, dan 120. Hitung belum dibaca tiap anggota dan jumlah baris yang ditulis saat satu pesan masuk.
Lihat pembahasan
  1. Unique constraint (conversation_id, client_msg_id) menolak insert kedua. Server mencari pesan yang ada dan membalas ack dengan seq = 52 yang sama. Nggak ada pesan baru.
  2. Klien kirim last_seq = 50. Server membalas pesan 51, 52, dan seterusnya sampai last_seq terkini. Kalau real-time sempat mengirim 53 lebih dulu, klien melihat celah (50 lalu 53) dan menunggu atau meminta 51 sampai 52.
  3. Belum dibaca: 0, 5, dan 180. Satu pesan masuk menulis satu baris pesan dan menaikkan satu counter last_seq. Nggak ada baris per anggota. Update kursor baru terjadi saat anggota membaca.

Terkait

  • Sebelumnya: M13.02 Kelola koneksi.
  • Pola fan-out ke banyak penerima diperluas di M13.05 Buat feed.
  • Idempotensi dari M03 dan transaksi dari M05 dipakai langsung di sini. Penerimaan tahan lama untuk event nanti dipakai pada rancangan D14 dan proyek B10 yang belum tersedia.

Catatan

  • Asumsi: satu penulis logis per percakapan dan penyimpanan tahan lama dengan transaksi.
  • Yang nggak dibahas: enkripsi ujung ke ujung, sinkronisasi multi-perangkat penuh, edit dan hapus pesan, serta lampiran media.
  • Pendekatan ID plus kursor adalah pola umum, bukan klaim tentang aplikasi tertentu.
  • Terakhir ditinjau: 26 Oktober 2026.

Isi · System Design