Redis Pub/Sub di Go dengan go-redis: Panduan Praktis
Programming Tutorial Go #golang #redis #pub-sub #go-redis

Redis Pub/Sub di Go dengan go-redis: Panduan Praktis

A
Abd. Asis
4 min read
Bagikan:

Bayangkan punya layanan monitoring yang perlu memberi tahu beberapa komponen berbeda setiap kali server mengalami anomali — lonjakan CPU, memori hampir penuh, atau disk mau habis. Pendekatan naif adalah memanggil setiap komponen satu per satu dari titik yang mendeteksi anomali tersebut. Masalahnya: coupling antar komponen jadi ketat, dan setiap penambahan penerima baru berarti mengubah kode pengirim.

Redis Pub/Sub memutus coupling itu. Komponen yang mendeteksi anomali cukup mempublikasikan pesan ke sebuah channel — tanpa tahu siapa yang akan menerima. Komponen lain yang butuh informasi tersebut tinggal subscribe ke channel yang sama. Keduanya tidak saling mengenal, dan bisa berkembang secara independen.

Di artikel ini, kita akan membangun sistem notifikasi monitoring menggunakan go-redis v9 — library Redis resmi untuk Go yang dikelola aktif dan mendukung Redis 7+.

Cara Kerja Redis Pub/Sub

Pub/Sub (Publish/Subscribe) adalah pola messaging di mana pengirim dan penerima tidak terhubung secara langsung. Redis berperan sebagai perantara: menerima pesan dari publisher dan meneruskannya ke semua subscriber yang sedang aktif di channel yang sama.

Tiga aktor dalam pola ini:

  • Publisher — Mengirim pesan ke channel tertentu, tanpa peduli siapa yang mendengarkan.
  • Channel — Nama virtual tempat pesan mengalir. Tidak perlu didaftarkan dulu; cukup publish ke nama channel dan subscriber yang sudah ada akan menerimanya.
  • Subscriber — Mendaftarkan diri ke satu atau beberapa channel dan memproses setiap pesan yang masuk.

Satu hal penting yang perlu dipahami sebelum mulai:

Redis Pub/Sub bersifat fire-and-forget. Pesan yang dikirim saat tidak ada subscriber aktif akan hilang selamanya — Redis tidak menyimpannya. Jika butuh pesan yang persisten dan bisa di-replay, pertimbangkan Redis Streams.

Untuk kasus notifikasi real-time yang ephemeral — seperti alert monitoring yang relevannya hanya saat terjadi — Pub/Sub adalah pilihan yang tepat.

Setup Project dan Koneksi Redis

Buat direktori project baru dan inisialisasi Go module:

mkdir server-monitor && cd server-monitor
go mod init github.com/yourname/server-monitor

Pasang library go-redis v9:

go get github.com/redis/go-redis/v9

Buat file redis.go untuk menginisialisasi koneksi. Menggunakan singleton pattern agar koneksi tidak dibuat berulang kali:

// redis.go
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/redis/go-redis/v9"
)

var rdb *redis.Client

func initRedis() {
	rdb = redis.NewClient(&redis.Options{
		Addr:     "localhost:6379",
		Password: "",
		DB:       0,
	})

	ctx := context.Background()
	if err := rdb.Ping(ctx).Err(); err != nil {
		log.Fatalf("gagal konek ke Redis: %v", err)
	}

	fmt.Println("koneksi Redis berhasil")
}

redis.NewClient membuat client dengan konfigurasi koneksi yang diberikan. Ping memastikan server Redis memang bisa dijangkau — gagal di sini berarti ada masalah di konfigurasi atau server Redis belum berjalan.

Menjalankan Redis dengan Docker

Jika belum punya Redis lokal, cara paling cepat adalah lewat Docker:

docker run -d --name redis-monitor -p 6379:6379 redis:7-alpine

Redis 7 Alpine dipilih karena ukurannya kecil dan sudah mendukung semua fitur yang dibutuhkan.

Membuat Publisher untuk Alert Monitoring

Publisher adalah komponen yang mendeteksi kondisi server dan mempublikasikan alert ke channel Redis. Buat file publisher.go:

// publisher.go
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"log"
	"time"
)

type ServerAlert struct {
	ServerID  string    `json:"server_id"`
	AlertType string    `json:"alert_type"`
	Value     float64   `json:"value"`
	Threshold float64   `json:"threshold"`
	Timestamp time.Time `json:"timestamp"`
}

const alertChannel = "server:alerts"

func publishAlert(ctx context.Context, alert ServerAlert) error {
	payload, err := json.Marshal(alert)
	if err != nil {
		return fmt.Errorf("gagal marshal alert: %w", err)
	}

	result := rdb.Publish(ctx, alertChannel, payload)
	if result.Err() != nil {
		return fmt.Errorf("gagal publish ke channel %s: %w", alertChannel, result.Err())
	}

	receiverCount := result.Val()
	log.Printf("alert %s dikirim ke %d subscriber", alert.AlertType, receiverCount)
	return nil
}

Beberapa hal menarik di sini. ServerAlert di-serialize ke JSON sebelum dikirim — Redis hanya mengerti string, jadi semua struktur data perlu dikonversi dulu. Nilai kembalian dari Publish menyimpan jumlah subscriber yang menerima pesan, berguna untuk debugging ketika tidak ada yang merespons alert.

Membuat Subscriber untuk Memproses Alert

Subscriber membuka koneksi ke Redis, mendaftarkan diri ke channel, lalu memproses pesan yang masuk secara berkelanjutan. Buat file subscriber.go:

// subscriber.go
package main

import (
	"context"
	"encoding/json"
	"log"
)

func startAlertSubscriber(ctx context.Context, handlerName string, handler func(ServerAlert)) {
	pubsub := rdb.Subscribe(ctx, alertChannel)
	defer pubsub.Close()

	log.Printf("[%s] mulai mendengarkan channel: %s", handlerName, alertChannel)

	msgCh := pubsub.Channel()
	for {
		select {
		case msg, ok := <-msgCh:
			if !ok {
				log.Printf("[%s] channel ditutup", handlerName)
				return
			}

			var alert ServerAlert
			if err := json.Unmarshal([]byte(msg.Payload), &alert); err != nil {
				log.Printf("[%s] gagal parse pesan: %v", handlerName, err)
				continue
			}

			handler(alert)

		case <-ctx.Done():
			log.Printf("[%s] context dibatalkan, subscriber berhenti", handlerName)
			return
		}
	}
}

pubsub.Channel() mengembalikan Go channel yang akan menerima setiap pesan baru. Pola select dengan ctx.Done() memungkinkan subscriber berhenti dengan bersih saat context dibatalkan — penting untuk graceful shutdown. Jika channel ditutup dari sisi Redis atau koneksi putus, ok akan bernilai false dan fungsi bisa berhenti dengan tepat.

go-redis secara otomatis menangani reconnect saat koneksi terputus. Subscriber tidak perlu menulis logic retry sendiri — library yang mengurus ini di balik layar.

Menyatukan Publisher dan Subscriber di main.go

Buat main.go yang menjalankan beberapa subscriber secara paralel, lalu mensimulasikan publisher yang mengirim alert:

// main.go
package main

import (
	"context"
	"log"
	"os"
	"os/signal"
	"syscall"
	"time"
)

func main() {
	initRedis()

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	// subscriber pertama: mencatat semua alert ke log
	go startAlertSubscriber(ctx, "logger", func(alert ServerAlert) {
		log.Printf("[ALERT LOG] server=%s type=%s value=%.1f threshold=%.1f",
			alert.ServerID, alert.AlertType, alert.Value, alert.Threshold)
	})

	// subscriber kedua: simulasi kirim email untuk alert kritis
	go startAlertSubscriber(ctx, "email-notifier", func(alert ServerAlert) {
		if alert.Value >= alert.Threshold*1.2 {
			log.Printf("[EMAIL] kirim notifikasi ke admin: %s di %s melampaui 120%% threshold",
				alert.AlertType, alert.ServerID)
		}
	})

	// beri waktu subscriber untuk terhubung
	time.Sleep(500 * time.Millisecond)

	// simulasi publisher mendeteksi anomali
	alerts := []ServerAlert{
		{
			ServerID:  "web-01",
			AlertType: "cpu_usage",
			Value:     87.5,
			Threshold: 80.0,
			Timestamp: time.Now(),
		},
		{
			ServerID:  "db-01",
			AlertType: "memory_usage",
			Value:     97.3,
			Threshold: 85.0,
			Timestamp: time.Now(),
		},
		{
			ServerID:  "cache-01",
			AlertType: "disk_usage",
			Value:     72.1,
			Threshold: 90.0,
			Timestamp: time.Now(),
		},
	}

	for _, alert := range alerts {
		if err := publishAlert(ctx, alert); err != nil {
			log.Printf("error publish: %v", err)
		}
		time.Sleep(300 * time.Millisecond)
	}

	// tunggu sinyal shutdown (Ctrl+C)
	quit := make(chan os.Signal, 1)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
	<-quit

	log.Println("menerima sinyal shutdown, menutup subscriber...")
	cancel()
	time.Sleep(200 * time.Millisecond)
}

Dua subscriber berjalan sebagai goroutine terpisah — keduanya menerima pesan yang sama dari channel yang sama, tapi memrosesnya dengan cara berbeda. Publisher tidak tahu tentang keduanya. Saat Ctrl+C ditekan, cancel() dipanggil dan semua subscriber berhenti lewat mekanisme ctx.Done().

Menjalankan dan Melihat Output

Jalankan program:

go run .

Output yang diharapkan:

koneksi Redis berhasil
[logger] mulai mendengarkan channel: server:alerts
[email-notifier] mulai mendengarkan channel: server:alerts
alert cpu_usage dikirim ke 2 subscriber
[ALERT LOG] server=web-01 type=cpu_usage value=87.5 threshold=80.0
[logger] mulai mendengarkan channel: server:alerts  (duplikat karena dua subscriber)
alert memory_usage dikirim ke 2 subscriber
[ALERT LOG] server=db-01 type=memory_usage value=97.3 threshold=85.0
[EMAIL] kirim notifikasi ke admin: memory_usage di db-01 melampaui 120% threshold
alert disk_usage dikirim ke 2 subscriber
[ALERT LOG] server=cache-01 type=disk_usage value=72.1 threshold=90.0

Angka 2 subscriber di baris publisher mengonfirmasi bahwa kedua subscriber menerima pesan. Alert memory_usage di db-01 dengan nilai 97.3 dari threshold 85.0 memenuhi kondisi 120%, sehingga email-notifier bereaksi.

Subscribe ke Beberapa Channel Sekaligus

go-redis mendukung subscribe ke beberapa channel dalam satu panggilan, maupun menggunakan pattern matching dengan PSubscribe:

// subscribe ke beberapa channel eksplisit
pubsub := rdb.Subscribe(ctx, "server:alerts", "server:metrics", "server:heartbeat")

// subscribe menggunakan glob pattern
pubsub := rdb.PSubscribe(ctx, "server:*")

PSubscribe dengan pattern server:* akan otomatis menangkap semua channel yang namanya diawali server:. Ini berguna saat jumlah channel dinamis atau belum diketahui sebelumnya. Pesan dari PSubscribe berisi field tambahan Pattern dan Channel sehingga subscriber bisa membedakan dari channel mana pesan berasal.

Hal yang Perlu Diperhatikan di Produksi

Ada beberapa perilaku yang tidak langsung terlihat dari kode, tapi penting saat aplikasi sudah di production.

Pertama, satu koneksi per subscriber. Setiap pemanggilan rdb.Subscribe() membuka koneksi baru ke Redis. Jika punya banyak subscriber, perhatikan batas koneksi di konfigurasi Redis (maxclients). Untuk skenario dengan ratusan subscriber, pertimbangkan arsitektur fan-out yang lebih efisien.

Kedua, pesan yang terlewat saat restart. Jika subscriber mati dan kembali hidup, semua pesan yang dikirim selama downtime tersebut tidak akan pernah diterima. Untuk kasus di mana setiap pesan harus diproses, Redis Streams dengan consumer group adalah solusi yang lebih tepat.

Ketiga, backpressure pada Channel(). Secara default pubsub.Channel() memiliki buffer 100 pesan. Jika subscriber lambat memproses dan pesan datang lebih cepat dari kemampuan prosesnya, pesan baru akan dibuang. Atur buffer dengan pubsub.Channel(redis.WithChannelSize(1000)) jika butuh kapasitas lebih besar.

Kesimpulan

Redis Pub/Sub memberi cara yang elegan untuk membangun komunikasi antar komponen tanpa coupling langsung. go-redis v9 membungkus kompleksitas koneksi dan reconnect di balik API yang bersih — cukup Subscribe, iterasi channel, proses pesan. Untuk sistem notifikasi, live update, atau event broadcasting yang tidak butuh persistensi, kombinasi Go dan Redis Pub/Sub adalah pilihan yang solid dan efisien.

Referensi

  1. 1Golang Redis PubSub — Dokumentasi Resmi go-redis (uptrace.dev)
  2. 2redis/go-redis — Repository GitHub Resmi
  3. 3Redis Streams — Dokumentasi Resmi Redis
  4. 4Implementing Pub/Sub in Go with Redis for Real-Time Apps — Relia Software

Tentang Penulis

Abd. Asis

Abd. Asis

Software Developer dan Laravel Programmer dari Madura, Indonesia. Passionate tentang PHP, Laravel, dan teknologi web modern.

Komentar

Artikel Terkait

Artikel lain yang mungkin menarik untuk kamu