VibeKoding / Ensiklopedia ยท Fondasi KuatEnsiklopedia ยท Fondasi Kuat / Principles of Caching: Strategies and PatternsPrinciples of Caching: Strategies and Patterns
VK

Principles of Caching: Strategies and PatternsPrinciples of Caching: Strategies and Patterns

๐Ÿ“š Ensiklopedia ยท Fondasi KuatEnsiklopedia ยท Fondasi Kuat ๐ŸŒ Dual Bahasa (ID / EN) โšก VibeKoding Native

Ensiklopedia VibeKoding: Principles of Caching: Strategies and Patterns.Ensiklopedia VibeKoding: Principles of Caching: Strategies and Patterns.

๐Ÿ’ก Tips Praktis๐Ÿ’ก Pro Tip

Why do some websites load in 50 milliseconds while others take 5 seconds? It's like asking: why does grabbing a book from your backpack take 1 second, while going to the library to find it takes 10 minutes? The answer is โ€” caching. This chapter will take you deep into the core principles, design patterns, and practical techniques of caching, so you can boost your system performance by 100x.Why do some websites load in 50 milliseconds while others take 5 seconds? It's like asking: why does grabbing a book from your backpack take 1 second, while going to the library to find it takes 10 minutes? The answer is โ€” caching. This chapter will take you deep into the core principles, design patterns, and practical techniques of caching, so you can boost your system performance by 100x.

------

1. Motivation for Cachinging1. Motivation for Cachinging

1.1 The Evolution from "Query Every Time" to "Remember Frequently Used Data"1.1 The Evolution from "Query Every Time" to "Remember Frequently Used Data"

In the early days of computing, programmers would query the hard drive or database every time they needed data. It's like flipping through a textbook to look up a formula for every single math problem โ€” accurate, but terribly inefficient. As systems scaled, this "query every time" approach began to expose serious problems: database CPU spiking to 95%, response times ballooning from 100 milliseconds to 8 seconds, and eventually, complete system collapse.In the early days of computing, programmers would query the hard drive or database every time they needed data. It's like flipping through a textbook to look up a formula for every single math problem โ€” accurate, but terribly inefficient. As systems scaled, this "query every time" approach began to expose serious problems: database CPU spiking to 95%, response times ballooning from 100 milliseconds to 8 seconds, and eventually, complete system collapse.

This is like a student who runs from the dorm to the library to look up references for every class, 50 trips a day, eventually collapsing from exhaustion halfway there. The solution is simple: keep a notebook of commonly used formulas in your backpack, and check the backpack first instead of running to the library every time. Caching is the computer system's "formula notebook" โ€” it stores frequently used data in a fast-access location so the system doesn't have to go to the "library" (the database) every time.This is like a student who runs from the dorm to the library to look up references for every class, 50 trips a day, eventually collapsing from exhaustion halfway there. The solution is simple: keep a notebook of commonly used formulas in your backpack, and check the backpack first instead of running to the library every time. Caching is the computer system's "formula notebook" โ€” it stores frequently used data in a fast-access location so the system doesn't have to go to the "library" (the database) every time.

๐ŸŒ Without Cache๐ŸŒ Without Cache

  • Every request hits the databaseEvery request hits the database
  • Database CPU usage at 95%Database CPU usage at 95%
  • Response time 5โ€“8 secondsResponse time 5โ€“8 seconds
  • System prone to crashesSystem prone to crashes

With Cache With Cache

  • 95% of requests return directly95% of requests return directly
  • Database CPU usage < 20%Database CPU usage < 20%
  • Response time 50 millisecondsResponse time 50 milliseconds
  • System runs stablySystem runs stably

This is the core problem caching solves: by storing copies of frequently used data, it reduces access to slow storage (the database), making the system faster and more stable.This is the core problem caching solves: by storing copies of frequently used data, it reduces access to slow storage (the database), making the system faster and more stable.

1.2 Case: Caching Is a Lifesaver1.2 Case: Caching Is a Lifesaver

You might be thinking: "My system is fine right now, why design for caching in advance?" Let me tell you a real story so you understand why caching is not "optional" but "mandatory."You might be thinking: "My system is fine right now, why design for caching in advance?" Let me tell you a real story so you understand why caching is not "optional" but "mandatory."

โš ๏ธ Catatan Keamanan / Peringatanโš ๏ธ Warning / Security Note

Ah-Qiang was a full-stack engineer at a startup building a social app. Early on, with only a few hundred users, the system ran smoothly. Ah-Qiang figured there was no need for caching โ€” just query the database directly. Six months later, the user base grew to 100,000. One day, a celebrity posted on the app, and 100,000 users flooded in instantly. The database buckled: CPU hit 100%, response time went from 100ms to 30 seconds, and the entire app crashed, causing massive user churn. Post-mortem: if there had been a simple caching layer (like Redis) to cache the trending posts, database load would have dropped by at least 95%, and the system would have easily weathered the traffic spike. Ah-Qiang learned a hard lesson: caching is not a nice-to-have; it's a lifeline for high-concurrency systems. Going without caching is like driving without a seatbelt โ€” fine most of the time, but fatal when things go wrong.Ah-Qiang was a full-stack engineer at a startup building a social app. Early on, with only a few hundred users, the system ran smoothly. Ah-Qiang figured there was no need for caching โ€” just query the database directly. Six months later, the user base grew to 100,000. One day, a celebrity posted on the app, and 100,000 users flooded in instantly. The database buckled: CPU hit 100%, response time went from 100ms to 30 seconds, and the entire app crashed, causing massive user churn. Post-mortem: if there had been a simple caching layer (like Redis) to cache the trending posts, database load would have dropped by at least 95%, and the system would have easily weathered the traffic spike. Ah-Qiang learned a hard lesson: caching is not a nice-to-have; it's a lifeline for high-concurrency systems. Going without caching is like driving without a seatbelt โ€” fine most of the time, but fatal when things go wrong.

๐Ÿ“– Konsep Penting๐Ÿ“– Core Concept

The value of caching isn't just "faster" โ€” more importantly, it's "protection." It protects the database from being overwhelmed and keeps the system stable under high traffic. When designing a system, don't wait until something breaks to think about caching โ€” make it a core part of your architecture from day one.The value of caching isn't just "faster" โ€” more importantly, it's "protection." It protects the database from being overwhelmed and keeps the system stable under high traffic. When designing a system, don't wait until something breaks to think about caching โ€” make it a core part of your architecture from day one.

------

2. Core Concepts: Overview of Caching2. Core Concepts: Overview of Caching

๐Ÿ’ก Tips Praktis๐Ÿ’ก Pro Tip

Simply put, a cache is a storage space for data copies. It's like sticking a sticky note on your desk with frequently dialed phone numbers, so you don't have to scroll through your phone's contact list every time. Three key points: 1. Copy: Data in the cache is a copy of the original data (in the database), not the primary data 2. Fast access: Caches are typically in memory, which is 100,000 times faster to read than a hard drive 3. Limited capacity: Cache space is finite โ€” it can only store the most frequently used data So, caching is trading space for time โ€” sacrificing some memory space in exchange for blazing-fast data access.Simply put, a cache is a storage space for data copies. It's like sticking a sticky note on your desk with frequently dialed phone numbers, so you don't have to scroll through your phone's contact list every time. Three key points: 1. Copy: Data in the cache is a copy of the original data (in the database), not the primary data 2. Fast access: Caches are typically in memory, which is 100,000 times faster to read than a hard drive 3. Limited capacity: Cache space is finite โ€” it can only store the most frequently used data So, caching is trading space for time โ€” sacrificing some memory space in exchange for blazing-fast data access.

Before diving into specific technologies, we need to clarify a few core concepts. To help you understand, we'll use a "student's backpack" as an analogy for a caching system.Before diving into specific technologies, we need to clarify a few core concepts. To help you understand, we'll use a "student's backpack" as an analogy for a caching system.

2.1 Understanding Core Caching Concepts Through the "Backpack Analogy"2.1 Understanding Core Caching Concepts Through the "Backpack Analogy"

Imagine you're a student who needs to look up various references every day. This process is strikingly similar to how a caching system works:Imagine you're a student who needs to look up various references every day. This process is strikingly similar to how a caching system works:

Concept Backpack AnalogyTechnical MeaningReal-World Example
Cache HitThe formula you need is right there on your sticky noteThe requested data is found in the cacheQuerying user info โ€” it's in Redis, returned directly
Cache MissThe sticky note doesn't have it; you have to flip through the bookThe requested data is not in the cacheQuerying user info โ€” not in Redis, must query the database
Hit RatioOut of 100 formula lookups, 95 are on the sticky noteThe proportion of cache hits95% hit ratio means 95% of requests never touch the database
TTL (Time To Live)Write "tear off in 3 days" on the sticky noteThe cache entry's expiration timeSet user info cache to expire automatically after 30 minutes
EvictionBackpack is full โ€” toss the oldest sticky noteDeleting old data when the cache is fullRedis memory is full โ€” auto-delete the least-used data

2.2 Cache Hit vs. Cache Miss2.2 Cache Hit vs. Cache Miss

The performance difference between a cache hit and a cache miss is enormous. Let's look at the concrete numbers:The performance difference between a cache hit and a cache miss is enormous. Let's look at the concrete numbers:

OperationResponse TimeRelative SpeedSuitable Scenario
CPU L1 Cache~0.5 nsFastest (baseline)Internal CPU operations
Memory Read~100 ns200ร— slowerLocal cache (e.g., Caffeine)
Redis Query~1 ms2,000,000ร— slowerDistributed cache
MySQL Query~10 ms20,000,000ร— slowerDisk-based database query
๐Ÿ’ก Tips Praktis๐Ÿ’ก Pro Tip

The performance gap is staggering: memory operations are 100,000 times faster than MySQL queries! It's like the difference between grabbing a book from your desk (1 second) and going to the library to find it (100,000 seconds, or about 28 hours). Three-tier performance ladder: 1. Local cache (memory): Fastest, but small capacity โ€” ideal for hot data 2. Redis cache: Medium speed, large capacity โ€” ideal for distributed scenarios 3. Database: Slowest, but unlimited capacity โ€” the ultimate source of truth Practical takeaway: Your system should serve over 95% of requests from the cache layer, with fewer than 5% needing to hit the database. This keeps database load low and overall system performance high.The performance gap is staggering: memory operations are 100,000 times faster than MySQL queries! It's like the difference between grabbing a book from your desk (1 second) and going to the library to find it (100,000 seconds, or about 28 hours). Three-tier performance ladder: 1. Local cache (memory): Fastest, but small capacity โ€” ideal for hot data 2. Redis cache: Medium speed, large capacity โ€” ideal for distributed scenarios 3. Database: Slowest, but unlimited capacity โ€” the ultimate source of truth Practical takeaway: Your system should serve over 95% of requests from the cache layer, with fewer than 5% needing to hit the database. This keeps database load low and overall system performance high.

๐Ÿ“– Konsep Penting๐Ÿ“– Core Concept

Let's compare the two scenarios with code: ``javascript // Scenario: querying user information // ===== Cache Hit ===== // 1. Check Redis cache first const userFromCache = await redis.get('user:123') if (userFromCache) { // Hit! Return directly, ~1 millisecond return JSON.parse(userFromCache) } // ===== Cache Miss ===== // 2. Not in cache, query the database const userFromDB = await db.query('SELECT * FROM users WHERE id = 123') // Miss! Must query database, ~10 milliseconds, 10ร— slower // 3. Write to cache after querying, so next time it hits await redis.set('user:123', JSON.stringify(userFromDB), 'EX', 1800) return userFromDB `` Key points: - Cache hit: returns in 1 millisecond โ€” excellent user experience - Cache miss: returns in 10 milliseconds โ€” slightly worse user experience - The value of caching: turning misses into hits yields a 10ร— performance improvementLet's compare the two scenarios with code: ``javascript // Scenario: querying user information // ===== Cache Hit ===== // 1. Check Redis cache first const userFromCache = await redis.get('user:123') if (userFromCache) { // Hit! Return directly, ~1 millisecond return JSON.parse(userFromCache) } // ===== Cache Miss ===== // 2. Not in cache, query the database const userFromDB = await db.query('SELECT * FROM users WHERE id = 123') // Miss! Must query database, ~10 milliseconds, 10ร— slower // 3. Write to cache after querying, so next time it hits await redis.set('user:123', JSON.stringify(userFromDB), 'EX', 1800) return userFromDB `` Key points: - Cache hit: returns in 1 millisecond โ€” excellent user experience - Cache miss: returns in 10 milliseconds โ€” slightly worse user experience - The value of caching: turning misses into hits yields a 10ร— performance improvement

2.3 The Cache Lifecycle2.3 The Cache Lifecycle

A cache entry goes through a complete lifecycle from creation to destruction. Understanding this process is crucial for designing caching systems.A cache entry goes through a complete lifecycle from creation to destruction. Understanding this process is crucial for designing caching systems.

Four phases:Four phases:

Phase 1: WritePhase 1: Write

Phase 2: Hit / MissPhase 2: Hit / Miss

Phase 3: ExpirationPhase 3: Expiration

Phase 4: EvictionPhase 4: Eviction

๐Ÿ‘‡ Try it yourself:๐Ÿ‘‡ Try it yourself:

The demo below illustrates the cache lifecycle. Click "Add Cache" and observe how cache entries go through the full process of write, hit, expiration, and eviction:The demo below illustrates the cache lifecycle. Click "Add Cache" and observe how cache entries go through the full process of write, hit, expiration, and eviction:

------

3. The Evolution of Caching: From Single-Machine to Distributed3. The Evolution of Caching: From Single-Machine to Distributed

๐Ÿ’ก Tips Praktis๐Ÿ’ก Pro Tip

Just like you keep reference materials in different places when studying: the most frequently used on your desk (sticky notes), commonly used in your backpack (notebook), and all materials in the library (stacks). Caching systems work the same way: - Local cache (desk): Fastest, small capacity โ€” for super-hot data - Distributed cache (public locker): Fairly fast, large capacity โ€” for commonly used data - Database (library): Slowest, unlimited capacity โ€” for all data Why layer them? Because different tiers have different performance and cost characteristics. Combining them wisely yields the optimal result.Just like you keep reference materials in different places when studying: the most frequently used on your desk (sticky notes), commonly used in your backpack (notebook), and all materials in the library (stacks). Caching systems work the same way: - Local cache (desk): Fastest, small capacity โ€” for super-hot data - Distributed cache (public locker): Fairly fast, large capacity โ€” for commonly used data - Database (library): Slowest, unlimited capacity โ€” for all data Why layer them? Because different tiers have different performance and cost characteristics. Combining them wisely yields the optimal result.

Now that we've covered the concepts, let's look at a real case study: how an e-commerce system evolved from "no cache" to a "multi-level caching architecture." This case study will give you a more intuitive understanding of why cache design matters.Now that we've covered the concepts, let's look at a real case study: how an e-commerce system evolved from "no cache" to a "multi-level caching architecture." This case study will give you a more intuitive understanding of why cache design matters.

3.1 Stage 1: The No-Cache Era โ€” Database Running Naked3.1 Stage 1: The No-Cache Era โ€” Database Running Naked

Background: In the early days, with only a few hundred users, all requests hit the database directly with no caching layer.Background: In the early days, with only a few hundred users, all requests hit the database directly with no caching layer.

Tech stack:Tech stack:

System architecture:System architecture:

CODE
User Request โ†’ Application Server โ†’ MySQL Database

Characteristics of this stage:Characteristics of this stage:

๐Ÿ“– Konsep Penting๐Ÿ“– Core Concept

Code example (querying the database every time): ``javascript // Get product details โ€” query the database every single time async function getProduct(productId) { // Direct database query, no caching whatsoever const product = await db.query( 'SELECT * FROM products WHERE id = ?', [productId] ) return product } `` Problems encountered: 1. Database CPU spikes: Every request hits the database, CPU usage 80%+ 2. Slow responses: Complex queries take 50โ€“100 ms โ€” poor user experience 3. Poor concurrency: Database QPS (queries per second) capped at 2000 โ€” anything more and it collapses 4. Hot product bottleneck: Trending product detail pages are queried relentlessly; the database becomes the bottleneck Temporary workarounds at the time: - Buy more expensive servers (more CPU, more RAM) โ€” costly, limited effect - Database read/write separation โ€” relieves read pressure, but write pressure remains - SQL optimization โ€” 20โ€“30% improvement, but doesn't solve the fundamental problemCode example (querying the database every time): ``javascript // Get product details โ€” query the database every single time async function getProduct(productId) { // Direct database query, no caching whatsoever const product = await db.query( 'SELECT * FROM products WHERE id = ?', [productId] ) return product } `` Problems encountered: 1. Database CPU spikes: Every request hits the database, CPU usage 80%+ 2. Slow responses: Complex queries take 50โ€“100 ms โ€” poor user experience 3. Poor concurrency: Database QPS (queries per second) capped at 2000 โ€” anything more and it collapses 4. Hot product bottleneck: Trending product detail pages are queried relentlessly; the database becomes the bottleneck Temporary workarounds at the time: - Buy more expensive servers (more CPU, more RAM) โ€” costly, limited effect - Database read/write separation โ€” relieves read pressure, but write pressure remains - SQL optimization โ€” 20โ€“30% improvement, but doesn't solve the fundamental problem

This "running naked" model works when users < 1,000, but as the user base grows to 10,000 or 100,000, the database begins to crash frequently, and the team urgently needs to introduce caching.This "running naked" model works when users < 1,000, but as the user base grows to 10,000 or 100,000, the database begins to crash frequently, and the team urgently needs to introduce caching.

3.2 Stage 2: Introducing Redis Cache โ€” 10ร— Performance Improvement3.2 Stage 2: Introducing Redis Cache โ€” 10ร— Performance Improvement

Background: Users grew to 10,000; the database couldn't keep up. The team decided to introduce Redis as a caching layer.Background: Users grew to 10,000; the database couldn't keep up. The team decided to introduce Redis as a caching layer.

Tech stack:Tech stack:

System architecture:System architecture:

CODE
User Request โ†’ Application Server โ†’ Redis Cache (query DB only on miss) โ†’ MySQL Database

Characteristics of this stage:Characteristics of this stage:

๐Ÿ“– Konsep Penting๐Ÿ“– Core Concept

Code example (adding Redis cache): ``javascript // Get product details โ€” check Redis first, fall back to database async function getProduct(productId) { // 1. Check Redis cache first const cacheKey = product:${productId} const cached = await redis.get(cacheKey) if (cached) { // Cache hit! Return directly, ~1 millisecond return JSON.parse(cached) } // 2. Cache miss, query the database const product = await db.query( 'SELECT * FROM products WHERE id = ?', [productId] ) // 3. Write to Redis after querying, set 30-minute TTL await redis.setex( cacheKey, 1800, // 30 minutes = 1800 seconds JSON.stringify(product) ) return product } `` Performance comparison: | Scenario | Without Cache | With Redis Cache | Improvement | |------|-------|--------------|---------| | Regular product query | 50ms | 5ms (on cache hit) | 10ร— | | Hot product query | 80ms | 1ms (95% hit rate) | 80ร— | | Database QPS | 2000 (at capacity) | 200 (90% intercepted by cache) | 10ร— less DB load | | Max system concurrency | 2,000 users | 20,000 users | 10ร— | Improvements achieved: 1. Response speed: On cache hit, response time drops from 50ms to 1โ€“5ms 2. Concurrency capacity: System can support 20,000 users, up from 2,000 3. Database load: 90% of requests intercepted by Redis, DB CPU drops from 80% to 20% 4. User experience: Noticeably faster page loads, fewer user complaints New challenges: 1. Cache consistency: Product price changes, the database is updated, but the cache still holds the old value 2. Cache penetration: Malicious queries for non-existent product IDs (e.g., id=-1) punch through to the database every time 3. Cache avalanche: After a system restart, all caches expire simultaneously, flooding the database with requests 4. Redis single point of failure: If Redis goes down, all requests hit the database directly โ€” the system may collapse Solutions: - Cache consistency: When updating the database, delete the cache synchronously - Cache penetration: Cache non-existent data too (with an empty value and a short TTL, e.g., 5 minutes) - Cache avalanche: Add a random offset to cache TTLs to avoid simultaneous expirationCode example (adding Redis cache): ``javascript // Get product details โ€” check Redis first, fall back to database async function getProduct(productId) { // 1. Check Redis cache first const cacheKey = product:${productId} const cached = await redis.get(cacheKey) if (cached) { // Cache hit! Return directly, ~1 millisecond return JSON.parse(cached) } // 2. Cache miss, query the database const product = await db.query( 'SELECT * FROM products WHERE id = ?', [productId] ) // 3. Write to Redis after querying, set 30-minute TTL await redis.setex( cacheKey, 1800, // 30 minutes = 1800 seconds JSON.stringify(product) ) return product } `` Performance comparison: | Scenario | Without Cache | With Redis Cache | Improvement | |------|-------|--------------|---------| | Regular product query | 50ms | 5ms (on cache hit) | 10ร— | | Hot product query | 80ms | 1ms (95% hit rate) | 80ร— | | Database QPS | 2000 (at capacity) | 200 (90% intercepted by cache) | 10ร— less DB load | | Max system concurrency | 2,000 users | 20,000 users | 10ร— | Improvements achieved: 1. Response speed: On cache hit, response time drops from 50ms to 1โ€“5ms 2. Concurrency capacity: System can support 20,000 users, up from 2,000 3. Database load: 90% of requests intercepted by Redis, DB CPU drops from 80% to 20% 4. User experience: Noticeably faster page loads, fewer user complaints New challenges: 1. Cache consistency: Product price changes, the database is updated, but the cache still holds the old value 2. Cache penetration: Malicious queries for non-existent product IDs (e.g., id=-1) punch through to the database every time 3. Cache avalanche: After a system restart, all caches expire simultaneously, flooding the database with requests 4. Redis single point of failure: If Redis goes down, all requests hit the database directly โ€” the system may collapse Solutions: - Cache consistency: When updating the database, delete the cache synchronously - Cache penetration: Cache non-existent data too (with an empty value and a short TTL, e.g., 5 minutes) - Cache avalanche: Add a random offset to cache TTLs to avoid simultaneous expiration

After introducing Redis, system performance improved dramatically, but new problems emerged. The team began researching how to solve these cache-related issues.After introducing Redis, system performance improved dramatically, but new problems emerged. The team began researching how to solve these cache-related issues.

3.3 Stage 3: Multi-Level Cache Architecture โ€” Another 5ร— Performance Boost3.3 Stage 3: Multi-Level Cache Architecture โ€” Another 5ร— Performance Boost

Background: Users grew to 100,000. Even Redis caching started becoming a bottleneck (single-instance Redis QPS caps at ~100,000). The team decided to introduce multi-level caching.Background: Users grew to 100,000. Even Redis caching started becoming a bottleneck (single-instance Redis QPS caps at ~100,000). The team decided to introduce multi-level caching.

Tech stack:Tech stack:

System architecture:System architecture:

CODE
User Request โ†’ CDN Cache (static assets) โ†’ Application Server โ†“ L1: Local Cache (Caffeine) โ†’ Miss โ†’ L2: Redis โ†’ Miss โ†’ MySQL

Characteristics of this stage:Characteristics of this stage:

๐Ÿ“– Konsep Penting๐Ÿ“– Core Concept

Code example (local cache + Redis two-level caching): ``javascript // Using Caffeine local cache const caffeine = require('caffeine') const localCache = new caffeine.Cache({ max: 1000, // Max 1000 entries ttl: 30, // 30-second TTL }) // Get product details โ€” two-level caching async function getProduct(productId) { const cacheKey = product:${productId} // L1: Check local cache first (fastest, ~0.1 ms) const localCached = localCache.get(cacheKey) if (localCached) { console.log('L1 hit') return localCached } // L2: Local cache miss, check Redis (fast, ~1 ms) const redisCached = await redis.get(cacheKey) if (redisCached) { console.log('L2 hit, backfilling L1') const product = JSON.parse(redisCached) // Backfill local cache localCache.set(cacheKey, product) return product } // L3: Redis also missed, query database (slowest, ~10 ms) console.log('L3 hit, backfilling L2 and L1') const product = await db.query( 'SELECT * FROM products WHERE id = ?', [productId] ) // Backfill Redis (30-minute TTL) await redis.setex(cacheKey, 1800, JSON.stringify(product)) // Backfill local cache localCache.set(cacheKey, product) return product } `` Multi-level cache performance comparison: | Cache Tier | Response Time | Hit Rate | Suitable Data | |---------|---------|--------|--------------| | L1: Local Cache | ~0.1 ms | 70% (super-hot) | Hot products, system config, user sessions | | L2: Redis Cache | ~1 ms | 25% (warm) | Most product data, comment aggregates | | L3: Database | ~10 ms | 5% (cold data) | Complete product catalog | Overall performance improvement: - Average response time: 5ms (Stage 2) โ†’ 1ms (Stage 3), another 5ร— improvement - Max system concurrency: 20,000 users (Stage 2) โ†’ 100,000 users (Stage 3), 5ร— improvement - Database QPS: 200 (Stage 2) โ†’ 50 (Stage 3), another 4ร— reduction New problems solved at this stage: 1. Local cache consistency: Multiple app instances may have inconsistent local caches (Instance A has the old price cached, Instance B has the new one) - Solution: Set a short local cache TTL (30 seconds) to minimize the inconsistency window 2. Cache preheating: After a system restart, the local cache is empty โ€” massive requests punch through to Redis - Solution: Actively load hot data into the local cache at system startupCode example (local cache + Redis two-level caching): ``javascript // Using Caffeine local cache const caffeine = require('caffeine') const localCache = new caffeine.Cache({ max: 1000, // Max 1000 entries ttl: 30, // 30-second TTL }) // Get product details โ€” two-level caching async function getProduct(productId) { const cacheKey = product:${productId} // L1: Check local cache first (fastest, ~0.1 ms) const localCached = localCache.get(cacheKey) if (localCached) { console.log('L1 hit') return localCached } // L2: Local cache miss, check Redis (fast, ~1 ms) const redisCached = await redis.get(cacheKey) if (redisCached) { console.log('L2 hit, backfilling L1') const product = JSON.parse(redisCached) // Backfill local cache localCache.set(cacheKey, product) return product } // L3: Redis also missed, query database (slowest, ~10 ms) console.log('L3 hit, backfilling L2 and L1') const product = await db.query( 'SELECT * FROM products WHERE id = ?', [productId] ) // Backfill Redis (30-minute TTL) await redis.setex(cacheKey, 1800, JSON.stringify(product)) // Backfill local cache localCache.set(cacheKey, product) return product } `` Multi-level cache performance comparison: | Cache Tier | Response Time | Hit Rate | Suitable Data | |---------|---------|--------|--------------| | L1: Local Cache | ~0.1 ms | 70% (super-hot) | Hot products, system config, user sessions | | L2: Redis Cache | ~1 ms | 25% (warm) | Most product data, comment aggregates | | L3: Database | ~10 ms | 5% (cold data) | Complete product catalog | Overall performance improvement: - Average response time: 5ms (Stage 2) โ†’ 1ms (Stage 3), another 5ร— improvement - Max system concurrency: 20,000 users (Stage 2) โ†’ 100,000 users (Stage 3), 5ร— improvement - Database QPS: 200 (Stage 2) โ†’ 50 (Stage 3), another 4ร— reduction New problems solved at this stage: 1. Local cache consistency: Multiple app instances may have inconsistent local caches (Instance A has the old price cached, Instance B has the new one) - Solution: Set a short local cache TTL (30 seconds) to minimize the inconsistency window 2. Cache preheating: After a system restart, the local cache is empty โ€” massive requests punch through to Redis - Solution: Actively load hot data into the local cache at system startup

Multi-level cache architectures are widely used by large internet companies (like Taobao, JD.com) and can support millions of QPS.Multi-level cache architectures are widely used by large internet companies (like Taobao, JD.com) and can support millions of QPS.

3.4 Cache Architecture Evolution Panorama3.4 Cache Architecture Evolution Panorama

StageArchitectureResponse TimeMax ConcurrencyKey Change
Stage 1: No CacheApp โ†’ Database50ms2,000 usersDatabase running naked, poor performance
Stage 2: Single-Level CacheApp โ†’ Redis โ†’ Database5ms20,000 usersRedis introduced, 10ร— performance boost
Stage 3: Multi-Level CacheApp โ†’ Local Cache โ†’ Redis โ†’ Database1ms100,000 usersLocal cache + Redis, another 5ร— boost
๐Ÿ’ก Tips Praktis๐Ÿ’ก Pro Tip

Stage 1 โ†’ Stage 2: A qualitative leap. Introducing Redis brings a 10ร— performance improvement and 90% reduction in database load. This is the critical step from "barely working" to "good enough." Stage 2 โ†’ Stage 3: Extreme optimization. Adding local cache brings another 5ร— improvement. This is the progression from "good enough" to "excellent," suited for ultra-high-traffic scenarios. Practical recommendations: - Users < 10,000: Stage 1 (no cache) suffices, but introducing Redis (Stage 2) is recommended - Users 10,000โ€“100,000: Stage 2 (Redis cache) is the optimal choice - Users > 100,000: Consider Stage 3 (multi-level cache), but be mindful of the consistency complexity In summary: Cache architecture evolution isn't just about "adding more cache layers" โ€” it's about choosing the right architecture for your traffic scale. Over-engineering adds unnecessary complexity; under-engineering leads to performance bottlenecks.Stage 1 โ†’ Stage 2: A qualitative leap. Introducing Redis brings a 10ร— performance improvement and 90% reduction in database load. This is the critical step from "barely working" to "good enough." Stage 2 โ†’ Stage 3: Extreme optimization. Adding local cache brings another 5ร— improvement. This is the progression from "good enough" to "excellent," suited for ultra-high-traffic scenarios. Practical recommendations: - Users < 10,000: Stage 1 (no cache) suffices, but introducing Redis (Stage 2) is recommended - Users 10,000โ€“100,000: Stage 2 (Redis cache) is the optimal choice - Users > 100,000: Consider Stage 3 (multi-level cache), but be mindful of the consistency complexity In summary: Cache architecture evolution isn't just about "adding more cache layers" โ€” it's about choosing the right architecture for your traffic scale. Over-engineering adds unnecessary complexity; under-engineering leads to performance bottlenecks.

------

4. The Three Classic Cache Problems: Penetration, Breakdown, and Avalanche4. The Three Classic Cache Problems: Penetration, Breakdown, and Avalanche

In practice, caching introduces three classic problems. If you don't understand them, your system could suddenly collapse at some point. Let's use everyday analogies to understand these problems.In practice, caching introduces three classic problems. If you don't understand them, your system could suddenly collapse at some point. Let's use everyday analogies to understand these problems.

4.1 Cache Penetration: Querying Non-Existent Data4.1 Cache Penetration: Querying Non-Existent Data

Problem definition: Querying data that doesn't exist (e.g., id=-1). It's not in the cache (because it was never stored), and it's not in the database โ€” so every request punches straight through to the database.Problem definition: Querying data that doesn't exist (e.g., id=-1). It's not in the cache (because it was never stored), and it's not in the database โ€” so every request punches straight through to the database.

๐Ÿ’ก Tips Praktis๐Ÿ’ก Pro Tip

Imagine you're at a library looking for a book. You ask the librarian: "Do you have 'The Nonexistent Book'?" Normal flow: - The librarian checks the catalog: "No such book" - You leave Cache penetration scenario: - You come the 1st time, the librarian checks the database: "No," tells you - You come the 2nd time, the librarian checks the database again: "No" - You come the 100th time, the librarian still checks the database: "No" Problem: The librarian (database) is exhausted โ€” every query hits the database, even though the answer is always "no." Solution: The librarian remembers that "'The Nonexistent Book' doesn't exist." Next time you ask, they say "no" immediately without checking the database. This is caching a null object.Imagine you're at a library looking for a book. You ask the librarian: "Do you have 'The Nonexistent Book'?" Normal flow: - The librarian checks the catalog: "No such book" - You leave Cache penetration scenario: - You come the 1st time, the librarian checks the database: "No," tells you - You come the 2nd time, the librarian checks the database again: "No" - You come the 100th time, the librarian still checks the database: "No" Problem: The librarian (database) is exhausted โ€” every query hits the database, even though the answer is always "no." Solution: The librarian remembers that "'The Nonexistent Book' doesn't exist." Next time you ask, they say "no" immediately without checking the database. This is caching a null object.

Real-world scenarios:Real-world scenarios:

Solution 1: Cache Null ObjectsSolution 1: Cache Null Objects

javascript
async function getProduct(productId) { const cacheKey = `product:${productId}` // 1. Check cache first const cached = await redis.get(cacheKey) if (cached !== null) { // Note: cached could be the string "null" if (cached === 'null') { // Cached "null object" โ€” the database doesn't have this data return null } return JSON.parse(cached) } // 2. Query the database const product = await db.query( 'SELECT * FROM products WHERE id = ?', [productId] ) // 3. Even if the database has nothing, cache "null" with a short TTL (e.g., 5 minutes) if (!product) { await redis.setex(cacheKey, 300, 'null') return null } // 4. Data found, cache normally await redis.setex(cacheKey, 1800, JSON.stringify(product)) return product }

Solution 2: Bloom FilterSolution 2: Bloom Filter

A Bloom filter is a tool for "quickly determining whether data exists" โ€” it's like a "super index":A Bloom filter is a tool for "quickly determining whether data exists" โ€” it's like a "super index":

๐Ÿ’ก Tips Praktis๐Ÿ’ก Pro Tip

Imagine you have a "magic black box": - You ask it: "Does product ID 123 exist?" - It says: "Definitely not" โ†’ then it truly doesn't exist โ€” no need to query the database - It says: "Might exist" โ†’ then go query the database to confirm Characteristics: - No false negatives: If it says something doesn't exist, it truly doesn't - Possible false positives: If it says something might exist, it might actually not (low probability, tunable) Value: A Bloom filter can intercept 99% of "doesn't exist" requests before they even reach the cache, protecting the database.Imagine you have a "magic black box": - You ask it: "Does product ID 123 exist?" - It says: "Definitely not" โ†’ then it truly doesn't exist โ€” no need to query the database - It says: "Might exist" โ†’ then go query the database to confirm Characteristics: - No false negatives: If it says something doesn't exist, it truly doesn't - Possible false positives: If it says something might exist, it might actually not (low probability, tunable) Value: A Bloom filter can intercept 99% of "doesn't exist" requests before they even reach the cache, protecting the database.

javascript
// Using a Bloom filter const { BloomFilter } = require('bloom-filters') // Initialize Bloom filter (assuming up to 1 million product IDs) const bloomFilter = new BloomFilter(1000000, 0.01) // 1% false positive rate // At system startup, add all product IDs to the Bloom filter async function initBloomFilter() { const allIds = await db.query('SELECT id FROM products') allIds.forEach(row => { bloomFilter.add(row.id) }) } // Before querying a product, check with the Bloom filter first async function getProduct(productId) { // 1. Check with Bloom filter first if (!bloomFilter.has(productId)) { // Definitely doesn't exist โ€” return null, no need to query the database console.log('Bloom filter intercept: product does not exist') return null } // 2. Bloom filter says "might exist" โ€” check cache const cached = await redis.get(`product:${productId}`) if (cached) { return JSON.parse(cached) } // 3. Cache miss โ€” query database const product = await db.query( 'SELECT * FROM products WHERE id = ?', [productId] ) if (!product) { // Bloom filter false positive (very low probability) โ€” actually doesn't exist await redis.setex(`product:${productId}`, 300, 'null') return null } // 4. Data found, write to cache await redis.setex(`product:${productId}`, 1800, JSON.stringify(product)) return product }

4.2 Cache Breakdown: Hot Data Expiration4.2 Cache Breakdown: Hot Data Expiration

Problem definition: A piece of hot data (like a trending product or hot news) expires in the cache (TTL runs out). At that moment, a flood of concurrent requests all arrive simultaneously and all query the database, causing a sudden spike in database load.Problem definition: A piece of hot data (like a trending product or hot news) expires in the cache (TTL runs out). At that moment, a flood of concurrent requests all arrive simultaneously and all query the database, causing a sudden spike in database load.

๐Ÿ’ก Tips Praktis๐Ÿ’ก Pro Tip

Imagine the library has a copy of "Harry Potter" โ€” super popular, 100 people want to borrow it. Normal situation: - The library keeps "Harry Potter" at the "checkout counter" (cache) - Everyone grabs it directly from the counter โ€” no need to search the shelves Cache breakdown scenario: - The copy of "Harry Potter" at the checkout counter expires (gets returned to the shelves) - 100 people come to borrow it at the same time, find the counter empty - All 100 rush to the shelves (database) - The shelf manager (database) gets overwhelmed Problem: It's not a "non-existent book" โ€” it's a "super popular book" that suddenly vanished from the cache, causing a massive surge of requests to hit the database simultaneously.Imagine the library has a copy of "Harry Potter" โ€” super popular, 100 people want to borrow it. Normal situation: - The library keeps "Harry Potter" at the "checkout counter" (cache) - Everyone grabs it directly from the counter โ€” no need to search the shelves Cache breakdown scenario: - The copy of "Harry Potter" at the checkout counter expires (gets returned to the shelves) - 100 people come to borrow it at the same time, find the counter empty - All 100 rush to the shelves (database) - The shelf manager (database) gets overwhelmed Problem: It's not a "non-existent book" โ€” it's a "super popular book" that suddenly vanished from the cache, causing a massive surge of requests to hit the database simultaneously.

Real-world scenarios:Real-world scenarios:

Solution 1: Mutex LockSolution 1: Mutex Lock

javascript
async function getProduct(productId) { const cacheKey = `product:${productId}` // 1. Check cache first const cached = await redis.get(cacheKey) if (cached) { return JSON.parse(cached) } // 2. Cache miss โ€” acquire distributed lock const lockKey = `lock:${productId}` const lock = await redis.set(lockKey, '1', 'NX', 'EX', 10) // 10-second lock if (lock === 'OK') { // 3. Lock acquired โ€” query database console.log('Lock acquired, querying database') const product = await db.query( 'SELECT * FROM products WHERE id = ?', [productId] ) // 4. Write to cache await redis.setex(cacheKey, 1800, JSON.stringify(product)) // 5. Release lock await redis.del(lockKey) return product } else { // 6. Lock not acquired โ€” wait 50ms and retry console.log('Lock not acquired, waiting and retrying') await new Promise(resolve => setTimeout(resolve, 50)) return getProduct(productId) // Recursive retry } }

Solution 2: Logical ExpirationSolution 2: Logical Expiration

javascript
async function getProduct(productId) { const cacheKey = `product:${productId}` // 1. Check cache const cached = await redis.get(cacheKey) if (cached) { const data = JSON.parse(cached) // 2. Check logical expiration time if (Date.now() < data.expireTime) { // Not expired โ€” return directly return data.product } else { // 3. Logically expired โ€” rebuild cache asynchronously while returning old data console.log('Logically expired, rebuilding cache asynchronously') rebuildCacheAsync(productId) // Async rebuild return data.product // Return old data } } // 4. Cache doesn't exist (first load) โ€” query database synchronously const product = await db.query( 'SELECT * FROM products WHERE id = ?', [productId] ) // 5. Write to cache (including logical expiration time) const cacheData = { product: product, expireTime: Date.now() + 30 * 60 * 1000 // Logically expires in 30 minutes } await redis.set(cacheKey, JSON.stringify(cacheData)) return product } // Async cache rebuild async function rebuildCacheAsync(productId) { const lockKey = `rebuild:${productId}` const lock = await redis.set(lockKey, '1', 'NX', 'EX', 10) if (lock === 'OK') { console.log('Async cache rebuild started') const product = await db.query( 'SELECT * FROM products WHERE id = ?', [productId] ) const cacheData = { product: product, expireTime: Date.now() + 30 * 60 * 1000 } await redis.set(`product:${productId}`, JSON.stringify(cacheData)) await redis.del(lockKey) console.log('Async cache rebuild complete') } }

4.3 Cache Avalanche: Mass Simultaneous Expiration4.3 Cache Avalanche: Mass Simultaneous Expiration

Problem definition: A large amount of cached data expires at the same moment (or Redis goes down), causing all requests to punch through to the database simultaneously and overwhelm it instantly.Problem definition: A large amount of cached data expires at the same moment (or Redis goes down), causing all requests to punch through to the database simultaneously and overwhelm it instantly.

๐Ÿ’ก Tips Praktis๐Ÿ’ก Pro Tip

Imagine the library's "checkout counter" (cache) has 1,000 books. Normal situation: - The return dates for these books are scattered: some due today, some tomorrow, some the day after - Only a few dozen books expire each day โ€” the manager (database) can handle it easily Cache avalanche scenario: - After a system restart, the manager sets all 1,000 books to "due in 30 days" - 30 days later, all 1,000 books expire at the same time - 1,000 people come to borrow at once, find the counter empty - All 1,000 rush to the shelves - The shelf manager (database) gets overwhelmed instantly Problem: It's not about a single book โ€” it's about mass data expiring simultaneously, causing a sudden, massive spike in database pressure.Imagine the library's "checkout counter" (cache) has 1,000 books. Normal situation: - The return dates for these books are scattered: some due today, some tomorrow, some the day after - Only a few dozen books expire each day โ€” the manager (database) can handle it easily Cache avalanche scenario: - After a system restart, the manager sets all 1,000 books to "due in 30 days" - 30 days later, all 1,000 books expire at the same time - 1,000 people come to borrow at once, find the counter empty - All 1,000 rush to the shelves - The shelf manager (database) gets overwhelmed instantly Problem: It's not about a single book โ€” it's about mass data expiring simultaneously, causing a sudden, massive spike in database pressure.

Real-world scenarios:Real-world scenarios:

Solution 1: Random TTLSolution 1: Random TTL

javascript
async function getProduct(productId) { const cacheKey = `product:${productId}` const cached = await redis.get(cacheKey) if (cached) { return JSON.parse(cached) } const product = await db.query( 'SELECT * FROM products WHERE id = ?', [productId] ) // Key: add a random offset (ยฑ5 minutes) to the base TTL (30 minutes) const baseTTL = 1800 // 30 minutes const randomOffset = Math.floor(Math.random() * 600) - 300 // -5 to +5 minutes const finalTTL = baseTTL + randomOffset console.log(`Cache TTL: ${finalTTL} seconds (${Math.floor(finalTTL / 60)} minutes)`) await redis.setex(cacheKey, finalTTL, JSON.stringify(product)) return product }

Solution 2: Cache PreheatingSolution 2: Cache Preheating

javascript
// At system startup, proactively load hot data into the cache async function cacheWarmup() { console.log('Starting cache preheating...') // 1. Query the top 1,000 hottest products (sorted by view count) const hotProducts = await db.query(` SELECT * FROM products ORDER BY view_count DESC LIMIT 1000 `) // 2. Batch-write to Redis for (const product of hotProducts) { const cacheKey = `product:${product.id}` const ttl = 1800 + Math.floor(Math.random() * 600) // 30 minutes ยฑ 5 minutes await redis.setex(cacheKey, ttl, JSON.stringify(product)) } console.log(`Cache preheating complete, loaded ${hotProducts.length} hot products`) } // Execute at application startup cacheWarmup()

Solution 3: Circuit Breaker with FallbackSolution 3: Circuit Breaker with Fallback

javascript
// Use a circuit breaker to protect the database const CircuitBreaker = require('opossum') // Configure the circuit breaker const dbQueryBreaker = new CircuitBreaker( async (productId) => { return await db.query('SELECT * FROM products WHERE id = ?', [productId]) }, { timeout: 3000, // 3-second timeout errorThresholdPercentage: 50, // Trip when error rate exceeds 50% resetTimeout: 30000 // Attempt recovery after 30 seconds } ) // Fallback handling when the circuit is open dbQueryBreaker.fallback(() => { console.log('Database circuit open, returning fallback data') return { id: productId, name: 'Service is busy, please try again later', status: 'degraded' } }) async function getProduct(productId) { const cacheKey = `product:${productId}` const cached = await redis.get(cacheKey) if (cached) { return JSON.parse(cached) } // Query database through the circuit breaker const product = await dbQueryBreaker.fire(productId) if (product.status === 'degraded') { return product // Return fallback data } await redis.setex(cacheKey, 1800, JSON.stringify(product)) return product }

๐Ÿ‘‡ Try it yourself:๐Ÿ‘‡ Try it yourself:

The demo below compares the scenarios and solutions for cache penetration, breakdown, and avalanche:The demo below compares the scenarios and solutions for cache penetration, breakdown, and avalanche:

------

5. Cache Consistency Strategies: Keeping Cache and Database in Sync5. Cache Consistency Strategies: Keeping Cache and Database in Sync

A cache is, by nature, a copy of data. There's always a window of inconsistency between the copy and the original data (the database). How to control this window is the core challenge of cache design.A cache is, by nature, a copy of data. There's always a window of inconsistency between the copy and the original data (the database). How to control this window is the core challenge of cache design.

5.1 Motivation for Caching and Database Become Inconsistent5.1 Motivation for Caching and Database Become Inconsistent

๐Ÿ’ก Tips Praktis๐Ÿ’ก Pro Tip

Imagine you've written on a sticky note: "Xiao Ming's phone: 123456" โ€” this is a copy of your address book (the database). Inconsistency scenario: - You update your address book, changing Xiao Ming's number to "7654321" - But you forget to update the sticky note - Next time you look up the number, you check the sticky note โ€” still the old "123456" The problem: The sticky note (cache) and the address book (database) are out of sync. The cause: The original data was updated, but the copy wasn't synced. In computer systems, this happens because "updating the database" and "updating the cache" are two independent operations with a time gap between them, during which other operations can interfere.Imagine you've written on a sticky note: "Xiao Ming's phone: 123456" โ€” this is a copy of your address book (the database). Inconsistency scenario: - You update your address book, changing Xiao Ming's number to "7654321" - But you forget to update the sticky note - Next time you look up the number, you check the sticky note โ€” still the old "123456" The problem: The sticky note (cache) and the address book (database) are out of sync. The cause: The original data was updated, but the copy wasn't synced. In computer systems, this happens because "updating the database" and "updating the cache" are two independent operations with a time gap between them, during which other operations can interfere.

A real concurrency scenario:A real concurrency scenario:

TimeThread A (updating user age)Thread B (querying user)DatabaseCache
T1Starts updating database-age=20age=20
T2Database updated to age=25Queries cache, hits age=20age=25age=20 โŒ
T3Deletes cache-age=25-
T4--age=25Loads age=25 from DB โœ…

The problem: At time T2, Thread B reads the old value 20 from the cache, while the database already has 25. This is cache inconsistency.The problem: At time T2, Thread B reads the old value 20 from the cache, while the database already has 25. This is cache inconsistency.

5.2 Best Practice: Update the Database First, Then Delete the Cache5.2 Best Practice: Update the Database First, Then Delete the Cache

๐Ÿ’ก Tips Praktis๐Ÿ’ก Pro Tip

You might wonder: why not just "update the cache" directly instead of "deleting the cache"? Problems with updating the cache: - Under concurrent updates, Thread A might update the cache first, then Thread B updates the database but the cache doesn't get updated - Updating the cache can be expensive (e.g., it may require aggregating data from multiple tables) - If the data gets deleted right after updating, the effort is wasted Advantages of deleting the cache: - The next query automatically loads the latest data from the database (lazy loading) - Avoids dirty data caused by concurrent updates - Simple and reliable โ€” the industry best practiceYou might wonder: why not just "update the cache" directly instead of "deleting the cache"? Problems with updating the cache: - Under concurrent updates, Thread A might update the cache first, then Thread B updates the database but the cache doesn't get updated - Updating the cache can be expensive (e.g., it may require aggregating data from multiple tables) - If the data gets deleted right after updating, the effort is wasted Advantages of deleting the cache: - The next query automatically loads the latest data from the database (lazy loading) - Avoids dirty data caused by concurrent updates - Simple and reliable โ€” the industry best practice

Standard flow:Standard flow:

javascript
// Update product information async function updateProduct(productId, updateData) { // 1. Update the database first await db.query( 'UPDATE products SET name = ?, price = ? WHERE id = ?', [updateData.name, updateData.price, productId] ) // 2. Then delete the cache (not update it!) await redis.del(`product:${productId}`) // 3. On the next query, cache miss triggers automatic reload of the latest data console.log('Update complete, cache deleted') }
๐Ÿ“– Konsep Penting๐Ÿ“– Core Concept

Comparing three update strategies: Strategy 1: Update cache first, then database โŒ Not Recommended ``javascript // Problem: if the DB update fails, cache holds the new value, DB holds the old โ€” inconsistent await redis.set('product:1', newProduct) // Cache update succeeds await db.query('UPDATE products SET ...') // Database update fails! // Result: cache has new value, database has old value โ€” permanently inconsistent! ` Strategy 2: Delete cache first, then update database โŒ Not Recommended `javascript // Problem: between delete and update, another thread queries and loads old data into cache await redis.del('product:1') // Cache deleted // At this moment, Thread B queries, finds no cache, queries DB (still old value), writes to cache await db.query('UPDATE products SET ...') // Database updated // Result: cache has old value, database has new value โ€” inconsistent! ` Strategy 3: Update database first, then delete cache โœ… Recommended `javascript // Advantage: the DB update acquires a row lock โ€” other threads must wait, avoiding dirty data await db.query('UPDATE products SET ...') // Update database (acquires row lock) await redis.del('product:1') // Delete cache // Even if cache deletion fails, the next query just goes back to the source โ€” no lingering dirty data `` Why is Strategy 3 optimal? 1. Database lock protection: The update operation acquires a row lock โ€” other read/write operations must wait 2. Low impact of deletion failure: Even if cache deletion fails, it just means the next read will hit the source โ€” no dirty data 3. Simple and reliable: No additional complex logic neededComparing three update strategies: Strategy 1: Update cache first, then database โŒ Not Recommended ``javascript // Problem: if the DB update fails, cache holds the new value, DB holds the old โ€” inconsistent await redis.set('product:1', newProduct) // Cache update succeeds await db.query('UPDATE products SET ...') // Database update fails! // Result: cache has new value, database has old value โ€” permanently inconsistent! ` Strategy 2: Delete cache first, then update database โŒ Not Recommended `javascript // Problem: between delete and update, another thread queries and loads old data into cache await redis.del('product:1') // Cache deleted // At this moment, Thread B queries, finds no cache, queries DB (still old value), writes to cache await db.query('UPDATE products SET ...') // Database updated // Result: cache has old value, database has new value โ€” inconsistent! ` Strategy 3: Update database first, then delete cache โœ… Recommended `javascript // Advantage: the DB update acquires a row lock โ€” other threads must wait, avoiding dirty data await db.query('UPDATE products SET ...') // Update database (acquires row lock) await redis.del('product:1') // Delete cache // Even if cache deletion fails, the next query just goes back to the source โ€” no lingering dirty data `` Why is Strategy 3 optimal? 1. Database lock protection: The update operation acquires a row lock โ€” other read/write operations must wait 2. Low impact of deletion failure: Even if cache deletion fails, it just means the next read will hit the source โ€” no dirty data 3. Simple and reliable: No additional complex logic needed

5.3 Delayed Double-Delete: Consistency Guarantee for Extreme Scenarios5.3 Delayed Double-Delete: Consistency Guarantee for Extreme Scenarios

Scenario: Under high concurrency, even "update DB first, then delete cache" has a tiny probability of inconsistency. Delayed double-delete maximizes consistency through two deletions.Scenario: Under high concurrency, even "update DB first, then delete cache" has a tiny probability of inconsistency. Delayed double-delete maximizes consistency through two deletions.

Flow:Flow:

CODE
1. Delete cache 2. Update database 3. Wait for a period (e.g., 500ms) 4. Delete cache again
javascript
async function updateProduct(productId, updateData) { const cacheKey = `product:${productId}` // 1. First cache deletion await redis.del(cacheKey) // 2. Update database await db.query( 'UPDATE products SET name = ?, price = ? WHERE id = ?', [updateData.name, updateData.price, productId] ) // 3. Wait 500ms (let other threads' queries complete) await new Promise(resolve => setTimeout(resolve, 500)) // 4. Second cache deletion (remove old data possibly loaded by other threads) await redis.del(cacheKey) console.log('Delayed double-delete complete, data synced') }

Comparison of three consistency strategies:Comparison of three consistency strategies:

StrategyConsistency LevelPerformance ImpactComplexitySuitable Scenario
Update DB first, then delete cacheEventual consistency (inconsistency window < 100ms)LowLowMost scenarios; recommended as the default
Delayed double-deleteStrong eventual consistency (inconsistency window < 10ms)Medium (500ms delay)MediumScenarios requiring higher consistency (e.g., finance, inventory)
Delete cache first, then update DBWeak (large inconsistency window)LowLowโŒ Not recommended; prone to inconsistency

๐Ÿ‘‡ Try it yourself:๐Ÿ‘‡ Try it yourself:

The demo below compares the effects of the three consistency strategies. Click "Update Data" and observe how cache and database consistency changes:The demo below compares the effects of the three consistency strategies. Click "Update Data" and observe how cache and database consistency changes:

------

6. Hands-On: Building a Complete Caching System6. Hands-On: Building a Complete Caching System

Now that we've covered the principles, let's look at a real case study: how to design a complete caching system for an e-commerce product detail page.Now that we've covered the principles, let's look at a real case study: how to design a complete caching system for an e-commerce product detail page.

6.1 Business Scenario Analysis6.1 Business Scenario Analysis

Requirement: Users visit a product detail page that needs to display basic product info, price, inventory, reviews, and more.Requirement: Users visit a product detail page that needs to display basic product info, price, inventory, reviews, and more.

Characteristics:Characteristics:

Performance targets:Performance targets:

6.2 Architecture Design6.2 Architecture Design

Multi-level cache architecture:Multi-level cache architecture:

CODE
User Request โ†“ CDN Cache (static assets: images, CSS, JS) โ†“ Miss Nginx Local Cache (aggregated product info) โ†“ Miss Application Server โ†“ โ”œโ”€ L1: Local Cache (Caffeine, hot products) โ”‚ โ†“ Miss โ”œโ”€ L2: Redis Cache (all product data) โ”‚ โ†“ Miss โ””โ”€ L3: MySQL Database (complete dataset)

6.3 Core Code Implementation6.3 Core Code Implementation

Complete multi-level cache implementation (simplified):Complete multi-level cache implementation (simplified):

javascript
const caffeine = require('caffeine') // L1: Local cache (30-second TTL) const localCache = new caffeine.Cache({ max: 1000, ttl: 30, }) // Get product details (multi-level cache) async function getProduct(productId) { const cacheKey = `product:${productId}` // L1: Local cache (~0.1 ms) const localCached = localCache.get(cacheKey) if (localCached) { console.log('L1 hit') return localCached } // L2: Redis cache (~1 ms) const redisCached = await redis.get(cacheKey) if (redisCached) { console.log('L2 hit, backfilling L1') const product = JSON.parse(redisCached) localCache.set(cacheKey, product) return product } // L3: Database (~10 ms, with distributed lock to prevent breakdown) const lockKey = `lock:${productId}` const lock = await redis.set(lockKey, '1', 'NX', 'EX', 10) if (lock === 'OK') { console.log('L3 hit, querying database') const product = await db.query( 'SELECT * FROM products WHERE id = ?', [productId] ) if (product) { // Write to Redis (30 minutes + random TTL) const ttl = 1800 + Math.floor(Math.random() * 600) - 300 await redis.setex(cacheKey, ttl, JSON.stringify(product)) // Backfill local cache localCache.set(cacheKey, product) } await redis.del(lockKey) return product } else { // Lock not acquired โ€” wait and retry await new Promise(resolve => setTimeout(resolve, 50)) return getProduct(productId) } } // Update product info (update DB first, then delete cache) async function updateProduct(productId, updateData) { const cacheKey = `product:${productId}` // 1. Update database await db.query( 'UPDATE products SET name = ?, price = ? WHERE id = ?', [updateData.name, updateData.price, productId] ) // 2. Delete local cache localCache.del(cacheKey) // 3. Delete Redis cache await redis.del(cacheKey) console.log('Update complete, cache deleted') }

๐Ÿ‘‡ Try it yourself:๐Ÿ‘‡ Try it yourself:

The demo below shows the complete workflow of a multi-level caching system. Click "Query Product" and observe how requests flow through the cache tiers:The demo below shows the complete workflow of a multi-level caching system. Click "Query Product" and observe how requests flow through the cache tiers:

------

7. Summary and Learning Path7. Summary and Learning Path

7.1 Core Knowledge Review7.1 Core Knowledge Review

ConceptOne-Line ExplanationProblem SolvedPractical Tip
Cache HitData found in the cache10โ€“100ร— performance improvementTarget hit ratio > 95%
Cache PenetrationQuerying non-existent data hits the DB every timeDatabase dragged down by malicious queriesBloom filter + cache null objects
Cache BreakdownHot data expires, massive requests hit the DBSudden spike in database pressureMutex lock + logical expiration
Cache AvalancheMass simultaneous data expirationDatabase overwhelmedRandom TTL + cache preheating
Multi-Level CacheLocal cache + Redis + DatabaseUltimate performance optimizationL1 local cache 70% hit rate, L2 Redis 25% hit rate
Cache ConsistencyKeeping cache and database in syncData accuracyUpdate DB first, then delete cache
Delayed Double-DeleteDelete cache before and after the updateConsistency in extreme scenariosWait 500ms before the second delete

7.2 Recommended Learning Path7.2 Recommended Learning Path

Phase 1: Understand the Principles (1โ€“2 days)Phase 1: Understand the Principles (1โ€“2 days)

Phase 2: Master the Basics (2โ€“3 days)Phase 2: Master the Basics (2โ€“3 days)

Phase 3: Solve the Classic Problems (1 week)Phase 3: Solve the Classic Problems (1 week)

Phase 4: Multi-Level Caching (1โ€“2 weeks)Phase 4: Multi-Level Caching (1โ€“2 weeks)

Phase 5: Production-Grade Practice (ongoing)Phase 5: Production-Grade Practice (ongoing)

๐Ÿ“– Konsep Penting๐Ÿ“– Core Concept

Caching is the cornerstone of high-concurrency systems. From Taobao's product detail pages to Weibo's trending topics, from WeChat Moments to Douyin's video feeds โ€” every high-performance system has a carefully designed caching architecture behind it. Understanding caching isn't just about learning a technology โ€” it's about grasping the architectural mindset of trading space for time and using copies to protect the primary data. When you truly master caching, your system performance will leap from "barely working" to "good," and ultimately to "excellent." I hope this article helps you build a complete understanding of caching systems. When you encounter performance problems in real projects, you'll be able to ask yourself: "Can caching solve this?"Caching is the cornerstone of high-concurrency systems. From Taobao's product detail pages to Weibo's trending topics, from WeChat Moments to Douyin's video feeds โ€” every high-performance system has a carefully designed caching architecture behind it. Understanding caching isn't just about learning a technology โ€” it's about grasping the architectural mindset of trading space for time and using copies to protect the primary data. When you truly master caching, your system performance will leap from "barely working" to "good," and ultimately to "excellent." I hope this article helps you build a complete understanding of caching systems. When you encounter performance problems in real projects, you'll be able to ask yourself: "Can caching solve this?"