Practical Guide to In-Memory Caching with Redis

Why caching matters

Every request that reaches the database consumes CPU, memory, and disk I/O. When traffic spikes, the database becomes the first bottleneck. A cache layer—placed between the application and the datastore—absorbs the majority of read requests, reduces latency, and acts as a circuit-breaker when the primary store is unavailable.

Typical caching topology

Client → Load-balancer → Application → Cache → Database
If the cache misses, the application fetches the value from the database, stores it in the cache, and returns it to the client. On subsequent requests the value is served directly from RAM.

Choosing a cache engine

Memcached

  • Simple key/value map in RAM.
  • No persistence, replication, or sharding.
  • Ideal for small, transient objects.

Redis

  • Rich data structures: strings, hashes, lists, sets, sorted sets, streams, bitmaps, HyperLogLog.
  • Disk snapshots (RDB) and append-only logs (AOF) for durability.
  • Master/replica replication and Redis Cluster for horizontal scaling.

Redis performance drivers

  • Single-threaded event loop eliminates lock contention.
  • Epoll/kqueue-based I/O multiplexing keeps one thread busy with thousands of sockets.
  • All data lives in RAM; optional disk writes happen asynchronously.
  • Micro-optimised data strucutres (e.g., compressed lists for small hashes/zsets).

Core data types and patterns

Type Use case example
String Counter, session token, JSON blob
Hash User profile with multiple feilds
List Activity feed, message queue
Set Unique tags, IP whitelist
Sorted Set Leaderboard, rate-limiter sliding window

Working with large keyspaces

Finding keys by prefix

SCAN 0 MATCH user:* COUNT 1000

Unlike KEYS, SCAN returns a cursor and a partial result set, avoiding server stalls.

Distributed locking recipe

String lockKey = "order:lock:" + orderId;
String uuid   = UUID.randomUUID().toString();
boolean ok = redis.set(lockKey, uuid, "NX", "EX", 5); // 5-second TTL
if (ok) {
    try {
        // critical section
    } finally {
        // Lua script to release only if value matches
        String lua =
          "if redis.call('get', KEYS[1]) == ARGV[1] then " +
          "  return redis.call('del', KEYS[1]) " +
          "else return 0 end";
        redis.eval(lua, 1, lockKey, uuid);
    }
}

Preventing cache avalanche

Stagger expiration times by adding random jitter:

int ttl = 3600 + ThreadLocalRandom.current().nextInt(600); // 3600–4200 s

Lightweight message queue

Producer:

LPUSH notifications '{"id":123,"msg":"hello"}'

Consumer:

BRPOP notifications 0   // blocks until item arrives

For pub/sub fan-out:

PUBLISH alerts '{"severity":"high"}'

Persistence options

RDB snapshot

  • Binary point-in-time image.
  • Triggered by time (save 900 1) or manual BGSAVE.
  • Fast restart; risk of minutes of data loss.

AOF log

  • Every write command is appended.
  • Three fsync policies: no, every second, always.
  • Smaller data loss window; larger file, slower restart.
  • Can be rewritten in background to shrink size.

Batching with pipelines

Pipeline p = redis.pipelined();
for (String k : keys) {
    p.get(k);
}
List<Object> results = p.syncAndReturnAll();

Sends N commands in one round-trip, cutting network overhead.

Replication and sharding

  • Asynchronous replica streams commands from master’s backlog.
  • Sentinel monitors health and promotes a new master on failure.
  • Redis Cluster uses 16 384 hash slots; CRC16(key) mod 16384 determines the slot and owning node.
  • Clients can cache slot-to-node mapping to avoid extra redirects.

Tags: Redis Memcached Caching In-Memory Database Distributed Locking

Posted on Tue, 26 May 2026 18:49:24 +0000 by helpwanted