Redis #
Redis (Remote Dictionary Server) is an in-memory data structure store used as a cache, database, message broker, and session store. Redis’s main advantage is its incredible speed — all operations run in sub-milliseconds because data is stored in RAM. Redis isn’t just a simple key-value store: it supports various data types (String, Hash, List, Set, Sorted Set, Stream), each with specific use cases. The redis package provides a Dart client for communicating with Redis via the RESP (Redis Serialization Protocol) protocol.
Why Redis? #
flowchart LR
App["Dart App"] -->|request| Cache{"Is the data\nalready in Redis?"}
Cache -->|Cache HIT\n~0.1ms| App
Cache -->|Cache MISS| DB["Database\n~100ms"]
DB -->|save to Redis| Cache
DB -->|return data| AppRedis is used for various needs:
CACHE — store expensive database query results
SESSION — store user session data with a TTL
RATE LIMITER — limit requests per user per time window
LOCK — distributed locks for operations that must not run in parallel
PUB/SUB — real-time messaging between services
LEADERBOARD — sorted sets for score rankings
QUEUE — simple task queues with lists
Package Setup #
dart pub add redis
# pubspec.yaml
dependencies:
redis: ^4.1.0
Connecting to Redis #
import 'package:redis/redis.dart';
Future<void> main() async {
// Simple connection
final conn = RedisConnection();
final command = await conn.connect('localhost', 6379);
// Authentication if Redis is configured with a password
await command.send_object(['AUTH', 'password_redis']);
// Select a database (default: 0, Redis supports 0-15)
await command.send_object(['SELECT', '0']);
print('Connected to Redis');
// Close the connection
await conn.close();
}
Connection Pool for Servers #
import 'package:redis/redis.dart';
// Redis connection pool — manages multiple connections
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 initialized with $_ukuran connections');
}
static Future<T> jalankan<T>(Future<T> Function(Command cmd) operasi) async {
// Find an available connection
for (int i = 0; i < _pool.length; i++) {
if (_tersedia[i]) {
_tersedia[i] = false;
try {
return await operasi(_pool[i]);
} finally {
_tersedia[i] = true;
}
}
}
// If all are busy, wait and try again
await Future.delayed(Duration(milliseconds: 10));
return jalankan(operasi);
}
}
// Simpler usage: use the redis_pool package
// or ReidsServer (built into some versions)
Redis Data Types #
String — The Most Basic Type #
import 'package:redis/redis.dart';
Future<void> contohString(Command cmd) async {
// SET and GET
await cmd.set('kunci', 'nilai');
final nilai = await cmd.get('kunci');
print(nilai); // 'nilai'
// SET with a TTL (expire in seconds)
await cmd.send_object(['SET', 'sesi:USR001', 'data_sesi', 'EX', '3600']);
// or: SETEX
await cmd.send_object(['SETEX', 'sesi:USR002', '3600', 'data_sesi_2']);
// SET only if it doesn't exist (NX = Not eXists)
final berhasil = await cmd.send_object(['SET', 'kunci_baru', 'nilai', 'NX']);
print(berhasil); // 'OK' if successful, null if it already exists
// INCREMENT — atomic operation for counters
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 and SET at once
final lama = await cmd.send_object(['GETSET', 'kunci', 'nilai_baru']);
print('Old value: $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 remaining: $ttl seconds');
// Remove the TTL (make persistent)
await cmd.send_object(['PERSIST', 'sesi:USR001']);
// Delete keys
await cmd.send_object(['DEL', 'kunci']);
await cmd.send_object(['DEL', 'kunci_a', 'kunci_b', 'kunci_c']); // batch delete
}
Hash — a Map for Objects #
Future<void> contohHash(Command cmd) async {
// Set several fields at once
await cmd.send_object([
'HSET', 'user:USR001',
'nama', 'Budi Santoso',
'email', '[email protected]',
'level', 'premium',
'loginTerakhir', DateTime.now().toIso8601String(),
]);
// Get one field
final nama = await cmd.send_object(['HGET', 'user:USR001', 'nama']);
print('Nama: $nama');
// Get all fields and values
final semuaField = await cmd.send_object(['HGETALL', 'user:USR001']);
// The result is a flat list: ['nama', 'Budi', 'email', 'budi@...', ...]
// Convert to a 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 several specific fields
final fields = await cmd.send_object(['HMGET', 'user:USR001', 'nama', 'email']);
// Update one field
await cmd.send_object(['HSET', 'user:USR001', 'level', 'vip']);
// Increment a numeric field
await cmd.send_object(['HINCRBY', 'user:USR001', 'loginCount', '1']);
// Check whether a field exists
final ada = await cmd.send_object(['HEXISTS', 'user:USR001', 'nama']);
print('Field nama exists: ${ada == 1}');
// Delete a field
await cmd.send_object(['HDEL', 'user:USR001', 'loginTerakhir']);
// Field count
final jumlah = await cmd.send_object(['HLEN', 'user:USR001']);
}
List — Queues and Stacks #
Future<void> contohList(Command cmd) async {
// LPUSH — push to the left (head), RPUSH — push to the right (tail)
await cmd.send_object(['RPUSH', 'antrian:email', '[email protected]', '[email protected]']);
await cmd.send_object(['LPUSH', 'antrian:email', '[email protected]']); // add to the front
// LRANGE — get elements in a range (0 = first, -1 = last)
final semua = await cmd.send_object(['LRANGE', 'antrian:email', '0', '-1']);
print(semua); // ['[email protected]', '[email protected]', '[email protected]']
// LPOP / RPOP — take and remove from the left/right (like dequeue)
final pertama = await cmd.send_object(['LPOP', 'antrian:email']);
print('Processed: $pertama');
// BLPOP — blocking pop (wait until an element arrives, timeout 0 = wait forever)
// Useful for task queues waiting for work
final tugas = await cmd.send_object(['BLPOP', 'antrian:tugas', '5']); // 5-second timeout
// List length
final panjang = await cmd.send_object(['LLEN', 'antrian:email']);
}
Set — Unique Collections #
Future<void> contohSet(Command cmd) async {
// SADD — add members
await cmd.send_object(['SADD', 'tag:artikel_1', 'dart', 'flutter', 'mobile']);
await cmd.send_object(['SADD', 'tag:artikel_2', 'dart', 'backend', 'server']);
// SMEMBERS — all members
final tags = await cmd.send_object(['SMEMBERS', 'tag:artikel_1']);
print('Tags: $tags');
// SISMEMBER — membership check
final adaDart = await cmd.send_object(['SISMEMBER', 'tag:artikel_1', 'dart']);
print('Has the dart tag: ${adaDart == 1}');
// Set operations
final irisan = await cmd.send_object(['SINTER', 'tag:artikel_1', 'tag:artikel_2']);
print('Shared tags: $irisan'); // ['dart']
final gabungan = await cmd.send_object(['SUNION', 'tag:artikel_1', 'tag:artikel_2']);
print('All tags: $gabungan');
// Member count
final count = await cmd.send_object(['SCARD', 'tag:artikel_1']);
}
Sorted Sets — Leaderboards and Rankings #
Future<void> contohSortedSet(Command cmd) async {
// ZADD — add members with scores
await cmd.send_object([
'ZADD', 'skor:game',
'1500', 'player_A',
'2200', 'player_B',
'1800', 'player_C',
'3000', 'player_D',
]);
// ZRANGE — sort from lowest to highest score (with scores)
final ranking = await cmd.send_object([
'ZRANGE', 'skor:game', '0', '-1', 'WITHSCORES', 'REV'
]); // REV = sort from highest
print('Leaderboard: $ranking');
// ZRANK — the position (rank) of a member (0-based)
final rank = await cmd.send_object(['ZREVRANK', 'skor:game', 'player_B']);
print('Rank player_B: ${(rank as int) + 1}'); // +1 for 1-based
// ZSCORE — get a member's score
final skor = await cmd.send_object(['ZSCORE', 'skor:game', 'player_B']);
print('Score player_B: $skor');
// ZINCRBY — add to a score
await cmd.send_object(['ZINCRBY', 'skor:game', '500', 'player_A']);
// ZRANGEBYSCORE — filter by score range
final skorTinggi = await cmd.send_object([
'ZRANGEBYSCORE', 'skor:game', '2000', '+inf', 'WITHSCORES'
]);
}
Common Caching Patterns #
Cache-Aside (Lazy Loading) #
import 'package:redis/redis.dart';
import 'dart:convert';
// The most common pattern — the app manages the cache itself
Future<Map<String, dynamic>?> ambilProduk(
Command redis,
String id,
Future<Map<String, dynamic>?> Function(String) dariDatabase,
) async {
final kunciCache = 'produk:$id';
// 1. Check the 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 → fetch from the database
print('Cache MISS: $kunciCache');
final produk = await dariDatabase(id);
if (produk == null) return null;
// 3. Save to the cache with a 1-hour TTL
await redis.send_object([
'SET', kunciCache, jsonEncode(produk),
'EX', '3600',
]);
return produk;
}
// Cache invalidation when data changes
Future<void> updateProduk(
Command redis,
String id,
Map<String, dynamic> dataBaru,
Future<void> Function(String, Map<String, dynamic>) updateDatabase,
) async {
// Update the database first
await updateDatabase(id, dataBaru);
// Delete the cache so stale data isn't used
await redis.send_object(['DEL', 'produk:$id']);
print('Cache deleted for 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', // 24-hour TTL
]);
}
Future<Map<String, dynamic>?> ambilSesi(Command redis, String idSesi) async {
final data = await redis.get('sesi:$idSesi');
if (data == null) return null;
// Extend the TTL every time the session is accessed
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 using INCR + EXPIRE
Future<bool> cekRateLimit(
Command redis,
String idPengguna, {
int maksRequest = 100,
int perDetik = 60,
}) async {
final kunci = 'rate:$idPengguna:${DateTime.now().minute}';
// Increment and set the TTL atomically
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 for $idPengguna: $jumlahRequest/$maksRequest');
return false; // Rejected
}
return true; // Allowed
}
// Usage in a handler
Future<Response> handler(Request req, String idPengguna) async {
if (!await cekRateLimit(redisCmd, idPengguna)) {
return Response(429, body: 'Too Many Requests');
}
// Continue processing the request
return Response.ok('OK');
}
Distributed Locks #
// Distributed lock using SET NX EX
Future<bool> ambilLock(
Command redis,
String namaLock,
String lockId, {
int ttlDetik = 30,
}) async {
// SET NX = set only if it doesn't exist (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 {
// Check that the lock is ours before releasing (atomic with a 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]);
}
// Using a 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 unavailable — another process is running');
return;
}
try {
print('Lock acquired, running the critical operation...');
await generateLaporan();
} finally {
await lepasLock(redis, 'generate-laporan', lockId);
print('Lock released');
}
}
Pub/Sub in Redis #
Redis supports pub/sub for real-time inter-process communication:
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('Message sent to $penerima receivers on channel $channel');
}
// Subscriber — needs a separate connection
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');
}
});
}
Redis Anti-Patterns #
Storing Data That’s Too Large #
// ANTI-PATTERN: storing an entire large dataset in Redis
final semuaProduk = await ambilSemua10RibuProduk();
await cmd.set('produk:semua', jsonEncode(semuaProduk)); // ✗ can be hundreds of MB in RAM!
// CORRECT: only cache frequently accessed data, use pagination
await cmd.send_object([
'SET', 'produk:halaman:1',
jsonEncode(semuaProduk.take(20).toList()),
'EX', '300',
]);
Not Setting TTLs #
// ANTI-PATTERN: storing data without a TTL — Redis eventually fills up
await cmd.set('laporan:2024-01', jsonEncode(laporan)); // ✗ will never expire!
// CORRECT: always set a reasonable TTL
await cmd.send_object([
'SET', 'laporan:2024-01', jsonEncode(laporan),
'EX', '86400', // expire after 1 day
]);
// Or set an explicit TTL for existing keys
await cmd.send_object(['EXPIRE', 'kunci_lama', '3600']);
Using KEYS in Production #
// ANTI-PATTERN: KEYS does a full scan — blocking Redis!
final semuaKunci = await cmd.send_object(['KEYS', 'user:*']); // ✗ DON'T in production
// CORRECT: use SCAN for incremental iteration (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('Found ${hasilKunci.length} keys');
Summary #
- Redis data types are chosen by use case:
Stringfor simple caches and counters,Hashfor objects/structs,Listfor queues,Setfor unique collections,Sorted Setfor rankings/leaderboards.- Always set TTLs for all data in Redis — without TTLs, data keeps accumulating until Redis runs out of memory. Use
EXwithSETorEXPIREfor existing keys.- Cache-Aside (Lazy Loading) is the most common caching pattern — check the cache first, on a miss fetch from the database and store it in the cache with a TTL.
- Cache invalidation with
DELwhen data changes in the database — safer than updating the cache, which can cause race conditions.SET NX EXfor atomic distributed locks — NX ensures only one succeeds, EX ensures the lock auto-releases if a process crashes.- Rate limiters with
INCR+EXPIRE— increment a counter per time window, reject when the limit is exceeded. Use a Lua script for atomic check-and-increment.- Redis Pub/Sub for lightweight real-time messaging — good for notifications and simple cross-process events, not for event streaming that needs persistence (use Kafka/Pub Sub for that).
- Don’t use
KEYS *in production — it blocks Redis until the scan finishes. UseSCANwith a cursor for non-blocking incremental iteration.- Connection pools are important for servers — a single Redis connection can become a bottleneck with many concurrent requests. Create several connections and rotate their usage.
- Use pipelines/multi to send several commands at once — significantly reduces network round-trips for batch operations.