Redis

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 #

AspekPredisPhpRedis
Instalasicomposer require predis/predispecl install redis + phpenmod redis
PerformaLebih lambat (PHP murni)Jauh lebih cepat (ekstensi C)
Portabilitas✓ Tanpa ekstensi✗ Butuh ekstensi
FiturLengkapLengkap + lebih
Cocok untukDevelopment, prototypingProduction

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 EXPIRE atau SETEX untuk 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.

← Sebelumnya: Elasticsearch   Berikutnya: Memcached →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact