Redis
Redis is an in-memory data store used by Java services for sub-millisecond caching, session storage, rate limiting, and distributed locks.
Introduction
Redis is an in-memory data store used by Java services for sub-millisecond caching, session storage, rate limiting, and distributed locks. The cache-aside pattern — read cache first, on miss load DB and populate — is the default integration with Spring Cache + Lettuce/Jedis.
This lesson covers TTL strategy, distributed cache consistency across pods, and eviction policies (LRU, LFU, volatile-lru) when memory fills. Redis is not your source of truth — it's an acceleration layer with explicit expiration and invalidation contracts.
Business problem
Database overload without caching:
- Hot keys: Product detail page — 10K RPS hits Postgres for same SKU — DB CPU 100%.
- Session state: Sticky sessions tie users to pods — scale-out impossible without shared session store.
- Rate limiting: Payment API abused — need distributed counter across 20 gateway pods.
- Latency SLO: p99 catalog read 200ms from Postgres — Redis cache hits < 2ms.
- Thundering herd: Cache expiry — 1000 threads simultaneously query DB for same key.
Why this topic exists
Redis exists because RAM latency (microseconds) beats disk/network DB round-trips (milliseconds) by 100×:
- Cache-aside: Application manages cache — read Redis, miss → DB → SET with TTL.
- TTL: Time-to-live — automatic expiry prevents stale data forever; tune per data freshness requirement.
- Distributed cache: All JVM pods share one Redis cluster — consistent view of sessions and counters.
- Eviction: When maxmemory reached — Redis evicts keys per policy; wrong policy drops hot data.
- Not a DB: Persistence optional (RDB/AOF) — design for cache loss on restart.
Core concepts
Redis caching concepts for Java:
- Cache-aside (lazy loading):
get(key)→ miss → load from DB →set(key, value, TTL). - Write-through: Write DB and cache synchronously — consistent but slower writes.
- Write-behind: Write cache first, async flush to DB — fast but data loss risk.
- TTL:
SET key value EX 300— expire in 300 seconds; jitter TTL to prevent simultaneous expiry. - Eviction policies: allkeys-lru, volatile-lru, allkeys-lfu — choose based on TTL usage.
- Spring Cache:
@Cacheable,@CacheEvictwith Redis backend — declarative cache-aside.
Internal architecture
Cache-aside with Java Spring Boot:
Request ──▶ Spring Controller│▼@Cacheable("products")ProductService.getById(sku)│┌──────────┴──────────┐▼ ▼Redis GET product:SKU123 Cache MISS│ │HIT │ ▼│ Postgres SELECT│ ││ ▼│ Redis SET EX 600▼ │Return Product ◀───────────┘Redis Cluster (distributed):Master 1 (slots 0-5460) ── replicaMaster 2 (slots 5461-10922) ── replicaMaster 3 (slots 10923-16383) ── replica
Cache-aside flow, TTL lifecycle, and eviction:
Code walkthrough
Spring Cache + Redis cache-aside with manual fallback:
- @Cacheable: Spring intercepts — Redis lookup before method body on hit.
- @CacheEvict on update: Prevents serving stale product after price change.
- Lock on miss: SET NX prevents thundering herd — one thread loads DB.
- Jitter on TTL: Random ±60s spread — avoids simultaneous mass expiry.
@Configuration@EnableCachingpublic class CacheConfig {@Beanpublic RedisCacheManager cacheManager(RedisConnectionFactory factory) {RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(10)).serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));return RedisCacheManager.builder(factory).cacheDefaults(config).build();}}@Servicepublic class ProductService {private final ProductRepository repo;@Cacheable(value = "products", key = "#sku", unless = "#result == null")public Product findBySku(String sku) {return repo.findById(sku).orElse(null); // only on cache miss}@CacheEvict(value = "products", key = "#product.sku")public Product update(Product product) {return repo.save(product); // invalidate stale cache entry}}// Manual cache-aside with Lettuce (fine-grained control)public Optional<Product> getWithLock(String sku) {String key = "product:" + sku;String cached = redis.get(key);if (cached != null) return Optional.of(deserialize(cached));if (redis.set(key + ":lock", "1", SetArgs.Builder.nx().ex(5))) {try {Product p = repo.findById(sku).orElseThrow();redis.setex(key, 600 + randomJitter(), serialize(p));return Optional.of(p);} finally {redis.del(key + ":lock");}}Thread.sleep(50); // brief wait, retry getreturn getWithLock(sku);}
Production example
Redis Cluster + Spring Boot production config:
- Redis Cluster: Hash slots across masters — horizontal scale beyond single node RAM.
- allkeys-lfu: Keeps frequently accessed catalog keys under memory pressure.
- Connection pool: Size pool × pod count < Redis max connections — avoid connection storm.
- INCR + EXPIRE: Atomic rate limit pattern — no separate GET/SET race.
# application.ymlspring:data:redis:cluster:nodes:- redis-0:6379- redis-1:6379- redis-2:6379lettuce:pool:max-active: 32max-idle: 16cache:type: redisredis:time-to-live: 600000 # 10 min default# redis.conf (ops)maxmemory 4gbmaxmemory-policy allkeys-lfu # hot catalog SKUs survive pressurenotify-keyspace-events Ex # optional: expiry listeners// Rate limiter — distributed counterpublic boolean allowRequest(String clientId) {String key = "ratelimit:" + clientId + ":" + minuteBucket();Long count = redis.incr(key);if (count == 1) redis.expire(key, 60);return count <= 100;}
Enterprise case study
Twitter/X — Redis at scale: Twitter used Redis for timelines, caching, and rate limiting serving hundreds of millions of users. Java/Spring services at many enterprises mirror this pattern for session and catalog cache. Lesson: monitor hit ratio — below 90% on hot paths means TTL too aggressive or invalidation gaps.
- Before: Postgres read replicas maxed — p99 latency 400ms on product pages.
- After: Redis cache-aside — 95% hit rate, p99 15ms, DB load dropped 80%.
- Incident: Missing @CacheEvict on bulk price update — stale prices for 10 minutes until TTL.
- Fix: Event-driven cache invalidation via Redis Pub/Sub on price change Kafka event.
Performance considerations
Redis performance for Java services:
- Pipeline/batch: Lettuce async — batch multiple GETs for product page fan-out.
- Serialization: JSON readable but bulky — consider Kryo or String for simple values.
- Key design: Short keys, consistent prefix —
p:SKU123not giant JSON keys. - Avoid big values: >512KB values block Redis — split or compress.
- Local L1 + Redis L2: Caffeine in-JVM for hottest keys — reduces network to Redis.
Security considerations
Redis security:
- AUTH + TLS: Never expose Redis without password on public network.
- No secrets in cache: Don't cache raw JWT or PCI data — cache opaque session ID only.
- ACLs (Redis 6+): App user READ/WRITE on
product:*only. - Cache poisoning: Validate data before SET — don't cache user-controlled unvalidated input.
Scalability considerations
Scaling Redis with Java fleet:
- Redis Cluster: Shard by hash slot — scale memory horizontally.
- Read replicas: Scale reads for session lookup — eventual consistency acceptable.
- Connection limits: 10K pods × 32 connections = plan Redis maxclients.
- Multi-region: Active-active cache is hard — prefer regional Redis with sticky routing.
Production challenges
Real Redis caching challenges:
- Cache stampede: Mass expiry → DB overload — lock on miss, jitter TTL, pre-warm.
- Stale reads: TTL-only without invalidation — user sees old data after update.
- Redis outage: Cache-aside should degrade to DB — circuit breaker, don't fail requests.
- Hot keys: Single key maxes one Redis slot — local cache or read replica for viral SKU.
- Serialization bugs: Jackson polymorphic types break cache — version cache format.
Common mistakes
- Caching without TTL — memory fills, eviction drops random hot keys.
- No invalidation on write — stale data until TTL; finance/catalog unacceptable.
- Using Redis as primary database — data loss on flush/restart.
- KEYS * in production — blocks Redis; use SCAN.
- Same TTL for all keys — thundering herd on simultaneous expiry.
Debugging guide
Debug Redis caching issues:
- Hit ratio:
INFO stats— keyspace_hits / (hits + misses). - Key exists:
redis-cli GET product:SKU123andTTL product:SKU123. - Spring Cache debug:
logging.level.org.springframework.cache=TRACE. - Memory:
INFO memory— used_memory, evicted_keys counter rising = pressure.
# Monitor hit rateredis-cli INFO stats | grep keyspace# Slow logredis-cli SLOWLOG GET 10# Check eviction policyredis-cli CONFIG GET maxmemory-policy# Spring — verify cache keyredis-cli KEYS 'products::*' # dev only; use SCAN in prod
Best practices
- Use cache-aside with explicit TTL on every key.
- @CacheEvict or event-driven invalidation on writes.
- Add TTL jitter to prevent thundering herd.
- Monitor hit ratio and evicted_keys.
- Implement cache miss lock for extremely hot keys.
- Choose allkeys-lfu for read-heavy catalog; volatile-lru if mix of TTL and permanent keys.
- Graceful degradation — DB fallback when Redis unavailable.
Anti-patterns
- Cache everything — cache only proven hot paths with measured DB cost.
- Long TTL without invalidation on mutable data — stale reads guaranteed.
- Distributed lock via Redis for every request — overhead; use for herd protection only.
- Storing large object graphs — cache DTOs, not entity graphs with lazy collections.
- Single Redis instance in production — SPOF; use Cluster or Sentinel.
Staff engineer notes
- Staff engineers measure before caching — profile DB, identify top 10 queries, cache those first.
- TTL is a freshness contract — document per cache namespace: catalog 10min, session 24h, rate limit 60s.
- Redis outage must not outage product — cache is optimization layer, not dependency for availability.
- Eviction policy is capacity planning — allkeys-lfu beats allkeys-lru for skewed access patterns (e-commerce).
- Spring @Cacheable is convenient — understand it generates proxy; self-invocation bypasses cache.
Interview questions
Interview preparation
15 questions grouped by difficulty — expand any card for a model answer and follow-up probe.
Beginner
1What is cache-aside pattern?
BeginnerModel answer
- Application checks cache first. On hit, return cached value. On miss, load from database, write to cache with TTL, return value. Application manages cache
- Redis passive store.
Follow-up probe
vs read-through?
2What is Redis TTL?
BeginnerModel answer
Time-to-live in seconds on key.
After expiry Redis deletes key automatically.
SET key value EX 300.
Prevents infinite staleness.
Tune per data change frequency.
Follow-up probe
TTL vs explicit delete?
3What is a distributed cache?
BeginnerModel answer
Shared cache cluster accessed by all application instances.
Redis Cluster or single Redis with replicas.
Enables session sharing, consistent rate limits, cache hit across pods.
Follow-up probe
vs local cache?
4Explain Redis eviction policies.
BeginnerModel answer
When maxmemory reached: noeviction (errors), allkeys-lru (evict least recently used any key), volatile-lru (evict LRU among TTL keys), allkeys-lfu (least frequently used).
Choose based on whether all keys have TTL.
Follow-up probe
LRU vs LFU?
5How integrate Redis with Spring Boot?
BeginnerModel answer
spring-boot-starter-data-redis + spring-boot-starter-cache.
@EnableCaching, RedisCacheManager bean.
@Cacheable on service methods.
@CacheEvict on updates.
Lettuce client default.
Follow-up probe
Lettuce vs Jedis?
Intermediate
6What is cache stampede/thundering herd?
IntermediateModel answer
g.
key expired).
All hit database at once.
Mitigate: lock on miss (SET NX), jitter TTL, pre-warm, stale-while-revalidate.
Follow-up probe
Lock implementation?
7Cache-aside vs write-through?
IntermediateModel answer
Cache-aside: app writes DB, invalidates/updates cache on write.
Write-through: write to cache and DB synchronously on every write.
Write-through consistent but slower writes.
Cache-aside most common in Java.
Follow-up probe
Write-behind?
8How invalidate cache on update?
IntermediateModel answer
@CacheEvict same key on update method.
Or publish invalidation event (Kafka/Redis Pub/Sub) all pods listen.
Or versioned keys (product:v2:sku).
TTL alone insufficient for mutable data.
Follow-up probe
Eventual consistency window?
9Redis Cluster vs Sentinel?
IntermediateModel answer
- Sentinel: HA for single master with failover
- one shard. Cluster: multi-master sharding by hash slots
- horizontal memory scale. Cluster for large cache; Sentinel for moderate size HA.
Follow-up probe
Hash slot?
10How handle Redis failure?
IntermediateModel answer
- Circuit breaker
- skip cache, query DB directly. Accept higher latency. Don't fail user requests. Monitor Redis separately. Multi-AZ Cluster for HA.
Follow-up probe
Retry storm to DB?
Advanced
11Design catalog cache for e-commerce.
AdvancedModel answer
Cache-aside, key product:{sku}, TTL 10min + jitter.
@CacheEvict on admin update.
Caffeine L1 for top 100 SKUs.
Redis Cluster 3 masters.
allkeys-lfu.
Hit ratio target 95%.
Kafka event invalidates on price change.
DB fallback on Redis down.
Follow-up probe
Cache product list?
12Redis for session vs JWT?
AdvancedModel answer
Redis session: server-side state, instant revoke, shared across pods.
JWT: stateless, no Redis needed, harder revoke.
Hybrid: short JWT + Redis blocklist for logout.
PCI apps often prefer server session.
Follow-up probe
Session serialization?
13When NOT to use Redis cache?
AdvancedModel answer
- Strong consistency required on every read. Data larger than memory budget. Write-heavy with low read ratio. Simple app with low traffic
- DB sufficient.
Follow-up probe
Redis vs Memcached?
14Implement distributed rate limiter.
AdvancedModel answer
INCR ratelimit:{clientId}:{minute} with EXPIRE 60 on first incr.
Compare count to limit.
Sliding window: sorted set with timestamp scores.
Token bucket: Lua script atomic.
All pods share Redis counter.
Follow-up probe
Race conditions?
15Cache consistency in microservices?
AdvancedModel answer
Each service owns cache of its data.
On update, emit domain event; consumers evict their cache keys.
Avoid shared cache namespace across services.
TTL as safety net.
Accept brief staleness or use shorter TTL on critical data.
Follow-up probe
Transactional cache?
Hands-on exercise
Lab: Redis caching patterns:
- Run playground — trace cache hit vs miss logic.
- Add updateProduct method comment with @CacheEvict annotation.
- Implement jitter: baseTtl + random(0, 60) in comment pseudocode.
- Explain which eviction policy for mix of session keys (TTL) and catalog keys (TTL).
- Bonus: sketch rate limiter with INCR pattern.
JavaRedis Caching for Java
Press Run to execute. Real javac + JVM via remote API. Falls back to simulator on network failure.Architecture trade-offs
- Cache-aside vs write-through: Cache-aside wins write latency; write-through wins read consistency.
- Redis vs Caffeine local: Redis wins shared state; Caffeine wins zero network latency.
- Long TTL vs frequent invalidation: Long TTL wins DB load; invalidation wins freshness.
- allkeys-lru vs allkeys-lfu: LRU wins general; LFU wins skewed hot-key catalogs.
Summary
Redis accelerates Java services when used with discipline — cache-aside, explicit TTL, write invalidation, and the right eviction policy. Treat Redis as disposable acceleration, not source of truth. Next: pair with Kafka for cache invalidation events across microservices.
Key takeaways
- Cache-aside: app reads Redis, on miss loads DB and SET with TTL.
- Always set TTL — eviction policy handles memory pressure.
- @CacheEvict on writes — TTL alone causes stale data.
- Distributed cache = shared Redis — sessions and rate limits across pods.
- Monitor hit ratio; degrade to DB when Redis unavailable.