Spring Boot 4 Complete Guide · บทเรียน

แคชถล่ม การทำให้ใช้ไม่ได้ และความสอดคล้อง

ป้องกันคำขอจำนวนมากที่ถาโถมและการอ่านข้อมูลเก่าด้วยการออกแบบคีย์ การแคชแบบมีเงื่อนไข และการโหลดที่ประสานกัน

บทเรียน 4 จาก 413 ขั้นตอน

แคชถล่ม การทำให้ใช้ไม่ได้ และความสอดคล้อง เป็นบทเรียน Spring Boot 4 Complete Guide ฟรีบน CoddyKit นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน Spring Boot 4 Complete Guide และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส Spring Boot 4 Complete Guide มีบทเรียนทั้งหมด 4 บทเรียน

บางส่วนของบทเรียนนี้ยังไม่ได้รับการแปล และแสดงเป็นภาษาอังกฤษ

The Problem: Cache Stampede

A cache stampede (a.k.a. thundering herd or dog-piling) happens when a hot cache entry expires and many concurrent requests all miss at once. Each request then hits the slow backing store (database, remote API) to recompute the same value.

  • One popular key expires
  • 1,000 requests arrive in the same instant
  • All 1,000 see a miss and stampede the database
  • The DB spikes, latency explodes, sometimes it falls over

In this lesson you'll learn to prevent thundering herds and stale reads using key design, conditional caching, and synchronized loads in Spring Boot 4.

Synchronized Loading with sync = true

Spring's @Cacheable supports sync = true. When several threads miss the same key at the same time, only one thread computes the value while the others block and wait for the result. This collapses the herd to a single load per key.

  • Requires a cache manager that supports synchronized loading (Caffeine does)
  • You cannot combine sync = true with multiple cache names, unless, or a custom condition that depends on the return value
@Service
public class ProductService {

    @Cacheable(cacheNames = "products", key = "#id", sync = true)
    public Product findById(Long id) {
        // Only ONE thread runs this per key, even under a stampede
        return loadFromDatabase(id);
    }
}

How sync Collapses the Herd

With Caffeine, sync = true maps to Cache.get(key, mappingFunction), which guarantees the mapping function runs at most once per key for concurrent callers. Here is the core idea in plain Java that an online judge can run.

Notice how many threads request the same key, yet the expensive load happens only once.

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

public class Main {
    static AtomicInteger dbLoads = new AtomicInteger();
    static ConcurrentHashMap<Long, Object> cache = new ConcurrentHashMap<>();

    static String load(Long id) {
        // computeIfAbsent runs the mapping function at most once per key
        return (String) cache.computeIfAbsent(id, k -> {
            dbLoads.incrementAndGet();
            try { Thread.sleep(50); } catch (InterruptedException e) {}
            return "product-" + k;
        });
    }

    public static void main(String[] args) throws Exception {
        ExecutorService pool = Executors.newFixedThreadPool(20);
        CountDownLatch done = new CountDownLatch(1000);
        for (int i = 0; i < 1000; i++) {
            pool.submit(() -> { load(42L); done.countDown(); });
        }
        done.await();
        pool.shutdown();
        System.out.println("DB loads for hot key: " + dbLoads.get());
    }
}

Conditional Caching with unless

Caching the wrong values causes stale or useless entries. Use condition and unless to be precise:

  • condition is evaluated before the method runs (on the arguments)
  • unless is evaluated after the method runs (on the return value), so it can inspect the result

A classic rule: never cache null or empty results, so a transient miss doesn't get pinned in the cache.

@Cacheable(
    cacheNames = "products",
    key = "#id",
    unless = "#result == null")
public Product findById(Long id) {
    return repository.findById(id).orElse(null);
}

Key Design: Stable, Specific, Collision-Free

Bad keys cause both stampedes and wrong reads. Good cache keys are:

  • Stable: the same logical input always maps to the same key
  • Specific: include every input that changes the result (tenant, locale, filters)
  • Collision-free: two different inputs never produce the same key

Avoid relying on default key generation when a method has multiple parameters; build an explicit composite key with SpEL so you control exactly what varies.

@Cacheable(
    cacheNames = "catalog",
    key = "'cat:' + #tenantId + ':' + #locale + ':' + #category",
    sync = true)
public List<Product> listCatalog(String tenantId, String locale, String category) {
    return repository.findByTenantAndCategory(tenantId, category, locale);
}

A Reusable KeyGenerator

When many methods share a key shape, a custom KeyGenerator keeps keys consistent and avoids copy-paste SpEL. Register it as a bean and reference it by name with keyGenerator.

This composes the class name, method name, and arguments into a single stable string, eliminating accidental collisions between methods that share argument types.

@Component("scopedKeyGen")
public class ScopedKeyGenerator implements KeyGenerator {

    @Override
    public Object generate(Object target, Method method, Object... params) {
        StringBuilder sb = new StringBuilder(target.getClass().getSimpleName())
            .append(':').append(method.getName());
        for (Object p : params) {
            sb.append(':').append(p);
        }
        return sb.toString();
    }
}

Invalidation on Writes with @CacheEvict

Stale reads happen when data changes but the cache still holds the old value. Evict on every write path so the next read reloads fresh data.

  • @CacheEvict(key = ...) removes a single entry
  • @CacheEvict(allEntries = true) clears the whole cache region
  • beforeInvocation = true evicts even if the method throws, useful for deletes
@CacheEvict(cacheNames = "products", key = "#product.id")
public Product update(Product product) {
    return repository.save(product);
}

@CacheEvict(cacheNames = "products", key = "#id", beforeInvocation = true)
public void delete(Long id) {
    repository.deleteById(id);
}

Atomic Update with @CachePut

@CachePut always runs the method and then stores the result, refreshing the entry instead of evicting it. This avoids a brief empty window between evict and the next read, which under load could itself trigger a mini-stampede.

Use @CachePut when the write method returns the new authoritative value and the key matches the read key exactly.

@CachePut(cacheNames = "products", key = "#result.id")
public Product create(ProductForm form) {
    Product saved = repository.save(form.toEntity());
    return saved; // freshly written value is placed into the cache
}

Consistency Across Nodes with Redis

Caffeine is a per-instance, in-process cache. With multiple app nodes, an evict on node A does not clear Caffeine on node B, causing stale reads. Solutions:

  • Use a shared distributed cache (Redis) so eviction is visible to all nodes
  • Or run a two-tier (near-cache) setup: Caffeine in front of Redis, with a pub/sub invalidation message to flush local copies

Configure a Redis cache manager with per-cache TTLs to bound staleness even if an eviction message is missed.

@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory cf) {
    RedisCacheConfiguration base = RedisCacheConfiguration.defaultCacheConfig()
        .entryTtl(Duration.ofMinutes(10))
        .disableCachingNullValues();

    Map<String, RedisCacheConfiguration> perCache = Map.of(
        "products", base.entryTtl(Duration.ofMinutes(5)),
        "catalog",  base.entryTtl(Duration.ofMinutes(1)));

    return RedisCacheManager.builder(cf)
        .cacheDefaults(base)
        .withInitialCacheConfigurations(perCache)
        .build();
}

Bounding Staleness with Jittered TTL

If many keys are created together (for example after a deploy or a bulk import), they can also expire together, recreating a synchronized stampede at TTL boundaries. Add a small random jitter to spread expirations out.

The judge-runnable snippet below shows how jitter turns a single sharp expiry spike into a smooth band of expiry times.

import java.util.concurrent.ThreadLocalRandom;

public class Main {
    static long baseTtlMs = 300_000; // 5 minutes

    static long ttlWithJitter() {
        // +/- 10% jitter to de-synchronize expirations
        long jitter = (long) (baseTtlMs * 0.10);
        return baseTtlMs + ThreadLocalRandom.current().nextLong(-jitter, jitter);
    }

    public static void main(String[] args) {
        long min = Long.MAX_VALUE, max = Long.MIN_VALUE;
        for (int i = 0; i < 5; i++) {
            long ttl = ttlWithJitter();
            System.out.println("key " + i + " ttl(ms)=" + ttl);
            min = Math.min(min, ttl);
            max = Math.max(max, ttl);
        }
        System.out.println("spread(ms)=" + (max - min));
    }
}

Putting It Together

A robust caching method combines several of these techniques:

  • sync = true to collapse concurrent misses into one load
  • unless to skip caching empty results that should not be pinned
  • explicit key that includes every input affecting the result
  • jittered TTL on the cache config to avoid synchronized expiry

Pair the read method with @CacheEvict or @CachePut on every write so reads never go stale.

@Service
public class CatalogService {

    @Cacheable(
        cacheNames = "catalog",
        key = "#tenantId + ':' + #category",
        sync = true,
        unless = "#result == null || #result.isEmpty()")
    public List<Product> list(String tenantId, String category) {
        return repository.find(tenantId, category);
    }

    @CacheEvict(cacheNames = "catalog", key = "#tenantId + ':' + #category")
    public void invalidate(String tenantId, String category) {
        // called after any write that changes this slice
    }
}

Quick Check

Test your understanding of stampede prevention in Spring Boot 4.

Recap

You learned how to prevent thundering herds and stale reads:

  • Stampede: a hot key expires and many requests recompute the same value at once
  • sync = true: collapses concurrent misses into a single synchronized load per key
  • condition / unless: cache precisely; never pin null or empty results
  • Key design: stable, specific, collision-free keys including every input that changes the result
  • @CacheEvict / @CachePut: invalidate or refresh on every write to avoid stale reads
  • Distributed consistency: Redis or pub/sub invalidation keeps multiple nodes coherent; per-cache TTLs bound staleness
  • Jittered TTL: de-synchronizes expirations so keys don't all expire together
เริ่มต้นได้ฟรี

เรียนรู้ Java ด้วย AI tutor — ฟรี

เขียนและเรียกใช้โค้ดจริงในเบราว์เซอร์ของคุณ รับความช่วยเหลือทันทีจาก AI tutor 24/7 และเรียนรู้ต่อจากที่คุณหยุดบนเว็บหรือในแอป

คอร์ส
21
บทเรียน
84

คำถามที่พบบ่อย

บทเรียน “แคชถล่ม การทำให้ใช้ไม่ได้ และความสอดคล้อง” ฟรีหรือไม่

ใช่ — ข้อความเต็มของ “แคชถล่ม การทำให้ใช้ไม่ได้ และความสอดคล้อง” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส Spring Boot 4 Complete Guide ให้อัปเกรดเป็น CoddyKit PRO คอร์ส Spring Boot 4 Complete Guide มีบทเรียนทั้งหมด 4 บทเรียน

คุณจะเรียนรู้อะไรในบทเรียน “แคชถล่ม การทำให้ใช้ไม่ได้ และความสอดคล้อง”

ป้องกันคำขอจำนวนมากที่ถาโถมและการอ่านข้อมูลเก่าด้วยการออกแบบคีย์ การแคชแบบมีเงื่อนไข และการโหลดที่ประสานกัน คุณปฏิบัติ Spring Boot 4 Complete Guide ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน

คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน Spring Boot 4 Complete Guide หรือไม่

ไม่จำเป็นต้องมีประสบการณ์มาก่อน Spring Boot 4 Complete Guide บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน

บทเรียน “แคชถล่ม การทำให้ใช้ไม่ได้ และความสอดคล้อง” ใช้เวลานานแค่ไหน

บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย

ฉันเขียนและรันโค้ดในบทเรียน Spring Boot 4 Complete Guide นี้ได้ไหม

ได้ บทเรียน Spring Boot 4 Complete Guide ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

บทเรียนทั้งหมดในหลักสูตรนี้

  1. พื้นฐานนามธรรมแคชของ Spring
  2. การแคชในหน่วยความจำด้วยการปรับ Caffeine
  3. การแคชแบบกระจายด้วย Redis และ TTL
  4. แคชถล่ม การทำให้ใช้ไม่ได้ และความสอดคล้อง
← กลับไปที่ Spring Boot 4 Complete Guide