Redis di Produksi
Resilience & Stack
Di produksi Redis akan mati suatu saat. Chapter ini memastikan saat itu terjadi, API tetap hidup dari PostgreSQL, cache tetap terpantau, memori terkendali, dan seluruh stack bisa dinyalakan dengan satu perintah.
Empat chapter pertama mengajarkan apa yang bisa dilakukan Redis dengan asumsi diam-diam bahwa ia selalu hidup. Chapter ini melepas asumsi itu dan menghadapi produksi: pertama membuat Redis boleh gagal tanpa menjatuhkan API (fallback ke sumber kebenaran), lalu memantau apakah cache benar-benar menolong dan menjaga memori tetap sehat, dan terakhir merakit stack lokal yang mencerminkan produksi dengan Docker Compose. Ini adalah lapisan yang membedakan “cache yang jalan di demo” dari “cache yang aman di production”.
Error Handling, Resilience, dan Observability
Redis boleh gagal tanpa menjatuhkan API, dan harus dipantau
Redis adalah akselerator, jadi ia boleh gagal tanpa membuat API ikut gagal. Bila Redis mati, API harus tetap melayani request dari PostgreSQL, sedikit lebih lambat. Dan agar tahu cache benar-benar menolong, ia harus dipantau.
Inti resilience adalah membedakan cache error dari business error. Cache error (Redis timeout, koneksi putus) tidak boleh menggagalkan request; ia hanya berarti “lewati cache kali ini, ambil dari database”. Business error (produk tidak ada, validasi gagal) adalah hasil sah yang memang harus dikembalikan ke client.
Di Laravel, Cache::remember tetap memanggil closure database bila cache miss, jadi kegagalan cache terasa transparan. Di Go kita harus eksplisit: setiap pemanggilan Redis dibungkus timeout pendek, dan kegagalannya di-fallback ke database, bukan dipropagasi sebagai error 500.
Pola fallback: coba cache dengan timeout pendek, dan apa pun yang bukan cache hit (miss atau error) jatuh ke database. Inilah yang membuat Redis bukan single point of failure.
internal/product/service.go// GetByIDResilient memperlakukan kegagalan Redis sebagai miss, bukan error fatal. func (s *Service) GetByIDResilient(ctx context.Context, id int64) (Product, error) { key := productKey(id) // Timeout pendek khusus cache: kalau Redis lambat, jangan tahan request. cacheCtx, cancel := context.WithTimeout(ctx, 100*time.Millisecond) cached, err := s.redis.Get(cacheCtx, key).Result() cancel() switch { case err == nil: var p Product if json.Unmarshal([]byte(cached), &p) == nil { s.metrics.RecordHit() return p, nil } case errors.Is(err, redis.Nil): s.metrics.RecordMiss() default: // Redis error (timeout, koneksi): catat, lalu lanjut ke database. s.metrics.RecordError() s.log.Warn("redis get gagal, fallback ke database", "key", key, "err", err) } // Sumber kebenaran selalu tersedia walau Redis bermasalah. p, dbErr := s.repo.FindByID(ctx, id) if dbErr != nil { return Product{}, dbErr } // Isi cache best-effort; abaikan error. if blob, mErr := json.Marshal(p); mErr == nil { setCtx, c := context.WithTimeout(ctx, 100*time.Millisecond) _ = s.redis.Set(setCtx, key, blob, productTTL).Err() c() } return p, nil }
flowchart TD
Req["GET /v1/products/123"] --> Try["coba Redis (timeout 100ms)"]
Try --> R{"hasil?"}
R -->|hit| Fast["balas cepat dari cache"]
R -->|miss| DB["ambil dari PostgreSQL"]
R -->|error / timeout| DB
DB --> Resp["balas dari database"]
DB --> Fill["isi cache best-effort"]Gambar 1. Dengan fallback, mematikan Redis hanya membuat lebih banyak request menyentuh PostgreSQL. API tetap hidup.
Cara terbaik membuktikan Redis bukan single point of failure adalah mematikannya saat development, lalu memastikan GET /v1/products/{id} tetap mengembalikan 200 dari PostgreSQL. Bila API ikut mati, berarti ada jalur yang memperlakukan cache error sebagai fatal; perbaiki itu.
Observability: tahu cache benar-benar efektif
Cache yang tidak dipantau adalah kotak ajaib. Metrik inti yang menentukan apakah cache efektif adalah hit rate (rasio hit terhadap total), latency pemanggilan Redis, pemakaian memori, eviction (key terbuang karena memori penuh), dan slowlog (command yang lambat).
| Metrik | Yang dijawab | Tindakan bila buruk |
|---|---|---|
| Hit rate | Seberapa sering cache menolong | Hit rate rendah berarti TTL terlalu pendek atau data tidak layak di-cache |
| Latency Redis call | Apakah Redis benar-benar cepat | Latency tinggi berarti masalah jaringan atau command berat |
| Memory usage | Seberapa penuh Redis | Mendekati batas berarti perlu TTL lebih ketat atau memori lebih besar |
| Eviction | Apakah key dibuang paksa | Eviction tinggi berarti memori kurang untuk beban cache |
| Slowlog | Command apa yang lambat | Hindari command yang memindai banyak key (mis. KEYS di produksi) |
Yang paling mudah dikontrol dari sisi aplikasi adalah hit rate dan latency. Catat hit, miss, dan error di service (seperti RecordHit/RecordMiss di kode di atas), lalu ukur durasi tiap pemanggilan Redis.
internal/cache/instrument.go// timed mengukur durasi satu operasi Redis untuk metrik latency. func timed(name string, log *slog.Logger, fn func() error) error { start := time.Now() err := fn() log.Info("redis op", "op", name, "ms", time.Since(start).Milliseconds(), "err", err) return err }
Jangan memakai KEYS * di produksi untuk mencari key; ia memblokir Redis selama memindai seluruh keyspace. Bila perlu iterasi, pakai SCAN dengan cursor. Slowlog akan menunjukkan command seperti ini, dan itu sinyal untuk segera mengubah pola akses.
Atur memori: maxmemory dan eviction policy
Metrik eviction di tabel di atas baru bermakna kalau Redis tahu kapan harus meng-evict. Saat Redis dipakai murni sebagai cache, praktik produksi yang penting adalah memberi batas memori (maxmemory) dan kebijakan eviction yang membuang key paling tidak berguna ketika batas tercapai, alih-alih membiarkan Redis kehabisan memori dan ditolak oleh OS. Untuk cache, kebijakan yang umum adalah allkeys-lru (buang key yang paling lama tidak diakses) atau allkeys-lfu (buang yang paling jarang diakses).
redis.conf (potongan untuk peran cache)maxmemory 512mb maxmemory-policy allkeys-lru
Dengan allkeys-lru, Redis akan dengan tenang membuang cache lama saat penuh, dan itu aman justru karena semua isinya bisa dibangun ulang dari PostgreSQL. Ini cocok untuk peran cache. Untuk data yang TIDAK boleh hilang diam-diam (mis. dipakai sebagai antrian), pertimbangkan kebijakan noeviction plus persistence, dan sadari bahwa itu bukan lagi peran “cache murni”.
Resilience dan observability memastikan Redis berperilaku baik di produksi. Tetapi sebelum produksi ada development, dan di sanalah kita harus bisa menyalakan seluruh stack dengan mudah.
Stack Lokal dan Peta Penggunaan Redis
Redis di Docker Compose plus peta di mana ia dipasang
Bagian ini menyatukan dua hal praktis: menambahkan Redis ke stack lokal lewat Docker Compose, dan memetakan di mana saja Redis dipakai (dan tidak dipakai) di online shop skincare.
Redis mudah dijalankan lokal bersama Go API dan PostgreSQL. Tambahkan satu service ringkas ke docker-compose.yml, dengan env REDIS_ADDR yang dibaca aplikasi.
docker-compose.ymlservices: api: build: . ports: - "8080:8080" environment: DATABASE_URL: postgres://app:secret@postgres:5432/skincare?sslmode=disable REDIS_ADDR: redis:6379 depends_on: - postgres - redis postgres: image: postgres:17 environment: POSTGRES_USER: app POSTGRES_PASSWORD: secret POSTGRES_DB: skincare ports: - "5432:5432" redis: image: redis:7 ports: - "6379:6379"
Laravel Sail memberi docker-compose.yml siap pakai dengan app, database, dan Redis. Di sini kita merakit yang setara untuk backend Go: satu service API, satu PostgreSQL sebagai sumber kebenaran, satu Redis sebagai akselerator. Aplikasi membaca alamat Redis dari REDIS_ADDR.
cmd/api/main.go// Baca alamat Redis dari environment, dengan default lokal yang masuk akal. addr := os.Getenv("REDIS_ADDR") if addr == "" { addr = "localhost:6379" } rdb, err := cache.New(ctx, addr) if err != nil { // Redis opsional: log peringatan, tetap jalan tanpa cache bila perlu. log.Printf("redis tidak tersedia, jalan tanpa cache: %v", err) }
- cmd/
- api/
- main.go baca REDIS_ADDR, buat client cache
- internal/
- cache/
- redis.go client + helper Get/Set
- instrument.go metrik latency cache
- product/
- service.go cache-aside detail produk
- ratelimit/
- fixedwindow.go INCR + EXPIRE per-IP / per-user
- auth/
- session.go session store + token blacklist
- docker-compose.yml service redis, postgres, api
Cara kerja docker-compose.yml, jaringan antar service, dan healthcheck dibahas tuntas di Course Docker. Di sini cukup pahami intinya: nama service (redis, postgres) menjadi hostname antar container, jadi REDIS_ADDR=redis:6379 menunjuk ke service Redis di jaringan Compose yang sama.
Peta penggunaan Redis di online shop
Berikut peta selektif: bagian mana memakai Redis, dan bagian mana tetap murni di PostgreSQL. Ini ringkasan keputusan dari seluruh course.
| Bagian | Pakai Redis? | Caranya |
|---|---|---|
| Detail produk dan kategori | Ya | Cache-aside dengan TTL pendek, delete-on-write saat admin update |
| Session login | Ya | Session store session:{id} dengan TTL alami |
| Login dan checkout throttle | Ya | Rate limit fixed window dengan INCR + EXPIRE |
| Top viewed products | Ya | Sorted Set untuk ranking |
| Stok inventory | Tidak | Selalu dari PostgreSQL dalam transaksi |
| Status order dan payment | Tidak | Selalu dari PostgreSQL, harus segar |
| Cart aktif | Tidak (sebagai kebenaran) | State hidup milik user, simpan di PostgreSQL |
flowchart LR
subgraph Redis["Redis (akselerator + state sementara)"]
PC["product / category cache"]
SS["user session"]
RL["login / checkout throttle"]
TV["top viewed (sorted set)"]
end
subgraph PG["PostgreSQL (sumber kebenaran)"]
INV["inventory / stok"]
ORD["order + payment status"]
CART["cart aktif"]
end
API["Go API"] --> Redis
API --> PGGambar 2. Peta penggunaan. Redis memegang yang cepat dan sementara; PostgreSQL memegang yang harus benar dan permanen.
Dengan docker compose up, kamu mendapat API plus PostgreSQL plus Redis sekaligus, mencerminkan produksi. Ini mempermudah menguji cache-aside, rate limit, dan fallback (matikan service redis, lalu lihat API tetap hidup) tanpa memasang apa pun secara manual.
Peta ini adalah seluruh course dalam satu tabel: tiap “Ya” adalah teknik yang sudah kita pelajari, tiap “Tidak” adalah garis disiplin yang kita jaga. Chapter terakhir merangkum semuanya dan menunjuk ke topik lanjutan yang sengaja kita tunda.
Ringkasan
Redis yang aman di produksi
Chapter ini menyiapkan Redis untuk produksi: gagal dengan anggun, terpantau, terkendali memorinya, dan mudah dinyalakan di stack lokal.
Kita bangun fallback yang memperlakukan kegagalan Redis sebagai cache miss, dengan timeout pendek agar Redis yang lambat tidak menahan request, sehingga API tetap hidup dari PostgreSQL. Kita pantau hit rate, latency, memori, eviction, dan slowlog, lalu mengatur maxmemory plus allkeys-lru agar Redis membuang cache lama dengan tenang saat penuh. Terakhir kita rakit stack Go plus PostgreSQL plus Redis dengan Docker Compose dan memetakan di mana Redis dipakai (cache, session, rate limit, ranking) dan di mana tidak (stok, order, payment, cart).
Yang Wajib Menempel
- Bedakan cache error (timeout, koneksi) dari business error; cache error berarti fallback ke database, bukan 500.
- Bungkus pemanggilan Redis dengan timeout pendek agar Redis yang lambat tidak menahan request.
- Uji ketahanan dengan mematikan Redis: API harus tetap mengembalikan 200 dari PostgreSQL.
- Pantau hit rate, latency, memory usage, eviction, dan slowlog; hindari
KEYSdi produksi, pakaiSCAN. - Untuk peran cache, set
maxmemoryplus kebijakan eviction (allkeys-lru/allkeys-lfu) agar memori terkendali. - Rakit stack lokal dengan Docker Compose; nama service jadi hostname,
REDIS_ADDR=redis:6379.
Redis kini siap produksi: cepat saat sehat, aman saat gagal. Di Chapter 6 kita menutup course dengan jujur menyebut topik yang sengaja ditunda (Pub/Sub, distributed lock, cache stampede, Streams) lengkap dengan peringatannya, lalu merangkum seluruh perjalanan ke dalam empat flow nyata online shop.