Mastering Cache Reads: Prevent Cache Penetration and Expiration in High‑Concurrency Systems

This article explains how to implement robust cache‑read logic with Redis, covering common pitfalls such as cache penetration and cache expiration, and demonstrates practical solutions—including placeholder caching and lock‑based cache rebuilding—to keep high‑traffic back‑ends performant and reliable.

21CTO
21CTO
21CTO
Mastering Cache Reads: Prevent Cache Penetration and Expiration in High‑Concurrency Systems

0x00 Introduction

Large‑scale architecture designs often focus on lofty concepts, but the real strength of a system lies in careful detail work. This series begins with caching, a cornerstone of high‑concurrency systems, and examines subtle issues that many developers overlook.

0x01 Read Flow

The typical read‑through flow using Redis looks like this:

public function getData($key)
{
    // get cache
    $data = $redis->get($key);

    // cache hit, return directly
    if (!empty($data)) {
        return $data;
    }

    // cache miss, fetch from DB
    $data = getDataFromDb($key);

    // write to cache
    $redis->set($key, $data, $expireTime);

    // return data
    return $data;
}

While this pattern works for most cases, two hidden details need attention.

0x02 Cache Penetration

Problem 1: If the database does not contain the requested key, Redis returns an empty value, causing every request to hit the DB—a situation known as cache penetration.

Solution: Store a placeholder (e.g., an empty array) in the cache and use is_null to distinguish a genuine miss from a cached empty result.

// get cache
$data = $redis->get($key);
if (!is_null($data)) {
    return $data; // cache hit (including empty placeholder)
}

// cache miss, fetch from DB
$data = getDataFromDb($key);
if (empty($data)) {
    $data = [];
}
$redis->set($key, $data, $expireTime);
return $data;

0x03 Cache Expiration

Problem 2: When a cache entry expires, a sudden surge of concurrent requests may all query the DB, similar to cache penetration.

Solution: Embed an expireTime inside the cached value, check it before using the data, and employ a Redis lock (SETNX) so that only one request rebuilds the cache while others serve stale data.

public function getData($key)
{
    $data = $redis->get($key);
    // cache not expired
    if (!is_null($data) && $data['expireTime'] > time()) {
        return $data['data'];
    }

    // try to acquire lock
    if (!$redis->setNx('lock:' . $key, 1, $lockTtl)) {
        // lock not acquired, return stale data
        return $data['data'];
    }

    // fetch fresh data from DB
    $rData = getDataFromDb($key);
    if (empty($rData)) {
        $rData = [];
    }

    // prepare new cache entry with early expiration
    $data = [
        'data' => $rData,
        'expireTime' => $expireTime - 60 // refresh 60 s earlier
    ];
    $redis->set($key, $data, $expireTime);
    return $rData;
}

These techniques effectively prevent cache penetration and reduce database load during cache expiration, ensuring stable performance in high‑concurrency environments.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Backendrediscachinghigh concurrencyPHPcache-penetrationcache expiration
21CTO
Written by

21CTO

21CTO (21CTO.com) offers developers community, training, and services, making it your go‑to learning and service platform.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.