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:
Stringuntuk cache sederhana dan counter,Hashuntuk objek/struct,Listuntuk queue,Setuntuk koleksi unik,Sorted Setuntuk ranking/leaderboard.- Selalu atur TTL untuk semua data di Redis — tanpa TTL, data akan terus menumpuk hingga Redis kehabisan memori. Gunakan
EXsaatSETatauEXPIREuntuk 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
DELsaat data berubah di database — lebih aman dari update cache yang bisa menyebabkan race condition.SET NX EXuntuk 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. GunakanSCANdengan 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.