Redis #
Redis (Remote Dictionary Server) adalah in-memory data store yang luar biasa serbaguna — ia bisa berfungsi sebagai cache, session storage, message broker, rate limiter, leaderboard real-time, job queue, dan banyak lagi. Yang membuat Redis unik dibanding cache sederhana adalah dukungannya untuk berbagai struktur data: String, Hash, List, Set, Sorted Set, dan lainnya — masing-masing dioptimalkan untuk use case yang berbeda. PHP mengakses Redis melalui dua library utama: Predis (murni PHP, mudah diinstal via Composer) dan PhpRedis (ekstensi C yang jauh lebih cepat, direkomendasikan untuk production). Artikel ini membahas semua struktur data Redis dengan pola penggunaan nyata di PHP.
Predis vs PhpRedis #
| Aspek | Predis | PhpRedis |
|---|---|---|
| Instalasi | composer require predis/predis | pecl install redis + phpenmod redis |
| Performa | Lebih lambat (PHP murni) | Jauh lebih cepat (ekstensi C) |
| Portabilitas | ✓ Tanpa ekstensi | ✗ Butuh ekstensi |
| Fitur | Lengkap | Lengkap + lebih |
| Cocok untuk | Development, prototyping | Production |
Artikel ini menggunakan PhpRedis untuk contoh kode karena lebih umum dipakai di production, tapi hampir semua sintaks identik di Predis.
Instalasi #
# PhpRedis (direkomendasikan untuk production)
sudo apt install php8.3-redis
# atau via PECL:
sudo pecl install redis
echo "extension=redis.so" | sudo tee /etc/php/8.3/mods-available/redis.ini
sudo phpenmod redis
# Predis (alternatif murni PHP)
composer require predis/predis
# Verifikasi PhpRedis
php -m | grep redis
Koneksi #
<?php
// PhpRedis
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$redis->auth('password-jika-ada'); // opsional
$redis->select(0); // pilih database (0-15, default 0)
// Dengan opsi timeout dan retry
$redis->connect('127.0.0.1', 6379, timeout: 2.0, retry_interval: 100);
// Persistent connection (reuse koneksi)
$redis->pconnect('127.0.0.1', 6379);
// Redis Cluster
$redis = new RedisCluster(null, [
'127.0.0.1:7000',
'127.0.0.1:7001',
'127.0.0.1:7002',
]);
// Redis Sentinel (high availability)
$sentinel = new RedisSentinel('127.0.0.1', 26379);
$master = $sentinel->getMasterAddrByName('mymaster');
$redis->connect($master[0], $master[1]);
// Predis
use Predis\Client;
$redis = new Client([
'scheme' => 'tcp',
'host' => '127.0.0.1',
'port' => 6379,
'password' => 'password-jika-ada',
'database' => 0,
]);
// Koneksi ke Redis Cloud / Upstash
$redis = new Client('rediss://username:password@hostname:6380');
String — Tipe Data Paling Dasar #
String di Redis bukan hanya teks — bisa menyimpan integer, float, JSON, atau binary data hingga 512MB:
<?php
// Set dan Get
$redis->set('kunci', 'nilai');
$nilai = $redis->get('kunci'); // "nilai"
// Set dengan TTL (Time To Live) — expired otomatis
$redis->setex('session:user:42', 3600, json_encode(['id' => 42, 'nama' => 'Budi'])); // expired 1 jam
$redis->set('token:abc123', 'user-id-42', ['ex' => 86400]); // syntax alternatif
// NX — hanya set jika belum ada (distributed lock)
$berhasil = $redis->set('lock:order:42', '1', ['nx' => true, 'ex' => 30]);
if (!$berhasil) {
throw new \RuntimeException("Order 42 sedang diproses oleh proses lain");
}
// XX — hanya set jika sudah ada
$redis->set('counter', '100', ['xx' => true]);
// Increment / Decrement — atomic operation
$redis->set('views:artikel:1', 0);
$redis->incr('views:artikel:1'); // +1
$redis->incrBy('views:artikel:1', 5); // +5
$redis->decrBy('views:artikel:1', 2); // -2
$views = $redis->get('views:artikel:1'); // string "4"
// Increment float
$redis->set('saldo', '1000.50');
$redis->incrByFloat('saldo', 500.25); // 1500.75
// GetSet — ambil nilai lama, set nilai baru (atomic)
$nilaiLama = $redis->getset('status', 'aktif'); // atomic get+set
// MSET / MGET — multiple keys sekaligus
$redis->mset(['a' => '1', 'b' => '2', 'c' => '3']);
$nilai = $redis->mget(['a', 'b', 'c', 'tidak_ada']); // ['1', '2', '3', false]
// Operasi string
$redis->set('nama', 'Budi');
$redis->append('nama', ' Santoso'); // "Budi Santoso"
echo $redis->strlen('nama'); // 12
echo $redis->getrange('nama', 0, 3); // "Budi"
Hash — Map di Dalam Redis #
Hash menyimpan sekumpulan field-value dalam satu key — ideal untuk objek atau record:
<?php
// HSET / HGET / HGETALL
$redis->hset('user:42', 'nama', 'Budi Santoso');
$redis->hset('user:42', 'email', '[email protected]');
$redis->hset('user:42', 'role', 'admin');
// Atau set semua sekaligus
$redis->hmset('user:42', [
'nama' => 'Budi Santoso',
'email' => '[email protected]',
'role' => 'admin',
'login_count'=> 0,
'created_at' => time(),
]);
echo $redis->hget('user:42', 'nama'); // Budi Santoso
$semuaField = $redis->hgetall('user:42');
// ['nama' => 'Budi Santoso', 'email' => '...', ...]
// HMGET — ambil beberapa field
$fields = $redis->hmget('user:42', ['nama', 'email', 'tidak_ada']);
// ['Budi Santoso', '[email protected]', false]
// HINCRBY — increment field dalam hash
$redis->hincrby('user:42', 'login_count', 1);
echo $redis->hget('user:42', 'login_count'); // 1
// Cek keberadaan field
var_dump($redis->hexists('user:42', 'nama')); // bool(true)
var_dump($redis->hexists('user:42', 'phone')); // bool(false)
// Daftar field dan nilai
$fields = $redis->hkeys('user:42'); // ['nama', 'email', 'role', ...]
$values = $redis->hvals('user:42'); // ['Budi Santoso', '...', ...]
echo $redis->hlen('user:42'); // 5
// Hapus field
$redis->hdel('user:42', 'role');
// TTL untuk hash — set pada key keseluruhan, bukan per field
$redis->expire('user:42', 1800); // expired 30 menit
List — Queue dan Stack #
List adalah doubly linked list — bisa ditambah/diambil dari kedua ujung:
<?php
// LPUSH / RPUSH — tambah ke kiri (head) / kanan (tail)
$redis->rpush('antrian:email', json_encode(['ke' => '[email protected]', 'subjek' => 'Halo']));
$redis->rpush('antrian:email', json_encode(['ke' => '[email protected]', 'subjek' => 'Selamat']));
$redis->lpush('log:error', "Error 500: Internal server error");
// RPOP / LPOP — ambil dari kanan / kiri (non-blocking)
$tugas = $redis->lpop('antrian:email'); // ambil dari depan antrian (FIFO)
$data = json_decode($tugas, true);
// BLPOP / BRPOP — blocking pop (tunggu sampai ada item)
// Sangat berguna untuk worker queue
$item = $redis->blpop(['antrian:email', 'antrian:sms'], timeout: 5); // timeout 5 detik
// $item = ['antrian:email', 'json...'] atau null jika timeout
// Implementasi worker
while (true) {
$item = $redis->blpop(['antrian:jobs'], timeout: 0); // timeout 0 = tunggu selamanya
if ($item !== null) {
[$queue, $payload] = $item;
$job = json_decode($payload, true);
prosesJob($job);
}
}
// LRANGE — ambil range elemen (0 = pertama, -1 = terakhir)
$recentErrors = $redis->lrange('log:error', 0, 9); // 10 error terbaru
// LLEN — panjang list
echo $redis->llen('antrian:email'); // jumlah email dalam antrian
// LTRIM — pertahankan hanya sebagian list (hapus sisanya)
// Berguna untuk activity log yang hanya simpan N item terbaru
$redis->lpush('activity:user:42', json_encode(['aksi' => 'login', 'waktu' => time()]));
$redis->ltrim('activity:user:42', 0, 99); // simpan hanya 100 terbaru
Set — Koleksi Unik #
Set menyimpan koleksi nilai unik tanpa urutan — cocok untuk tags, followers, online users:
<?php
// SADD / SMEMBERS
$redis->sadd('tags:artikel:1', 'php', 'redis', 'cache', 'backend');
$redis->sadd('tags:artikel:2', 'php', 'mysql', 'database');
$tags = $redis->smembers('tags:artikel:1'); // ['php', 'redis', 'cache', 'backend']
echo $redis->scard('tags:artikel:1'); // 4 (jumlah anggota)
// Cek keanggotaan
var_dump($redis->sismember('tags:artikel:1', 'php')); // bool(true)
var_dump($redis->sismember('tags:artikel:1', 'java')); // bool(false)
// Operasi himpunan
$irisan = $redis->sinter('tags:artikel:1', 'tags:artikel:2'); // ['php']
$gabungan = $redis->sunion('tags:artikel:1', 'tags:artikel:2'); // ['php','redis','cache','backend','mysql','database']
$beda = $redis->sdiff('tags:artikel:1', 'tags:artikel:2'); // ['redis','cache','backend']
// Simpan hasil ke key baru
$redis->sinterstore('tags:common', 'tags:artikel:1', 'tags:artikel:2');
// User online — tambah/hapus
$redis->sadd('users:online', 'user:42');
$redis->sadd('users:online', 'user:99');
echo $redis->scard('users:online') . " user online\n";
// Remove
$redis->srem('users:online', 'user:42');
// Random member (berguna untuk sampling atau hadiah random)
$randomUser = $redis->srandmember('users:online');
$dipilih = $redis->spop('users:online'); // ambil dan hapus random member
Sorted Set — Leaderboard dan Rate Limiting #
Sorted Set seperti Set tapi setiap anggota punya skor float — diurutkan berdasarkan skor:
<?php
// ZADD — tambah dengan skor
$redis->zadd('leaderboard:game', ['BudiSantoso' => 9500]);
$redis->zadd('leaderboard:game', ['SitiRahayu' => 11200]);
$redis->zadd('leaderboard:game', ['DaniPratama' => 8750]);
$redis->zadd('leaderboard:game', ['RinaAmelia' => 12000]);
// ZRANGE — urutkan ascending (skor terendah dulu)
$semua = $redis->zrange('leaderboard:game', 0, -1, ['withscores' => true]);
// ZREVRANGE — urutkan descending (skor tertinggi dulu) — TOP N
$top3 = $redis->zrevrange('leaderboard:game', 0, 2, ['withscores' => true]);
// ['RinaAmelia' => 12000, 'SitiRahayu' => 11200, 'BudiSantoso' => 9500]
// ZRANK / ZREVRANK — posisi dalam ranking (0-based)
echo $redis->zrevrank('leaderboard:game', 'BudiSantoso'); // 2 (peringkat 3)
// ZINCRBY — tambah skor
$redis->zincrby('leaderboard:game', 500, 'BudiSantoso'); // skor jadi 10000
// ZSCORE — ambil skor
echo $redis->zscore('leaderboard:game', 'RinaAmelia'); // 12000
// Rate Limiting dengan Sorted Set — sliding window
function cekRateLimit(Redis $redis, string $userId, int $maxRequest = 10, int $windowDetik = 60): bool
{
$sekarang = microtime(true) * 1000; // milidetik
$batas = $sekarang - ($windowDetik * 1000);
$key = "rate_limit:$userId";
// Hapus request yang sudah lebih dari window
$redis->zremrangebyscore($key, '-inf', $batas);
// Hitung request dalam window
$jumlah = $redis->zcard($key);
if ($jumlah >= $maxRequest) {
return false; // rate limit terlampaui
}
// Tambah request ini
$redis->zadd($key, [$sekarang => $sekarang]);
$redis->expire($key, $windowDetik + 1); // TTL agar key bersih sendiri
return true;
}
// Gunakan
if (!cekRateLimit($redis, 'user:42', maxRequest: 100, windowDetik: 60)) {
http_response_code(429);
header('Retry-After: 60');
die(json_encode(['error' => 'Terlalu banyak request. Coba lagi dalam 1 menit.']));
}
Pub/Sub — Komunikasi Antar Proses #
<?php
// Publisher — kirim pesan ke channel
$redis->publish('notifikasi:order', json_encode([
'event' => 'order_selesai',
'order_id' => 42,
'user_id' => 99,
]));
// Subscriber — dengarkan channel (proses terpisah)
// PERHATIAN: setelah subscribe, koneksi hanya bisa digunakan untuk pub/sub
$redisSubscriber = new Redis();
$redisSubscriber->connect('127.0.0.1', 6379);
$redisSubscriber->subscribe(['notifikasi:order', 'notifikasi:stok'], function($redis, $channel, $message) {
$data = json_decode($message, true);
echo "Channel: $channel\n";
echo "Event: {$data['event']}\n";
match($channel) {
'notifikasi:order' => prosesNotifOrder($data),
'notifikasi:stok' => prosesAlertStok($data),
};
});
// Pattern subscribe — dengari channel dengan pattern
$redisSubscriber->psubscribe(['notifikasi:*'], function($redis, $pattern, $channel, $message) {
echo "Pattern: $pattern, Channel: $channel\n";
echo "Pesan: $message\n";
});
Pipeline dan Transaksi #
<?php
// Pipeline — kirim banyak perintah sekaligus tanpa menunggu respons
// Mengurangi round-trip network secara drastis
$redis->pipeline(function($pipe) {
for ($i = 0; $i < 1000; $i++) {
$pipe->set("key:$i", "nilai-$i");
$pipe->expire("key:$i", 3600);
}
});
// 2000 perintah dikirim dalam satu batch — jauh lebih cepat dari 2000 round-trip
// Transaksi — MULTI/EXEC (semua perintah berhasil atau tidak ada)
$redis->multi(); // mulai transaksi
$redis->set('saldo:dari', 500);
$redis->set('saldo:ke', 1500);
$redis->exec(); // commit
// Rollback
$redis->multi();
$redis->set('kunci', 'nilai');
$redis->discard(); // batalkan transaksi
// WATCH — optimistic locking
$redis->watch('saldo:user:42');
$saldo = (float) $redis->get('saldo:user:42');
if ($saldo < 100000) {
$redis->unwatch();
throw new \DomainException("Saldo tidak cukup");
}
$redis->multi();
$redis->decrby('saldo:user:42', 100000);
$redis->incrby('saldo:merchant:99', 100000);
$hasil = $redis->exec();
if ($hasil === false) {
// Data berubah sejak WATCH — transaksi dibatalkan
throw new \RuntimeException("Transaksi gagal karena data berubah bersamaan");
}
Pola Caching yang Umum #
<?php
class CacheService
{
public function __construct(private Redis $redis) {}
// Cache-aside pattern — cek cache dulu, ambil dari DB jika miss
public function remember(string $kunci, int $ttl, callable $callback): mixed
{
$cached = $this->redis->get($kunci);
if ($cached !== false) {
return json_decode($cached, true);
}
$data = $callback();
$this->redis->setex($kunci, $ttl, json_encode($data));
return $data;
}
// Invalidasi cache setelah update
public function forget(string $kunci): void
{
$this->redis->del($kunci);
}
public function forgetPattern(string $pattern): void
{
$keys = $this->redis->keys($pattern); // HATI-HATI: KEYS bisa lambat di produksi
if (!empty($keys)) {
$this->redis->del($keys);
}
}
}
// Penggunaan
$cache = new CacheService($redis);
$produk = $cache->remember(
kunci: "produk:detail:42",
ttl: 3600, // 1 jam
callback: fn() => $db->query("SELECT * FROM produk WHERE id = 42")->fetch(),
);
// Setelah update database, invalidasi cache
$db->prepare("UPDATE produk SET harga = ? WHERE id = 42")->execute([14000000]);
$cache->forget("produk:detail:42");
Ringkasan #
- PhpRedis (ekstensi C) untuk production — jauh lebih cepat dari Predis (PHP murni). Predis untuk development atau environment tanpa kendali instalasi ekstensi.
- Pilih struktur data yang tepat: String untuk nilai tunggal/counter, Hash untuk objek/record, List untuk queue/stack, Set untuk koleksi unik, Sorted Set untuk leaderboard dan sliding window rate limit.
- TTL wajib untuk cache — set
EXPIREatauSETEXuntuk semua kunci cache agar Redis tidak kehabisan memori karena kunci yang tidak pernah dihapus.- BLPOP untuk worker queue — blocking pop menunggu sampai ada item tanpa busy-wait. Jauh lebih efisien dari polling LPOP dalam loop.
- Pipeline untuk batch operasi — mengirim banyak perintah dalam satu round-trip. Untuk 1000 operasi, pipeline bisa 100x lebih cepat dari eksekusi satu per satu.
- MULTI/EXEC untuk atomisitas — semua perintah dalam transaksi berhasil atau semua dibatalkan. Gunakan WATCH untuk optimistic locking.
- Rate limiting dengan Sorted Set — sliding window menggunakan timestamp sebagai skor memberikan batas yang akurat dibanding fixed window counter.
- Pub/Sub untuk event — setelah
subscribe(), koneksi Redis hanya bisa digunakan untuk pub/sub. Gunakan koneksi terpisah untuk operasi data biasa.