Redis

Redis #

Redis (Remote Dictionary Server) adalah in-memory data structure store yang digunakan sebagai cache, database, message broker, dan session store. Keunggulan utama Redis adalah kecepatannya yang luar biasa — semua operasi berjalan dalam sub-milidetik karena data disimpan di RAM. Redis bukan sekadar key-value store sederhana: ia mendukung berbagai tipe data (String, Hash, List, Set, Sorted Set, Stream) yang masing-masing memiliki use case spesifik. Package redis menyediakan client Dart untuk berkomunikasi dengan Redis via protokol RESP (Redis Serialization Protocol).

Mengapa Redis? #

flowchart LR
    App["Aplikasi Dart"] -->|request| Cache{"Data ada\ndi Redis?"}
    Cache -->|Cache HIT\n~0.1ms| App
    Cache -->|Cache MISS| DB["Database\n~100ms"]
    DB -->|simpan ke Redis| Cache
    DB -->|kembalikan data| App

Redis digunakan untuk berbagai kebutuhan:

CACHE        — simpan hasil query database yang mahal
SESSION      — simpan data sesi pengguna dengan TTL
RATE LIMITER — batasi jumlah request per pengguna per waktu
LOCK         — distributed lock untuk operasi yang tidak boleh paralel
PUB/SUB      — real-time messaging antar service
LEADERBOARD  — sorted set untuk ranking skor
QUEUE        — task queue sederhana dengan list

Setup Package #

dart pub add redis
# pubspec.yaml
dependencies:
  redis: ^4.1.0

Koneksi ke Redis #

import 'package:redis/redis.dart';

Future<void> main() async {
  // Koneksi sederhana
  final conn = RedisConnection();
  final command = await conn.connect('localhost', 6379);

  // Autentikasi jika Redis dikonfigurasi dengan password
  await command.send_object(['AUTH', 'password_redis']);

  // Pilih database (default: 0, Redis mendukung 0-15)
  await command.send_object(['SELECT', '0']);

  print('Terhubung ke Redis');

  // Tutup koneksi
  await conn.close();
}

Connection Pool untuk Server #

import 'package:redis/redis.dart';

// Redis connection pool — kelola beberapa koneksi
class RedisPool {
  static final List<Command> _pool = [];
  static final List<bool> _tersedia = [];
  static const int _ukuran = 10;
  static bool _diinisialisasi = false;

  static Future<void> inisialisasi({
    String host = 'localhost',
    int port = 6379,
    String? password,
    int database = 0,
  }) async {
    if (_diinisialisasi) return;

    for (int i = 0; i < _ukuran; i++) {
      final conn = RedisConnection();
      final cmd = await conn.connect(host, port);
      if (password != null) await cmd.send_object(['AUTH', password]);
      if (database > 0) await cmd.send_object(['SELECT', database.toString()]);
      _pool.add(cmd);
      _tersedia.add(true);
    }

    _diinisialisasi = true;
    print('Redis pool diinisialisasi dengan $_ukuran koneksi');
  }

  static Future<T> jalankan<T>(Future<T> Function(Command cmd) operasi) async {
    // Cari koneksi yang tersedia
    for (int i = 0; i < _pool.length; i++) {
      if (_tersedia[i]) {
        _tersedia[i] = false;
        try {
          return await operasi(_pool[i]);
        } finally {
          _tersedia[i] = true;
        }
      }
    }
    // Jika semua sibuk, tunggu dan coba lagi
    await Future.delayed(Duration(milliseconds: 10));
    return jalankan(operasi);
  }
}

// Penggunaan yang lebih sederhana: gunakan package redis_pool
// atau ReidsServer (built-in dalam beberapa versi)

Tipe Data Redis #

String — Tipe Paling Dasar #

import 'package:redis/redis.dart';

Future<void> contohString(Command cmd) async {
  // SET dan GET
  await cmd.set('kunci', 'nilai');
  final nilai = await cmd.get('kunci');
  print(nilai); // 'nilai'

  // SET dengan TTL (expire in seconds)
  await cmd.send_object(['SET', 'sesi:USR001', 'data_sesi', 'EX', '3600']);
  // atau: SETEX
  await cmd.send_object(['SETEX', 'sesi:USR002', '3600', 'data_sesi_2']);

  // SET hanya jika belum ada (NX = Not eXists)
  final berhasil = await cmd.send_object(['SET', 'kunci_baru', 'nilai', 'NX']);
  print(berhasil); // 'OK' jika berhasil, null jika sudah ada

  // INCREMENT — atomic operation untuk counter
  await cmd.set('view_count:artikel_1', '0');
  await cmd.send_object(['INCR', 'view_count:artikel_1']);   // → 1
  await cmd.send_object(['INCRBY', 'view_count:artikel_1', '5']); // → 6
  await cmd.send_object(['DECR', 'view_count:artikel_1']);   // → 5

  // GET dan SET sekaligus
  final lama = await cmd.send_object(['GETSET', 'kunci', 'nilai_baru']);
  print('Nilai lama: $lama');

  // Multiple GET/SET
  await cmd.send_object(['MSET', 'a', '1', 'b', '2', 'c', '3']);
  final banyak = await cmd.send_object(['MGET', 'a', 'b', 'c']);
  print(banyak); // ['1', '2', '3']

  // TTL check
  final ttl = await cmd.send_object(['TTL', 'sesi:USR001']);
  print('TTL tersisa: $ttl detik');

  // Hapus TTL (jadikan persistent)
  await cmd.send_object(['PERSIST', 'sesi:USR001']);

  // Hapus kunci
  await cmd.send_object(['DEL', 'kunci']);
  await cmd.send_object(['DEL', 'kunci_a', 'kunci_b', 'kunci_c']); // batch delete
}

Hash — Map untuk Object #

Future<void> contohHash(Command cmd) async {
  // Set beberapa field sekaligus
  await cmd.send_object([
    'HSET', 'user:USR001',
    'nama', 'Budi Santoso',
    'email', '[email protected]',
    'level', 'premium',
    'loginTerakhir', DateTime.now().toIso8601String(),
  ]);

  // Get satu field
  final nama = await cmd.send_object(['HGET', 'user:USR001', 'nama']);
  print('Nama: $nama');

  // Get semua field dan nilai
  final semuaField = await cmd.send_object(['HGETALL', 'user:USR001']);
  // Hasil berupa flat list: ['nama', 'Budi', 'email', 'budi@...', ...]
  // Konversi ke Map:
  final map = <String, String>{};
  for (int i = 0; i < (semuaField as List).length; i += 2) {
    map[semuaField[i] as String] = semuaField[i + 1] as String;
  }
  print(map);

  // Get beberapa field tertentu
  final fields = await cmd.send_object(['HMGET', 'user:USR001', 'nama', 'email']);

  // Update satu field
  await cmd.send_object(['HSET', 'user:USR001', 'level', 'vip']);

  // Increment field numerik
  await cmd.send_object(['HINCRBY', 'user:USR001', 'loginCount', '1']);

  // Cek apakah field ada
  final ada = await cmd.send_object(['HEXISTS', 'user:USR001', 'nama']);
  print('Field nama ada: ${ada == 1}');

  // Hapus field
  await cmd.send_object(['HDEL', 'user:USR001', 'loginTerakhir']);

  // Jumlah field
  final jumlah = await cmd.send_object(['HLEN', 'user:USR001']);
}

List — Queue dan Stack #

Future<void> contohList(Command cmd) async {
  // LPUSH — push ke kiri (head), RPUSH — push ke kanan (tail)
  await cmd.send_object(['RPUSH', 'antrian:email', '[email protected]', '[email protected]']);
  await cmd.send_object(['LPUSH', 'antrian:email', '[email protected]']); // tambah di depan

  // LRANGE — ambil elemen dalam rentang (0 = pertama, -1 = terakhir)
  final semua = await cmd.send_object(['LRANGE', 'antrian:email', '0', '-1']);
  print(semua); // ['[email protected]', '[email protected]', '[email protected]']

  // LPOP / RPOP — ambil dan hapus dari kiri/kanan (seperti dequeue)
  final pertama = await cmd.send_object(['LPOP', 'antrian:email']);
  print('Diproses: $pertama');

  // BLPOP — blocking pop (tunggu hingga ada elemen, timeout 0 = tunggu selamanya)
  // Berguna untuk task queue yang menunggu tugas
  final tugas = await cmd.send_object(['BLPOP', 'antrian:tugas', '5']); // timeout 5 detik

  // Panjang list
  final panjang = await cmd.send_object(['LLEN', 'antrian:email']);
}

Set — Koleksi Unik #

Future<void> contohSet(Command cmd) async {
  // SADD — tambah anggota
  await cmd.send_object(['SADD', 'tag:artikel_1', 'dart', 'flutter', 'mobile']);
  await cmd.send_object(['SADD', 'tag:artikel_2', 'dart', 'backend', 'server']);

  // SMEMBERS — semua anggota
  final tags = await cmd.send_object(['SMEMBERS', 'tag:artikel_1']);
  print('Tags: $tags');

  // SISMEMBER — cek keanggotaan
  final adaDart = await cmd.send_object(['SISMEMBER', 'tag:artikel_1', 'dart']);
  print('Ada tag dart: ${adaDart == 1}');

  // Operasi himpunan
  final irisan = await cmd.send_object(['SINTER', 'tag:artikel_1', 'tag:artikel_2']);
  print('Tag yang sama: $irisan'); // ['dart']

  final gabungan = await cmd.send_object(['SUNION', 'tag:artikel_1', 'tag:artikel_2']);
  print('Semua tag: $gabungan');

  // Jumlah anggota
  final count = await cmd.send_object(['SCARD', 'tag:artikel_1']);
}

Sorted Set — Leaderboard dan Ranking #

Future<void> contohSortedSet(Command cmd) async {
  // ZADD — tambah anggota dengan skor
  await cmd.send_object([
    'ZADD', 'skor:game',
    '1500', 'player_A',
    '2200', 'player_B',
    '1800', 'player_C',
    '3000', 'player_D',
  ]);

  // ZRANGE — urutkan dari skor terendah ke tertinggi (dengan skor)
  final ranking = await cmd.send_object([
    'ZRANGE', 'skor:game', '0', '-1', 'WITHSCORES', 'REV'
  ]); // REV = urutkan dari tertinggi
  print('Leaderboard: $ranking');

  // ZRANK — posisi (rank) dari anggota (0-based)
  final rank = await cmd.send_object(['ZREVRANK', 'skor:game', 'player_B']);
  print('Rank player_B: ${(rank as int) + 1}'); // +1 untuk 1-based

  // ZSCORE — dapatkan skor anggota
  final skor = await cmd.send_object(['ZSCORE', 'skor:game', 'player_B']);
  print('Skor player_B: $skor');

  // ZINCRBY — tambah skor
  await cmd.send_object(['ZINCRBY', 'skor:game', '500', 'player_A']);

  // ZRANGEBYSCORE — filter berdasarkan rentang skor
  final skorTinggi = await cmd.send_object([
    'ZRANGEBYSCORE', 'skor:game', '2000', '+inf', 'WITHSCORES'
  ]);
}

Pola Caching yang Umum #

Cache-Aside (Lazy Loading) #

import 'package:redis/redis.dart';
import 'dart:convert';

// Paling umum — aplikasi mengelola cache sendiri
Future<Map<String, dynamic>?> ambilProduk(
  Command redis,
  String id,
  Future<Map<String, dynamic>?> Function(String) dariDatabase,
) async {
  final kunciCache = 'produk:$id';

  // 1. Cek cache
  final cached = await redis.get(kunciCache);
  if (cached != null) {
    print('Cache HIT: $kunciCache');
    return jsonDecode(cached as String) as Map<String, dynamic>;
  }

  // 2. Cache MISS → ambil dari database
  print('Cache MISS: $kunciCache');
  final produk = await dariDatabase(id);
  if (produk == null) return null;

  // 3. Simpan ke cache dengan TTL 1 jam
  await redis.send_object([
    'SET', kunciCache, jsonEncode(produk),
    'EX', '3600',
  ]);

  return produk;
}

// Invalidasi cache saat data berubah
Future<void> updateProduk(
  Command redis,
  String id,
  Map<String, dynamic> dataBaru,
  Future<void> Function(String, Map<String, dynamic>) updateDatabase,
) async {
  // Update database terlebih dahulu
  await updateDatabase(id, dataBaru);

  // Hapus cache agar data lama tidak dipakai
  await redis.send_object(['DEL', 'produk:$id']);
  print('Cache dihapus untuk produk:$id');
}

Session Store #

Future<void> simpanSesi(Command redis, String idSesi, Map<String, dynamic> data) async {
  await redis.send_object([
    'SET',
    'sesi:$idSesi',
    jsonEncode(data),
    'EX', '86400',  // TTL 24 jam
  ]);
}

Future<Map<String, dynamic>?> ambilSesi(Command redis, String idSesi) async {
  final data = await redis.get('sesi:$idSesi');
  if (data == null) return null;

  // Perpanjang TTL setiap kali sesi diakses
  await redis.send_object(['EXPIRE', 'sesi:$idSesi', '86400']);
  return jsonDecode(data as String) as Map<String, dynamic>;
}

Future<void> hapusSesi(Command redis, String idSesi) async {
  await redis.send_object(['DEL', 'sesi:$idSesi']);
}

Rate Limiter #

// Rate limiter menggunakan INCR + EXPIRE
Future<bool> cekRateLimit(
  Command redis,
  String idPengguna, {
  int maksRequest = 100,
  int perDetik = 60,
}) async {
  final kunci = 'rate:$idPengguna:${DateTime.now().minute}';

  // Increment dan set TTL atomic
  final pipeline = redis.multi();
  pipeline.send_object(['INCR', kunci]);
  pipeline.send_object(['EXPIRE', kunci, perDetik.toString()]);
  final hasil = await pipeline.exec();

  final jumlahRequest = hasil[0] as int;

  if (jumlahRequest > maksRequest) {
    print('Rate limit exceeded untuk $idPengguna: $jumlahRequest/$maksRequest');
    return false; // Ditolak
  }

  return true; // Diizinkan
}

// Penggunaan di handler
Future<Response> handler(Request req, String idPengguna) async {
  if (!await cekRateLimit(redisCmd, idPengguna)) {
    return Response(429, body: 'Too Many Requests');
  }
  // Lanjutkan proses request
  return Response.ok('OK');
}

Distributed Lock #

// Distributed lock menggunakan SET NX EX
Future<bool> ambilLock(
  Command redis,
  String namaLock,
  String lockId, {
  int ttlDetik = 30,
}) async {
  // SET NX = set hanya jika belum ada (atomic)
  final hasil = await redis.send_object([
    'SET', 'lock:$namaLock', lockId,
    'NX', 'EX', ttlDetik.toString(),
  ]);
  return hasil == 'OK';
}

Future<void> lepasLock(Command redis, String namaLock, String lockId) async {
  // Cek apakah lock milik kita sebelum melepas (atomic dengan Lua script)
  final script = '''
    if redis.call("GET", KEYS[1]) == ARGV[1] then
      return redis.call("DEL", KEYS[1])
    else
      return 0
    end
  ''';
  await redis.send_object(['EVAL', script, '1', 'lock:$namaLock', lockId]);
}

// Penggunaan distributed lock
Future<void> operasiKritis(Command redis) async {
  final lockId = '${DateTime.now().microsecondsSinceEpoch}';
  final lockDapat = await ambilLock(redis, 'generate-laporan', lockId);

  if (!lockDapat) {
    print('Lock tidak tersedia — proses lain sedang berjalan');
    return;
  }

  try {
    print('Lock diperoleh, menjalankan operasi kritis...');
    await generateLaporan();
  } finally {
    await lepasLock(redis, 'generate-laporan', lockId);
    print('Lock dilepas');
  }
}

Pub/Sub di Redis #

Redis mendukung pub/sub untuk komunikasi real-time antar proses:

import 'package:redis/redis.dart';

// Publisher
Future<void> publish(Command cmd, String channel, String pesan) async {
  final penerima = await cmd.send_object(['PUBLISH', channel, pesan]);
  print('Pesan terkirim ke $penerima penerima di channel $channel');
}

// Subscriber — butuh koneksi terpisah
Future<void> subscribe(RedisConnection conn, List<String> channels) async {
  final cmd = await conn.connect('localhost', 6379);
  final subscription = await cmd.subscribe(channels.join(' '));

  subscription.listen((message) {
    if (message[0] == 'message') {
      final channel = message[1] as String;
      final pesan = message[2] as String;
      print('[$channel] $pesan');
    }
  });
}

Anti-Pattern Redis #

Menyimpan Data yang Terlalu Besar #

// ANTI-PATTERN: simpan seluruh dataset besar ke Redis
final semuaProduk = await ambilSemua10RibuProduk();
await cmd.set('produk:semua', jsonEncode(semuaProduk)); // ✗ bisa ratusan MB di RAM!

// BENAR: cache hanya yang sering diakses, gunakan pagination
await cmd.send_object([
  'SET', 'produk:halaman:1',
  jsonEncode(semuaProduk.take(20).toList()),
  'EX', '300',
]);

Tidak Mengatur TTL #

// ANTI-PATTERN: simpan data tanpa TTL — Redis akhirnya penuh
await cmd.set('laporan:2024-01', jsonEncode(laporan)); // ✗ tidak akan pernah expired!

// BENAR: selalu atur TTL yang masuk akal
await cmd.send_object([
  'SET', 'laporan:2024-01', jsonEncode(laporan),
  'EX', '86400',  // expire setelah 1 hari
]);

// Atau atur TTL eksplisit untuk kunci yang sudah ada
await cmd.send_object(['EXPIRE', 'kunci_lama', '3600']);

Gunakan KEYS di Production #

// ANTI-PATTERN: KEYS melakukan full scan — memblokir Redis!
final semuaKunci = await cmd.send_object(['KEYS', 'user:*']); // ✗ JANGAN di production

// BENAR: gunakan SCAN untuk iterasi bertahap (non-blocking)
String cursor = '0';
final hasilKunci = <String>[];
do {
  final hasil = await cmd.send_object(['SCAN', cursor, 'MATCH', 'user:*', 'COUNT', '100']);
  cursor = (hasil as List)[0] as String;
  hasilKunci.addAll(((hasil)[1] as List).cast<String>());
} while (cursor != '0');
print('Ditemukan ${hasilKunci.length} kunci');

Ringkasan #

  • Tipe data Redis dipilih berdasarkan use case: String untuk cache sederhana dan counter, Hash untuk objek/struct, List untuk queue, Set untuk koleksi unik, Sorted Set untuk ranking/leaderboard.
  • Selalu atur TTL untuk semua data di Redis — tanpa TTL, data akan terus menumpuk hingga Redis kehabisan memori. Gunakan EX saat SET atau EXPIRE untuk kunci yang sudah ada.
  • Cache-Aside (Lazy Loading) adalah pola caching paling umum — cek cache dulu, jika miss ambil dari database dan simpan ke cache dengan TTL.
  • Invalidasi cache dengan DEL saat data berubah di database — lebih aman dari update cache yang bisa menyebabkan race condition.
  • SET NX EX untuk distributed lock yang atomic — NX memastikan hanya satu yang berhasil, EX memastikan lock otomatis dilepas jika proses crash.
  • Rate limiter dengan INCR + EXPIRE — increment counter per window waktu, tolak jika melewati batas. Gunakan Lua script untuk atomic check-and-increment.
  • Pub/Sub Redis untuk real-time messaging ringan — cocok untuk notifikasi dan event sederhana antar proses, bukan untuk event streaming yang butuh persistensi (gunakan Kafka/Pub Sub untuk itu).
  • Jangan gunakan KEYS * di production — memblokir Redis hingga scan selesai. Gunakan SCAN dengan cursor untuk iterasi bertahap yang tidak memblokir.
  • Connection pool penting untuk server — satu koneksi Redis bisa menjadi bottleneck jika banyak concurrent request. Buat beberapa koneksi dan rotasi penggunaannya.
  • Gunakan pipeline/multi untuk mengirim beberapa perintah sekaligus — mengurangi round-trip network secara signifikan untuk operasi batch.

← Sebelumnya: Google Pub/Sub   Berikutnya: Memcached →

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