Laravel Cache

Laravel Cache: How To Speed Up Your Load Time

Spread the love

Caching plays a vital role in optimizing the performance of a web application. According to this article, google engineers discovered that 400ms is long enough to cause users to leave a site. That’s how crucial load time affects bounce rates currently. Slow load speeds could affect conversion rates in e-commerce sites as Amazon stands to lose 1.6 billion in sales for every one second in load time.

Caching is an essential part of the software development process if you care about the scalability and performance of your application.

What is Laravel Caching?

Laravel provides a robust and easy-to-use implementation of caching and supports multiple caching backends. Through this support, you can switch between caching backends to suit your needs.

laravel cache architecture
Cache Architecture

A cache hit occurs when data is available in cache memory and as a result, the application does not query the database. A cache miss, on the other hand, occurs when data is not present in the cache memory and as a result, the application has to query the database.

Laravel helps use model the same architecture using its inbuilt cache system.

Benefits of Caching

One of the benefits of caching is that it can reduce the workload on your application servers. For example, you can cache database queries, API responses and file contents thereby reducing server overheads.

Another benefit is that you can cache third-party API responses thereby reducing the chances of hitting a throttle limit or monthly request limit. For example, if your application is interacting with a third-party API say OpenWeatherMap API, instead of making a call to the OpenWeatherMap API each time, we can store the response in a cache and update it every 1 hour thereby reducing the number of requests made.

These are some of the benefits of caching.

Tradeoffs of Caching

Although caching has a lot of benefits, there are some drawbacks we need to be aware of. The first drawback is having outdated data in the cache. Let’s imagine we have some record stored in a cache with a time to live of 1 hour. In the event, that there is an update in the records, the cache may not be updated leading to outdated data being used as opposed to fresh data.

Another drawback is that it can be difficult to debug errors. It can be quite hard to detect points of failure in an application because records are stored in the cache. Let’s imagine we have a complex query to the database that does a lot of computations. Because we have a cache in place, we would not know about the complex query computation simply because the results might be cached and at surface level, the application might seem to be okay but deep down there are memory leaks that were shipped to production.

All in all, as much as there are drawbacks, the benefits outweigh the drawbacks and one would benefit massively from implementing caching in their application.

Configuration and Setup

Laravel comes already pre-configured to handle caching. It supports popular caching services such as Memcached, Redis, DynamoDB and the default relational database. Data can also be stored in a file.

Since Laravel supports multiple cache services, it uses drivers to determine which cache service is in use. Some of the drivers are database, redis, memcached , database, dynamodb, file and array. Depending on your stack needs, you can use any of the drivers to suit your needs. For me, I use file driver in development and redis driver in production

Using Laravel Cache Commands

Laravel provides a Cache Facade that enables us to interact with our cache system. We can use the methods available in the Facade to add and remove data from the cache easily.

use Illuminate\Support\Facades\Cache;

Cache Methods

Cache::remember()

This method helps us retrieve data from the cache if it is present in the cache or store it if it is not present. It requires 3 parameters; the cache key, ttl(time to live) and a callback function. Cache Key is a unique string that will be used to retrieve data from the cache. TTL is the amount of time in seconds the data will be stored in the cache. The Callback function is a function that will be invoked in the case the cache is empty.

use Illuminate\Support\Facades\Cache;

Cache::remember('categories', now()->addHour(), function () {
    return Category::with('subcategories')->get();
});

In this case, we have a cache key of products, TTL of 1 hour(3600 seconds) and a callback function that queries the database in the case of a cache miss. A cache miss occurs when data is not present in the cache memory.

Cache::get()

This method retrieves items from the cache memory. In the event of a Cache miss, it returns null as the value.

use Illuminate\Support\Facades\Cache;

$categories= Cache::get('categories');
 
$categories= Cache::get('categories', 'default');

Cache::forget()

This method helps remove items from the cache. This is extremely useful when we want to update the cache records with new data.

use Illuminate\Support\Facades\Cache;

Cache::forget('categories');

Cache::has()

This method determines if an item exists in the cache. It returns true if it exists and false otherwise.

use Illuminate\Support\Facades\Cache;

if(Cache::has('categories')){
  //execute some code
}

Cache::put()

This method is used to store data in the cache memory.

use Illuminate\Support\Facades\Cache;

Cache::put('key', 'value', $seconds = 15);

Cache::put('key', 'value');

These are some of the useful Cache methods you will use in your day-to-day development. There are other methods present which you can find them here.

Using Laravel Cache Commands in Production

Now that we know the basic commands, we can use them in our production codebase. When an application is deployed to production, there are numerous things we need to factor in. The first one is speed. It is known that bad SQL query designs can cause memory leaks and lead to slow applications. One way we can avoid such issues is to introduce a cache system.

The Cache would sit in between our Laravel application and the database and would return data in the case of a cache hit or retrieve the data from the database and store it in memory in the case of a cache miss.

In Laravel, we can use the remember method to handle this for us. It will return cached data if it is present and store it in memory by retrieving the data from the database. This way, we are able to improve the speed of the application by reducing the number of queries made to the database.

The second thing we need to factor in is efficiency. If we are writing a REST API in Laravel, we would want to paginate our responses as we don’t want to return thousands of records in one response.

But how do we deal with paginated data in Cache?

It is actually pretty easy. We can paginate the results being fetched from the database. However, to store them in the cache, we would need to know which page’s results are stored so that when we retrieve the data we can return the results for the correct page.

To do so, we can append the cache key with the page number and thus have the results for the different pages stored differently in memory.

use Illuminate\Support\Facades\Cache;

public function index()
    {
        $currentPage = request()->get('page', 1);
        return Cache::remember('categories' . $currentPage, now()->addHour(), function () {
                return Categories::with('subcategories')->paginate(10);
            })
        );
    }

Update the Cache

One of the challenges in caching is updating the cache. It is challenging because it is hard to know when to update the cache, especially in production. Since we won’t be managing the cache manually, we would need a way to update the cache automatically.

Luckily, Laravel provides a way in which we can update the cache.

Now that we have data in the cache, we would want to update as soon as new data is available. This is where we would use the forget method. This would remove the data for a specific cache key from the memory. In development, this would not be an issue since we can just clear the cache once new data is available. This is not the case in production.

So how do we update the cache at scale?

This is where Model Observers come in handy. Observers listen to events on a given model and execute code based on the triggered events. For example, if a new record is added to the database, we would want the cache to be updated to have the new data.

We can drop the previous data that was in the cache memory and update it at the next get request.

To create an observer, we can use the make:observer Artisan command.

php artisan make:observer CategoriesObserver --model=Category

We then need to register the Observer in the AppServiceProvider boot method in the App\Providers\AppServiceProvider.php file.

use App\Models\Category;
use App\Observers\CategoriesObserver;

public function boot()
    {
        Category::observe(CategoriesObserver::class);
    }

We can now add the Cache::forget() logic in the observer.

<?php

namespace App\Observers;

use App\Models\Category;
use Illuminate\Support\Facades\Cache;


class CategoriesObserver
{
    /**
     * Handle the Category "created" event.
     *
     * @param  \App\Models\Category  $category
     * @return void
     */
    public function created(Category $category)
    {
        Cache::forget('categories');
    }

    /**
     * Handle the Category "updated" event.
     *
     * @param  \App\Models\Category  $category
     * @return void
     */
    public function updated(Category $category)
    {
        Cache::forget('categories');
    }

    /**
     * Handle the Category "deleted" event.
     *
     * @param  \App\Models\Category  $category
     * @return void
     */
    public function deleted(Category $category)
    {
        Cache::forget('categories');
    }
}

This will remove the data based on certain events on a Model.

While this is a simple solution for a small application, it is definitely not a good one for a production-ready application. This is because, in production, you probably have paginated results. This is because the data stored in the cache contains the page number as part of the cache key.

So how do we update the cache with paginated results?

In this case, we would need to write a for loop that would remove all the data for all the cache keys for a model. I would first write a generic function in a trait through which I can use it anywhere in my codebase. Traits help us reuse our code in multiple classes. I will create a Traits folder in the app directory and create a ClearCache trait in the App\Traits folder

<?php

namespace App\Traits;

use Illuminate\Support\Facades\Cache;

trait ClearCache
{

    /**
     * This function is responsible for clearing the cache when a specific cache key is passed.
     * It is useful for paginated Resources that are cached
     * @param int $key The cache key index
     */
    public function clearCache($key)
    {
        for ($i = 1; $i <= 5000; $i++) {
            $newKey = $key . $i;
            if (Cache::has($newKey)) {
                Cache::forget($newKey);
            } else {
                break;
            }
        }
    }
}

With this, you are able to clear the cache and prepare the cache to be ready to store new data once it is available.

Now that we have the trait ready, we can update our Category Observer to use the trait.

<?php

namespace App\Observers;

use App\Models\Category;
use App\Traits\ClearCache;
use Illuminate\Support\Facades\Cache;


class CategoriesObserver
{
    use ClearCache;
    /**
     * Handle the Category "created" event.
     *
     * @param  \App\Models\Category  $category
     * @return void
     */
    public function created(Category $category)
    {
        $this->clearCache('categories');
    }

    /**
     * Handle the Category "updated" event.
     *
     * @param  \App\Models\Category  $category
     * @return void
     */
    public function updated(Category $category)
    {
        $this->clearCache('categories');
    }

    /**
     * Handle the Category "deleted" event.
     *
     * @param  \App\Models\Category  $category
     * @return void
     */
    public function deleted(Category $category)
    {
        $this->clearCache('categories');
    }
}

Observers listen to events that happen to one record only. For example, the updated method only executes if one record is updated. While this is good by design, there may exist some cases where you are executing updates on multiple records at the same time. In this case, the updated event would not fire and thus the cache would not be updated.

So how do we update the cache when multiple records are updated in the database?

In this case, we would need to just clear the cache manually. We can add the clearcache method from the trait in a controller after records have been updated. Though I would advise against it, this could be a workaround for that particular scenario.

Clear the Cache

To clear the cache, we can use the cache:cache Artisan command.

php artisan cache:clear

After implementing caching in my application, this was the major improvement I saw in my API requests.

laravel cache
Before Caching
laravel cache
After Caching

Conclusion

Caching helps improve the performance of an application. It does come with its fair share of challenges but the challenges are worth the effort as this can skyrocket your application to greater heights. I hope this article was able to shed some light on how to implement Laravel cache and some of the best practices. If you want to improve your application’s performance, you can read this article on Laravel Scheduling. Thank you for reading.

Similar Posts

8 Comments

  1. Note: this is not supported by file, dynamodb and database driver.

    Nice article, although clearing cache for paginated results can be done more effective by using:
    Cache::tags(‘categories’)->remember(‘categories’ . $currentPage, now()->addHour(), function () {
    return Categories::with(‘subcategories’)->paginate(10);
    });

    And then:
    Cache::tags(‘categories’)->flush();

Leave a Reply

Your email address will not be published.