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

    PatternHow It WorksProsCons
    Cache-AsideApp manages cache explicitlySimple, flexibleCache logic in app code
    Read-ThroughCache loads data automatically on missCleaner app codeRequires cache provider support
    Write-ThroughWrites go to cache AND DB synchronouslyCache always consistentHigher write latency
    Write-BehindWrites to cache, async write to DBFast writesRisk of data loss
    Redis Caching Patterns Diagram

    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

    FeatureRedisCaffeineEhcache
    TypeDistributedIn-process (JVM)Both
    Speed~1ms (network)~100ns (memory)Varies
    Shared✅ Yes❌ No (per JVM)Optional
    Best ForMulti-instance appsSingle instance, ultra-fastFlexible 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);}}

    💬 Comments & Discussion