Toko kini membaca data sungguhan dari PostgreSQL, tetapi setiap kunjungan pelanggan menempuh jalur yang sama mahalnya: proxy, backend, koneksi database, kueri, jawaban. Siti yang mengamati halaman depan selama satu jam sore menemukan pola yang mengganggu — daftar produk yang sama persis diminta ratusan kali, dan setiap permintaan menagih satu kueri penuh ke database untuk data yang tidak berubah sejak pagi. Chapter ini memasang service kelima toko — cache Redis di backend-net — beserta pola bacaan yang menjalaninya, cache-aside, lengkap dengan disiplin invalidasi dan ukuran rasio hit. Kemudian kita meminjam benda yang sama untuk peran kedua yang tampak serupa padahal berlawanan: antrian, yang tidak mempercepat pembacaan melainkan memutus pekerjaan lambat dari jalur permintaan — dan menutup dengan kandidat naik kelasnya, RabbitMQ.
Kueri yang Sama, Berulang Kali #
Angka-angka berikut ilustratif untuk membandingkan besaran, bukan hasil pengukuran mesin kita, tetapi rasio di antaranya bersifat umum:
| Operasi | Tempat kerja | Latensi kira-kira (ilustratif) |
|---|---|---|
GET produk:1 di Redis | memori container cache | di bawah 1 ms |
SELECT ... WHERE id = 1 di PostgreSQL | koneksi, parser, disk, perakit baris | belasan hingga puluhan ms |
Selisih satu sampai dua besaran itu adalah bahan bakar cache. Data yang dibaca seratus kali dan berubah sekali sehari hampir seluruhnya menempuh jalur murah pertama bila kita menyimpan salinannya di memori — dan satu-satunya yang tetap menempuh jalur mahal adalah permintaan pertama setelah data berubah.
Service cache di compose.yaml #
Redis hadir sebagai penghuni baru backend-net, dan backend diberi alamatnya lewat environment — dua perubahan yang menempel pada pola yang sudah tiga kali kita pakai di section ini:
cache:
image: redis:7-alpine
command: redis-server --maxmemory 128mb --maxmemory-policy allkeys-lru
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
networks:
- backend-net
backend:
build: ./api
image: toko-api:dev
environment:
DATABASE_URL: postgres://toko:rahasia123@db:5432/toko
REDIS_URL: redis://cache:6379
networks:
- frontend-net
- backend-net
depends_on:
db:
condition: service_healthy
cache:
condition: service_healthyMari kita telaah empat keputusan pada cuplikan tersebut:
image: redis:7-alpine— varian stabil yang sama dengan catatan riset Course ini; Redis 8 tersedia, tetapi7-alpinecukup dan lebih teruji untuk kebutuhan kita.command: redis-server --maxmemory 128mb --maxmemory-policy allkeys-lru— menimpaCMDimage (pola Chapter Command Override) dengan dua batas:maxmemory 128mbmembatasi memori aplikasi Redis untuk datanya — perlu digarisbawahi bahwa ini bukan batas cgroup container, melainkan batas internal yang dijaga Redis sendiri — danallkeys-lrumenyuruh Redis membuang kunci yang paling lama tidak dipakai begitu batas tercapai. Kombinasi ini adalah jati diri cache: boleh penuh, boleh kehilangan isi, tidak boleh membengkak.healthcheckdenganredis-cli ping— probe paling jujur untuk Redis: bertanya ke server sungguhan dan menuntut jawabanPONG; gerbangservice_healthybackend kini menjaga dua service sekaligus, db dan cache.REDIS_URL: redis://cache:6379— alamat koneksi memakai nama service dan port internal, sepenuhnya seiman denganDATABASE_URL; tanpaports, cache tidak pernah terpapar ke host.
Stack menyala dengan backend yang diciptakan ulang dan cache yang melewati masa uji kesehatannya:
$ docker compose up -dOutput yang Dihasilkan (dipotong):
[+] Running 7/7
✔ Network toko_frontend-net Running 0.0s
✔ Network toko_backend-net Running 0.0s
✔ Container toko-db-1 Healthy 0.0s
✔ Container toko-cache-1 Healthy 3.2s
✔ Container toko-backend-1 Recreated 0.3s
✔ Container toko-backend-1 Started 0.6s
...Jawaban PONG dari dalam container membuktikan hidupnya secara langsung:
$ docker compose exec cache redis-cli pingOutput yang Dihasilkan:
PONGPola Cache-Aside #
Cache tidak memasang dirinya sendiri di jalur aplikasi — aplikasi yang harus menggunakannya secara sadar, dan pola paling umum bernama cache-aside: aplikasi mencoba membaca dari cache, hanya turun ke database ketika meleset, lalu menaruh hasilnya kembali ke cache untuk yang berikutnya. Wujudnya pada backend toko — pseudo-code yang memakai klien Redis apa pun di Node.js:
async function ambilProduk(id) {
const kunci = `produk:${id}`;
const tersimpan = await redis.get(kunci);
if (tersimpan !== null) {
return JSON.parse(tersimpan);
}
const hasil = await pool.query(
"SELECT id, nama, harga FROM produk WHERE id = $1",
[id]
);
const produk = hasil.rows[0];
await redis.setex(kunci, 300, JSON.stringify(produk));
return produk;
}Mari kita telaah alur pada pseudo-code tersebut:
redis.get(kunci)lebih dulu — setiap pembacaan menanyakan cache sebelum database; kunciproduk:1sengaja dibangun dari nama tabel dan primary key agar mudah dirunut saat men-debug.if (tersimpan !== null)— jalur hit — jawaban kembali dari memori dalam hitungan sub-milidetik, tanpa menyentuh database sama sekali; inilah baris yang menanggung ratusan permintaan berulang Siti.- Jalur miss turun ke
pool.query— kegagalan menemukan kunci bukan galat melainkan undangan bekerja: baca dari sumber kebenaran. redis.setex(kunci, 300, ...)— hasil ditaruh kembali dengan masa hidup 300 detik; TTL inilah jaring pengaman utama cache — data basi paling lama berumur lima menit walau invalidasi kita lupa dijalankan.
Invalidasi adalah separuh kejujuran pola ini. Begitu harga sebuah produk berubah, kuncinya harus dibuang secara eksplisit, bukan menunggu TTL — lima menit menjual harga lama tetap lima menit terlalu lama untuk toko:
await redis.del(`produk:${id}`);Satu DEL untuk satu kunci yang menjadi sumbernya — bukan FLUSHDB yang mengosongkan seluruh cache dan mengundang banjir permintaan miss serentak ke database.
Pembukuan sore pertama toko terbaca langsung dari dalam container cache:
$ docker compose exec cache redis-cli INFO statsOutput yang Dihasilkan (dipotong):
keyspace_hits:214
keyspace_misses:9
...214 jawaban dari memori berbanding 9 turunan ke database pada sore pertama toko — rasio yang sehat untuk data produk.
Antrian untuk Pekerjaan yang Lambat #
Cache dan antrian sering disebut berdua karena sama-sama memakai penyimpanan cepat, tetapi keduanya menyelesaikan masalah yang berlawanan. Cache menyembunyikan pembacaan yang mahal; antrian menyembunyikan pekerjaan yang lambat. Contoh nyatanya di toko: pelanggan menyelesaikan checkout dan layak menerima jawaban seketika, sementara email struk pembayaran harus dikirim lewat SMTP yang bisa memakan belasan detik. Bila pengiriman email hidup di jalur checkout, pelanggan menunggu lama untuk hal yang bahkan tidak ia minta ditunggu — dan bila SMTP mati, checkout ikut gagal padahal pesanan sudah sah.
Keputusan desainnya disebut decoupling: endpoint checkout hanya mencatat pesanan dan menitipkan tugas ke antrian, lalu menjawab 201 seketika. Pengiriman email dipindahkan ke pekerja yang mengambil tugas dari antrian kapan pun ia sanggup. Redis sudah kita pegang, dan antrian paling ringan di atasnya cukup memakai satu struktur LIST — tanpa service baru sama sekali:
app.post("/checkout", async (req, res) => {
const pesanan = await simpanPesanan(req.body);
await redis.lpush(
"antrian:email-checkout",
JSON.stringify({ email: req.body.email, pesanan: pesanan.id })
);
res.sendStatus(201);
});async function pekerjaEmail() {
while (true) {
const [, isi] = await redis.brpop("antrian:email-checkout", 0);
const tugas = JSON.parse(isi);
await kirimEmail(tugas.email, tugas.pesanan);
}
}Mari kita telaah dua potongan yang saling melengkapi tersebut:
LPUSHdi sisi produksi — endpoint checkout menyelipkan satu pesan JSON ke kepala list; operasi ini selesai dalam hitungan sub-milidetik, sehingga respon pelanggan tidak pernah menunggu SMTP.BRPOPdengan timeout0di sisi pekerja — pekerja memblokir menunggu ekor list, selamanya bila perlu; sepasangLPUSH-BRPOPmenjadikan list sebuah antrian sungguhan — pesanan masuk dari satu ujung, dikerjakan dari ujung lain, urut dan satu per satu.- Pekerja mati tidak memusnahkan tugas — pesan yang sudah masuk list tetap menunggu; begitu pekerja hidup kembali,
BRPOPmelanjutkan dari ekor yang tersisa. - Beban puncak terserap — saat seribu checkout datang bersamaan, list menampung seribu tugas dan pekerja menghabiskannya dengan irama kotak masuk SMTP, bukan irama panik pengunjung.
Karakter antrian Redis ini jujur sebatas itu: bila container Redis mati sebelum pekerja selesai, pesan yang masih di list ikut lenyap — untuk email struk itu risiko yang bisa diterima, untuk pembayaran tidak.
RabbitMQ ketika Antrian Menjadi Serius #
Kebutuhan yang lebih berat — pengakuan pesan (acknowledgment), pesan yang gagal diantar ulang, dead-letter queue untuk tugas yang selalu gagal, routing satu pesan ke banyak konsumen, dan persistensi yang dijamin — adalah wilayah message broker sungguhan. RabbitMQ hadir di toko sebagai service opsional yang tidak menyala pada up biasa, memakai profiles — mekanisme yang akan dibedah tuntas pada Chapter Profiles:
services:
rabbitmq:
image: rabbitmq:4-management
profiles:
- queue
ports:
- "15672:15672"
volumes:
- rmq-data:/var/lib/rabbitmq
networks:
- backend-net
volumes:
db-data:
rmq-data:Mari kita telaah empat baris kuncinya: image rabbitmq:4-management membawa UI manajemen bawaan; profiles: [queue] membuat service ini absen dari docker compose up sehari-hari dan hanya hadir bila profil queue diaktifkan; port host 15672 menerbitkan UI manajemen — satu-satunya alasan RabbitMQ boleh punya ports, karena UI-nya memang ditujukan dibuka dari browser; dan volume rmq-data menjaga definisi antrean serta pesan persisten tetap hidup dari down. Aplikasi yang memakainya cukup menyambung ke amqp://rabbitmq:5672 — port internal 5672, tanpa penerbitan, seperti layanan internal mana pun:
$ docker compose --profile queue up -dOutput yang Dihasilkan:
[+] Running 2/2
✔ Volume toko_rmq-data Created 0.4s
✔ Container toko-rabbitmq-1 Started 3.1sHanya dua entitas yang bergerak — volume baru dan container baru; seluruh service lain tidak tersentuh karena deklarasinya tidak berubah, persis karakter idempoten yang kita saksikan pada Chapter Port dan Network. UI manajemen kini terbuka di localhost:15672 dari browser mana pun di mesin host.
Cache, Queue, dan Database dalam Satu Tabel #
Ketiga penyimpanan itu kini berdiri berdampingan di backend-net, dan batas keputusan antar mereka sebaiknya dipegang sebagai tabel:
| Aspek | db (PostgreSQL) | cache (Redis) | queue (LIST Redis / RabbitMQ) |
|---|---|---|---|
| Peran | sumber kebenaran | mempercepat pembacaan berulang | memutus pekerjaan lambat dari respon |
| Isi khas | data transaksi | salinan data panas | tugas yang menunggu dikerjakan |
| Boleh hilang? | tidak boleh | boleh — dibangun ulang dari db | sebagian — pesan hilang = pekerjaan hilang |
| Volume | wajib (db-data) | opsional | disarankan (rmq-data untuk RabbitMQ) |
| Ukuran | tumbuh terus | dibatasi maxmemory | kecil dan terus mengalir |
Baris ketiga adalah pemisah paling penting: cache dirancang untuk bisa musnah kapan saja, queue menampung janji yang belum ditebus, dan database menampung kebenaran yang tidak boleh hilang — tiga tingkat komitmen yang menuntut tiga tingkat perlakuan.
Toko kini lengkap lima service yang saling menyapa dengan nama. Tetapi seluruh percakapan itu bertumpu pada satu asumsi yang belum pernah kita uji secara sadar: bahwa nama db, cache, dan backend selalu menyelesaikan menjadi alamat yang benar, saat ini dan setiap kali container berganti. Chapter Service Discovery membongkar mesin di balik asumsi itu — kapan nama terdaftar, kapan dicabut, batas resolusi per network — lalu melengkapi aplikasi dengan satu kebiasaan terakhir yang membedakan stack yang tahan banting: retry dengan backoff eksponensial.
Referensi #
- Redis — Docker Hub
— image
redis:7-alpinedan perilakuredis-serversebagai command. - Redis Documentation
—
maxmemory, kebijakanallkeys-lru,SETEX, sertaLPUSHdanBRPOP. - RabbitMQ — Docker Hub
— image
rabbitmq:4-management, port UI 15672, dan batasan userguest. - profiles — Compose File Reference
— kunci
profilesuntuk service opsional.