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| App

Redis 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: String for simple caches and counters, Hash for objects/structs, List for queues, Set for unique collections, Sorted Set for rankings/leaderboards.
  • Always set TTLs for all data in Redis — without TTLs, data keeps accumulating until Redis runs out of memory. Use EX with SET or EXPIRE for 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 DEL when data changes in the database — safer than updating the cache, which can cause race conditions.
  • SET NX EX for 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. Use SCAN with 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.

← Previous: Google Pub/Sub   Next: Memcached →

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