Back to Redis Course
Module 3
⚡ Redis as a Cache
Use Redis correctly (and safely) as a cache layer for your applications.
Cache-Aside Pattern (Lazy Loading)
The most common caching pattern. Application manages cache explicitly.
Cache-Aside Flow
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Client │────────▶│ App │────────▶│ Cache │
└─────────┘ └────┬────┘ └────┬────┘
│ │
(cache miss) │
│ │
▼ │
┌─────────┐ │
│ DB │◀─────────────┘
└─────────┘ (populate cache)Cache-Aside Implementation
@ServicepublicclassUserService{@AutowiredprivateRedisTemplate<String,User> redisTemplate;@AutowiredprivateUserRepository userRepository;publicUsergetUser(Long id){String key ="user:"+ id;// 1. Try cache firstUser user = redisTemplate.opsForValue().get(key);if(user !=null){return user;// Cache HIT}// 2. Cache MISS - fetch from DB
user = userRepository.findById(id).orElse(null);if(user !=null){// 3. Populate cache with TTL
redisTemplate.opsForValue().set(key, user,Duration.ofMinutes(30));}return user;}publicvoidupdateUser(User user){
userRepository.save(user);// 4. Invalidate cache on write
redisTemplate.delete("user:"+ user.getId());}}Read-Through vs Write-Through Cache
| Pattern | How It Works | Pros | Cons |
|---|---|---|---|
| Cache-Aside | App manages cache explicitly | Simple, flexible | Cache logic in app code |
| Read-Through | Cache loads data automatically on miss | Cleaner app code | Requires cache provider support |
| Write-Through | Writes go to cache AND DB synchronously | Cache always consistent | Higher write latency |
| Write-Behind | Writes to cache, async write to DB | Fast writes | Risk of data loss |

TTL Strategies
TTL Patterns
// Fixed TTL - Simple but can cause stampede
redisTemplate.opsForValue().set(key, value,Duration.ofMinutes(30));// Jittered TTL - Add randomness to prevent thundering herdint baseTTL =30*60;// 30 minutesint jitter =newRandom().nextInt(300);// 0-5 minutes random
redisTemplate.opsForValue().set(key, value,Duration.ofSeconds(baseTTL + jitter));// Sliding TTL - Reset on each access (session-like)publicUsergetUser(Long id){String key ="user:"+ id;User user = redisTemplate.opsForValue().get(key);if(user !=null){// Reset TTL on each access
redisTemplate.expire(key,Duration.ofMinutes(30));}return user;}// Early expiration - Refresh before TTL expirespublicUsergetUserWithEarlyRefresh(Long id){String key ="user:"+ id;Long ttl = redisTemplate.getExpire(key);// If less than 5 minutes left, refresh asyncif(ttl !=null&& ttl <300){asyncRefreshCache(id);}return redisTemplate.opsForValue().get(key);}Cache Penetration, Breakdown & Avalanche
🔴 Cache Penetration
Problem: Queries for non-existent data always hit DB (attacker can exploit this)
Cache Penetration Solutions
// Solution 1: Cache null valuesUser user = userRepository.findById(id).orElse(null);if(user ==null){// Cache null/empty value with short TTL
redisTemplate.opsForValue().set(key,"NULL",Duration.ofMinutes(5));}// Solution 2: Bloom Filter (check if key CAN exist)BloomFilter<Long> userIds =BloomFilter.create(...);if(!userIds.mightContain(id)){returnnull;// Definitely doesn't exist, skip DB}🟡 Cache Breakdown (Hot Key Expiry)
Problem: A hot key expires, causing massive DB load
Cache Breakdown Solution
// Solution: Distributed lock for cache refreshpublicUsergetUserWithLock(Long id){String key ="user:"+ id;User user = redisTemplate.opsForValue().get(key);if(user ==null){String lockKey ="lock:"+ key;// Try to get lockBoolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey,"1",Duration.ofSeconds(10));if(Boolean.TRUE.equals(locked)){try{// Double-check after getting lock
user = redisTemplate.opsForValue().get(key);if(user ==null){
user = userRepository.findById(id).orElse(null);
redisTemplate.opsForValue().set(key, user,Duration.ofHours(1));}}finally{
redisTemplate.delete(lockKey);}}else{// Wait and retryThread.sleep(100);returngetUserWithLock(id);}}return user;}🟠 Cache Avalanche
Problem: Many keys expire at the same time, overwhelming DB
Cache Avalanche Solutions
// Solutions:// 1. Jittered TTL (shown above)// 2. Never expire hot data - use background refresh@Scheduled(fixedRate =60000)// Every minutepublicvoidrefreshCriticalCache(){List<String> hotKeys =getHotKeys();for(String key : hotKeys){refreshCacheAsync(key);}}// 3. Circuit breaker for DB@CircuitBreaker(name ="database", fallbackMethod ="getFallback")publicUsergetUserFromDB(Long id){return userRepository.findById(id).orElse(null);}Redis vs Caffeine vs Ehcache
| Feature | Redis | Caffeine | Ehcache |
|---|---|---|---|
| Type | Distributed | In-process (JVM) | Both |
| Speed | ~1ms (network) | ~100ns (memory) | Varies |
| Shared | ✅ Yes | ❌ No (per JVM) | Optional |
| Best For | Multi-instance apps | Single instance, ultra-fast | Flexible needs |
💡 Pro Tip: Two-Level Cache
Use Caffeine as L1 (local) cache + Redis as L2 (distributed) cache. Check Caffeine first → Redis → DB. Best of both worlds!
👉 Hands-on: Cache DB Results with Spring Boot
Spring Cache Example
// application.yml
spring:
cache:
type: redis
redis:
host: localhost
port:6379// Enable caching@SpringBootApplication@EnableCachingpublicclassApplication{}// Service with @Cacheable@ServicepublicclassProductService{@Cacheable(value ="products", key ="#id")publicProductgetProduct(Long id){
log.info("Fetching from DB...");// Only on cache missreturn productRepository.findById(id).orElse(null);}@CacheEvict(value ="products", key ="#product.id")publicProductupdateProduct(Product product){return productRepository.save(product);}@CacheEvict(value ="products", allEntries =true)publicvoidclearAllProducts(){// Clears entire "products" cache}@CachePut(value ="products", key ="#product.id")publicProductcreateProduct(Product product){// Always updates cache after executionreturn productRepository.save(product);}}