雷群问题:防止踩踏
Thundering herd problem: Preventing the stampede

原始链接: https://distributed-computing-musings.com/2025/08/thundering-herd-problem-preventing-the-stampede/

## 缓存雪崩问题及解决方案 “缓存雪崩”问题发生在缓存未命中频繁请求的条目时,导致大量并发请求同时击中数据库。虽然缓存旨在*减少*数据库负载,但这种情况下实际上可能*增加*负载,从而抵消了缓存的好处。 本文通过使用Postgres和Redis的Spring Boot应用程序,并采用旁路缓存模式,演示了这个问题。对相同缺失产品ID的并发请求导致了多次数据库查询和缓存回填,可以通过Zipkin追踪观察到。 提出了两种解决方案: 1. **分布式锁(使用Redis):** 在访问数据库之前获取锁。只有一条请求可以从数据库获取数据,其他请求则重试。这确保了每个键只有一个数据库调用,即使在多个节点上也是如此,但会引入网络开销。 2. **进程内同步(使用`CompletableFuture` & `ConcurrentHashMap`):** 通过使用进程内同步避免网络调用。`CompletableFuture`确保只有一条请求执行数据库查询和缓存更新。但是,此解决方案无法在多个应用程序节点之间协调,如果请求分散,可能会导致冗余查询。 作者的代码示例(可在GitHub上找到)说明了这两种方法,展示了它们如何缓解缓存雪崩问题并减少数据库负载。两者之间的选择取决于应用程序的架构和可扩展性需求。缓存并非万无一失的解决方案,理解这些边缘情况对于构建可扩展的应用程序至关重要。

相关文章

原文
elephants on road
public Product getProductById(UUID id) throws ProductNotFoundException {
    String cacheKey = PRODUCT_CACHE_KEY_PREFIX + id;

    // Check if product is in cache
    Product productFromCache = redisTemplate.opsForValue().get(cacheKey);

    // If product is in cache, return it
    if (productFromCache != null) {
        return productFromCache;
    }

    // If the product is not in cache, fetch it from DB
    Product product = productRepository.findById(id)
            .orElseThrow(() -> new ProductNotFoundException(id));

    // Backfill the cache
    redisTemplate.opsForValue().set(cacheKey, product, CACHE_TTL);

    return product;
}
public Product getProductById(UUID id) throws ProductNotFoundException {
    String cacheKey = PRODUCT_CACHE_KEY_PREFIX + id;
    // Check if product is in cache
    Product productFromCache = redisTemplate.opsForValue().get(cacheKey);

    // If product is in cache, return it
    if (productFromCache != null) {
        return productFromCache;
    }

    // If the product is not in the cache, then acquire a lock over
    // the cache key
    String lockKey = cacheKey + ":lock";
    String lockValue = UUID.randomUUID().toString();
    Duration lockTtl = Duration.ofSeconds(10);
    Boolean lockAcquired = stringRedisTemplate.opsForValue()
        .setIfAbsent(lockKey, lockValue, lockTtl);
    if (Boolean.TRUE.equals(lockAcquired)) {
        // This is required to avoid a race condition where another thread
        // could acquire the lock and backfills the cache in between the
        // current thread checks the cache and acquires the lock.
        Product doubleCacheLookup = redisTemplate.opsForValue().get(cacheKey);
        if (doubleCacheLookup != null) {
            return doubleCacheLookup;
        }
        try {
            // Look up the product from the database
            Product product = productRepository.findById(id)
                    .orElseThrow(() -> new ProductNotFoundException(id));

            // Backfill the cache
            redisTemplate.opsForValue().set(cacheKey, product, CACHE_TTL);

            return product;
        } finally {
            releaseLock(lockKey, lockValue);
        }
    } else {
        // If the lock was not acquired, wait for the cache to be populated and
        // then return the product from the cache
        return waitAndRetryFromCache(cacheKey, id);
    }
}

In-process synchronization

public Product getProductById(UUID id) throws ProductNotFoundException {
    String cacheKey = PRODUCT_CACHE_KEY_PREFIX + id;
    // Check if product is in cache
    Product productFromCache = redisTemplate.opsForValue().get(cacheKey);

    // If product is in cache, return it
    if (productFromCache != null) {
        return productFromCache;
    }

    // If not found in the cache, perform a database lookup and backfill the cache
    CompletableFuture<Product> future = ongoingRequests.computeIfAbsent(id,
        productId -> CompletableFuture.supplyAsync(() -> {
            try {
                // Look up the product from the database
                Product product = productRepository.findById(id)
                        .orElseThrow(() -> new ProductNotFoundException(id));

                // Backfill the cache
                redisTemplate.opsForValue().set(cacheKey, product, CACHE_TTL);
                return product;
            } finally {
                ongoingRequests.remove(productId);
            }
        }));
    try {
        return future.get();
    } catch (ExecutionException e) {
      // Handle exception
    }
}

联系我们 contact @ memedata.com