Pada chapter sebelumnya kita menyimulasikan cache dengan array PHP — pendekatan yang berhenti bekerja begitu request selesai, karena array tersebut ikut musnah bersama prosesnya. Aplikasi nyata membutuhkan cache yang hidup lintas request dan, ketika berjalan di beberapa server, lintas mesin. Redis adalah jawaban paling populer untuk kebutuhan itu: penyimpan key-value berbasis memori yang cepat, kaya struktur data, dan dapat diakses bersama oleh seluruh server aplikasi. Karena menjalankan Redis membutuhkan server terpisah, contoh operasi Redis pada chapter ini disajikan sebagai blok khusus; sementara bagian yang dapat diuji tanpa server — deteksi ekstensi, serialisasi nilai, dan pola read-through — kita jalankan sungguhan.
Apa Itu Redis #
Redis (REmote DIctionary Server) adalah penyimpan data key-value yang menempatkan seluruh datanya di RAM. Karena membaca tidak menyentuh disk sama sekali, satu operasi Redis biasanya selesai dalam hitungan mikrodetik — ratusan hingga ribuan kali lebih cepat daripada membaca dari disk. Nilai di Redis tidak terbatas pada string polos: Redis mendukung string, hash, list, set, dan sorted set sebagai tipe bawaan, ditambah fitur seperti TTL per key dan publish/subscribe.
Persistensi di Redis bersifat opsional. Mode RDB menyimpan snapshot data secara berkala, sedangkan mode AOF mencatat setiap penulisan ke log; keduanya bisa dimatikan untuk cache murni karena data cache selalu bisa diisi ulang dari sumber aslinya. Satu karakteristik lagi yang penting untuk dipahami: Redis memproses perintah satu per satu melalui event loop. Akibatnya, operasi seperti menambah sebuah counter dari dua worker yang bersamaan tetap dihitung dengan benar — sifat atomik didapat tanpa perlu kunci apa pun.
Kapan Memilih Redis #
Pilihan penyimpan cache ditentukan oleh lingkup hidup data dan arsitektur aplikasi. Tabel berikut memetakan tiga pilihan yang paling umum:
| Penyimpanan | Lingkup hidup | Kecepatan relatif | Dibagi antar server | Cocok untuk |
|---|---|---|---|---|
| Array PHP | satu request | sangat cepat | tidak | hasil antara dalam satu request |
| Cache berbasis berkas | lintas request | sedang (melalui disk) | sulit | hasil render besar yang jarang berubah |
| Redis | lintas request dan server | sangat cepat (RAM) | ya | data panas, counter, session, lock |
Selama aplikasi berjalan pada satu server, cache berbasis berkas atau cache lokal masih memadai. Begitu ada dua server di belakang load balancer, cache harus dibagi bersama: invalidasi yang dilakukan server A harus langsung terlihat oleh server B, jika tidak, pengguna akan menerima data berbeda tergantung server mana yang kebetulan melayani request-nya. Di titik ini Redis menjadi pilihan standar, termasuk untuk session storage dan pembatas laju request.
phpredis vs Predis #
Ada dua jalur utama untuk berbicara dengan Redis dari PHP. phpredis adalah ekstensi yang ditulis dalam C: kinerjanya tinggi, tetapi ekstensi ini harus tersedia di server — banyak layanan hosting bersama tidak memasangnya. Predis adalah library PHP murni yang dipasang lewat Composer: portabel ke lingkungan mana pun, dengan harga eksekusi sedikit lebih lambat. Sebelum memilih, periksa dulu apa yang tersedia di lingkungan Anda:
<?php
// Cek ketersediaan client Redis tanpa perlu koneksi ke server
$phpredisTerpasang = extension_loaded("redis");
$classRedisAda = class_exists("Redis");
$metodeSetexAda = method_exists("Redis", "setex");
echo "Ekstensi phpredis terpasang: " . ($phpredisTerpasang ? "ya" : "tidak") . "\n";
echo "Class Redis tersedia: " . ($classRedisAda ? "ya" : "tidak") . "\n";
echo "Metode setex() tersedia: " . ($metodeSetexAda ? "ya" : "tidak") . "\n";Output yang Dihasilkan:
Ekstensi phpredis terpasang: ya
Class Redis tersedia: ya
Metode setex() tersedia: yaMari kita telaah ketiga pemeriksaan di atas:
extension_loaded("redis")memastikan ekstensi C phpredis aktif di build PHP.class_exists("Redis")memastikan class client benar-benar terdaftar — pemeriksaan struktur yang lebih spesifik daripada sekadar nama ekstensi.method_exists("Redis", "setex")berguna sebelum memanggil metode yang hanya tersedia pada versi phpredis tertentu, sehingga kode dapat memberi jalur cadangan.
Ketiga baris mengembalikan ya pada lingkungan pengujian chapter ini karena ekstensi phpredis memang terpasang — meskipun tanpa server Redis yang hidup, struktur kodenya tetap dapat diperiksa. Contoh-contoh berikutnya memakai API phpredis; padanan di Predis serupa ($client->set(), $client->get(), dan sebagainya).
Operasi Dasar #
Operasi Redis yang paling sering dipakai untuk caching hanya segelintir. Berikut bentuknya pada phpredis — blok ini membutuhkan server Redis yang berjalan sehingga tidak dieksekusi di lingkungan chapter:
<?php
// Operasi dasar Redis (butuh server Redis berjalan)
$redis = new Redis();
$redis->connect("127.0.0.1", 6379);
$redis->set("salam", "Halo Redis"); // SET tanpa TTL
$redis->setex("sesi:abc", 3600, "budi"); // SET dengan TTL 3600 detik
echo $redis->get("salam") . "\n"; // Halo Redis
echo $redis->ttl("sesi:abc") . "\n"; // 3600
set() menyimpan nilai tanpa batas umur, get() membacanya kembali, dan ttl() melaporkan sisa detik sebelum key kedaluwarsa. Perhatikan setex(): satu perintah yang sekaligus menetapkan nilai dan TTL. Pola ini lebih aman daripada memanggil set() lalu expire() secara terpisah — bila proses mati di antara kedua perintah tersebut, key akan tersimpan tanpa TTL dan menjadi abadi. Untuk penamaan, gunakan tanda titik dua sebagai pemisah namespace, misalnya produk:detail:7, sehingga key mudah dipetakan dan diaudit.
Serialisasi Nilai #
Redis menyimpan nilai sebagai string/byte, sehingga array dan object PHP harus diubah lebih dulu sebelum disimpan. Dua kandidat utamanya adalah json_encode() dan serialize(). JSON portabel: data yang sama dapat dibaca oleh layanan Go, JavaScript, atau Python. serialize() spesifik PHP: formatnya mengikat tipe PHP lebih kaya (termasuk object dan protected property), tetapi hanya dipahami oleh PHP. Mari kita bandingkan keduanya lewat roundtrip data yang sama:
<?php
$produk = [
"nama" => "Kamera Mirrorless",
"harga" => 12500000,
"tag" => ["elektronik", "fotografi"],
];
// JSON: format teks universal, bisa dibaca layanan lain
$json = json_encode($produk, JSON_UNESCAPED_UNICODE);
$dariJson = json_decode($json, true);
// serialize: format khusus PHP, mendukung object dan private property
$biner = serialize($produk);
$dariSerialize = unserialize($biner);
echo $json . "\n";
echo "Roundtrip JSON identik: " . var_export($dariJson === $produk, true) . "\n";
echo "Roundtrip serialize identik: " . var_export($dariSerialize === $produk, true) . "\n";
echo "Lebih ringkas: " . (strlen($json) <= strlen($biner) ? "json_encode" : "serialize") . "\n";Output yang Dihasilkan:
{"nama":"Kamera Mirrorless","harga":12500000,"tag":["elektronik","fotografi"]}
Roundtrip JSON identik: true
Roundtrip serialize identik: true
Lebih ringkas: json_encodeKedua roundtrip menghasilkan array yang identik dengan data asli, dan pada contoh ini json_encode() menghasilkan representasi yang lebih pendek. Untuk cache yang dibaca tulis antar layanan, JSON adalah pilihan yang tepat; serialize() layak dipilih hanya bila nilai cache berisi object PHP lengkap dengan visibility property yang harus dipertahankan.
TTL dan Operasi Atomik #
Counter dan rate limiter adalah kasus di mana sifat atomik Redis benar-benar dibutuhkan. Pendekatan naif — baca dengan get(), tambah di PHP, tulis kembali dengan set() — mengandung race condition: dua worker yang membaca nilai yang sama akan saling menimpa, dan salah satu penambahan hilang. Perintah incr() menyelesaikannya karena Redis mengeksekusinya secara utuh tanpa bisa disisipkan perintah lain di tengahnya.
<?php
// Rate limiter sederhana dengan INCR + EXPIRE (butuh server Redis)
$redis = new Redis();
$redis->connect("127.0.0.1", 6379);
$idKlien = "klien:203.0.113.7";
$jumlah = $redis->incr("$idKlien:permintaan");
if ($jumlah === 1) {
// Permintaan pertama pada jendela ini → mulai hitung TTL 60 detik
$redis->expire("$idKlien:permintaan", 60);
}
if ($jumlah > 100) {
http_response_code(429);
exit("Terlalu banyak permintaan");
}Alurnya: incr() menaikkan counter dan mengembalikan nilai barunya; bila hasilnya 1, artinya inilah permintaan pertama sehingga jendela waktu 60 detik dimulai lewat expire(). Permintaan ke-101 dan seterusnya ditolak dengan status 429. Pola kerabat yang sama dipakai untuk lock terdistribusi melalui SET key nilai NX EX detik — set hanya bila key belum ada, sekaligus memasang TTL agar lock tidak menggantung selamanya bila pemiliknya mati.
Pola Read-Through #
Menyebarkan panggilan get()/set() ke seluruh kode aplikasi membuat logika cache sulit dirawat. Pola read-through memusatkan logika tersebut pada satu helper: aplikasi hanya meminta data, helper yang memeriksa cache, memuat dari sumber lambat bila miss, mengisi ulang cache, lalu mengembalikan hasilnya. Logika murni pola ini bisa kita uji tanpa server:
<?php
class ReadThroughCache
{
private array $storage = [];
public function __construct(private readonly Closure $backend) {}
public function get(string $key): mixed
{
if (array_key_exists($key, $this->storage)) {
return $this->storage[$key]; // cache hit
}
$nilai = ($this->backend)($key); // cache miss: muat dari sumber lambat
$this->storage[$key] = $nilai; // simpan untuk permintaan berikutnya
return $nilai;
}
}
$panggilanBackend = 0;
$backend = function (string $key) use (&$panggilanBackend): string {
$panggilanBackend++;
return "data $key dari database";
};
$cache = new ReadThroughCache($backend);
$cache->get("produk:1"); // miss → backend dipanggil
$cache->get("produk:1"); // hit
$cache->get("produk:1"); // hit
echo "Tiga permintaan, panggilan backend: $panggilanBackend\n";Output yang Dihasilkan:
Tiga permintaan, panggilan backend: 1Constructor menerima Closure $backend — tiruan sumber data lambat yang menghitung berapa kali dirinya dipanggil. Permintaan pertama menemukan miss, memanggil backend, lalu menyimpan hasilnya; dua permintaan berikutnya dilayani dari storage. Meskipun get() dipanggil tiga kali, backend hanya dipanggil satu kali — persis perilaku read-through pada Redis sungguhan. Pada implementasi nyata, isi method get() diganti dengan $this->redis->get($key) dan setex(), sementara $backend menjadi query database.
Konfigurasi Koneksi dari Environment #
Alamat server, nomor database, dan kredensial tidak boleh ditulis permanen di dalam kode — keduanya berbeda antar lingkungan (pengembangan, staging, produksi) dan rahasia seperti kata sandi tidak boleh masuk ke repositori. Baca konfigurasi dari environment variable dengan nilai bawaan yang masuk akal:
<?php
// Konfigurasi koneksi dibaca dari environment, bukan di-hardcode
$host = getenv("REDIS_HOST") ?: "127.0.0.1";
$port = (int) (getenv("REDIS_PORT") ?: "6379");
$database = (int) (getenv("REDIS_DB") ?: "0");
// $redis = new Redis();
// $redis->connect($host, $port);
// $redis->select($database);
Dengan pola ini, mesin pengembangan cukup memakai default lokal, sedangkan server produksi menyuntikkan REDIS_HOST dan variabel lainnya lewat konfigurasi proses atau platform deploy. Kata sandi Redis (bila diaktifkan) mengikuti pola yang sama lewat REDIS_PASSWORD — nilainya disimpan di secret manager, bukan di berkas yang ikut ter-commit.
Ringkasan #
Redis menyediakan cache bersama yang cepat dengan TTL per key, operasi atomik, dan struktur data yang kaya. Kita telah melihat cara memeriksa ketersediaan client (phpredis versus Predis), operasi dasar set/get/setex, pilihan serialisasi JSON versus serialize(), pola read-through yang memusatkan logika cache, serta konfigurasi koneksi dari environment. Cache memang membuat pembacaan cepat, tetapi bila sumber datanya sendiri lambat karena query yang boros, cache hanya menutupi gejala — chapter Query Optimization
berikutnya membedah sumber data tersebut secara langsung.