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 manualBGSAVE. - 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.