For large-scale web applications handling thousands of requests per hour (or more), one of the main bottlenecks in performance is often on the database layer. Whether you are using MySQL, PostgreSQL or any other relational or NoSQL database to store your data, hitting it with dozens of query per second can become a problem and hurt your application performance.

This is especially true for more complex queries, when you want to retrieve multiple entities of data using joins and multiple conditions or aggregations. The database being unable to answer to all queries at once, resulting timeouts can lead to a snowball effect that causes your application to not respond properly, and, in worst case scenarios, to just stop working at all, waking you up at 2 in the morning with some SMS alert.

That's why today caching is an essential part of any application that needs to handle an important amount of traffic, and can even be considered as a completely independent part of the engineering and design process. By caching data that needs to be accessed frequently, you reduce the overhead on the database, and instead use a more read-efficient data storage to retrieve the data from.

Today, Redis and memcached are ones of the most used database cache softwares used by all the big actors of the industry, and when used properly, can handle millions of data operations per second. Reddit, one of the biggest websites out there in terms of traffic, use multiple memcached nodes with a complex caching strategy, and wrote a very interesting blog post about it.

Redis is also a very powerful data store that can hold simple key-values pairs, or more complex data structures like lists and sets. Usually running on a separate machine, application servers connect to it remotely, and use commands to send and fetch data from the main store.

APC, on the other end, is a very simple key-value cache, specific to the PHP language, and executed on the same machine that runs the PHP code. Being compiled and optimized to work so closely to the PHP runtime makes it the fastest data cache for a PHP app.

In this post we're going to implement a very simple multi-layer caching system for a PHP application. Multi-layer means that data retrieved from the main database (let's say, MySQL) will be first cached into the global Redis datastore, and also into the local APC cache. By using this strategy, we:

  • Make data retrieval faster, since local cache will be queryied before the global one, avoiding a network transaction.
  • Keep mutliple layers of cache that can behave differently. For exemple, when some data is updated, we can easily replace the stale version with the new one it into the global Redis cache, since there is only one datastore to update. The APC local caches, having a much shorter expiration time (TTL), will regularly update their versions with the Redis one.

To simplify the management of multiple datastore backends, we can use the ICanBoogie Storage library. This library was precisely designed to handle multi-layer caching strategies, as we hold different data stores into a StorageCollection, from the less expensive (APC, in our case) to the most expensive one (the Redis global cache). The nice thing is that the library manages for ourself the task of updating the most local cache when new data has to be retrieved from the upstream cache.

$ composer require icanboogie/storage
use ICanBoogie\Storage\StorageCollection;  
use ICanBoogie\Storage\RedisStorage;  
use ICanBoogie\Storage\APCStorage;

$redis = new Redis();

$prefix = "my_cache::";

$cache = new StorageCollection(array(
    new APCStorage($prefix),
    new RedisStorage($redis, $prefix),

We can now use the $cache holding the cache chain to store data retrieved from our main database, and fetch it whenever we need.

$cacheKey = "user::" . $currentUser->getId() . "::followers";

// Try to get data from the cache chain.
// If data from the APC cache is expired,
// the StorageCollection implementation
// will manage to get it from the global cache.

if (null == ($followers = $cache->retrieve($cacheKey))) {  
    // Seems like data from the cache is expired,
    // do the heavy database query to get fresh data
    $followers = $userRepository->getFollowers($currentUser);

    // store the fresh data
    $cache->store($cacheKey, $followers);