Caching is one aspect of web development I am guilty of overlooking a lot and I am sure a lot us are guilty of that too. I have come to realize how important it is and I will explain the importance with techalyst as a case study.
From experience and a little observation, Techalyst schedules articles daily. Therefore, articles are not (for now) released within 24 hours of the last post. It follows that data on the landing page will remain the same for 24 hours. The main point here is, it is pointless to ask the database for articles within that 24 (or to be safe 22 - 23) hour range.
Cache to the rescue! In Laravel, we can cache the results for 22 hours and when a request is made, the controller responds with a cached value until the cache time expires.
We're going to look at the basic usage of the Laravel cache and then get into a quick demo app to see just how much faster caching can make our applications.
Basic Usage of Cache in Laravel
Laravel makes it easy for us to switch out how we want caching to be generated. It is extremely easy for us to switch out the drivers. Just check out config/cache.php
to see the available drivers which are:
- apc
- array
- database
- file
- memcached
- redis
You can then set the following line in your .env
file to change the cache driver:
CACHE_DRIVER=file
You may go ahead and try these examples out without bothering about configuration, as it defaults to
file
.
The Cache
facade exposes a lot of static methods to create, update, get, delete, and check for existence of a cache content. Let us explore some of these methods before we build a demo app.
# Create/Update Cache Value
We can add or update cache values with the put()
method. The method accepts 3 necessary arguments:
- a
key
- the
value
- the
expiration time
in minutes
For example:
Cache::put('key', 'value', 10);
The key
is a unique identifier for the cache and will be used to retrieve it when needed.
Furthermore, we can use the remember()
method to automate retrieving and updating a cache. The method first checks for the key
and if it has been created, returns it. Otherwise, it will create a new key
with the value returned from it's closure like so:
Cache::remember('articles', 15, function() {
return Article::all();
});
The value 15
is the number of minutes it will be cached. This way, we don't even have to do a check if a cache has expired. Laravel will do that for us and retrieve or regenerate the cache without us having to explicitly tell it to.
Retrieve Cache Value
The values of a cache can be retrieved using the get()
method which just needs a key
passed as argument to know which cache value to grab:
Cache::get('key');
Check for Existence
Sometimes it is very important to check if a cache key exists before we can retrieve or update it. The has()
method becomes handy:
if (Cache::has('key')){
Cache::get('key');
} else {
Cache::put('key', $values, 10);
}
Removing Cache Value
Cache values can be removed with the forget()
method and passing the key
to it:
Cache::forget('key');
We can also retrieve a cache value and delete it immediately. I like referring to this one as one-time caching:
$articles = Cache::pull('key');
We can also clear the cache even before they expire from the console using:
php artisan cache:clear
# Cache by Example
This is going to be a really simple demo based on my research on the time taken to process requests with or without caches. To get straight to the point, I suggest you set up a Laravel instance on your own and follow along with the tutorial.
Model and Migrations
Create a model named Article
using the command below:
php artisan make:model Article -m
The -m
option will automatically create a migration for us so we need not run a "create migration" command. This single command will create an App/Article.php
and a database/migrations/xxxx_xx_xx_xxxxxx_create_articles_table.php
file.
Update your new migration file to add two table fields:
public function up() {
Schema::create('articles', function (Blueprint $table) {
$table->increments('id');
// add the following
$table->string("title");
$table->string("content");
$table->timestamps();
});
}
Now we can migrate our database using the artisan command:
php artisan migrate
Seeding our Database
Next up is to seed the articles table. In database/seeds/DatabaseSeeder.php
, update the run()
command with:
public function run() {
Model::unguard();
// use the faker library to mock some data
$faker = Faker::create();
// create 30 articles
foreach(range(1, 30) as $index) {
Article::create([
'title' => $faker->sentence(5),
'content' => $faker->paragraph(6)
]);
}
Model::reguard();
}
The Faker
library is included in Laravel to help with quickly generating fake data. We are using the PHP's range()
method to generate 30 fake columns.
Now we can seed our database with the artisan command:
php artisan db:seed
# Creating an Article Controller
Next up, we can create a controller that will process the requests and caching. It will be empty for now:
php artisan make:controller ArticlesController
...then we add a route in the app/Http/routes.php
which will point to the controller's index method:
Route::group(['prefix' => 'api'], function() {
Route::get('articles', 'ArticlesController@index');
});
Now that our database is all set up with sample data, we can finally get to testing.
# Responses Without Cache
Let us see what our conventional controller action methods look like without caching and how long it takes to process the response. In the index()
method, return a resource of articles:
public function index() {
$articles = Articles::all();
return response()->json($articles);
}
You can now run the app and access the url (http://localhost/api/articles) from Postman or in browser as seen below.
Take note of the time taken to complete this request on a local development server.
# Responses with Cache
Let us now try to use caching and see if there will be any significant difference in the time taken to respond with data. Change the index()
method to:
public function index() {
$articles = Cache::remember('articles', 22*60, function() {
return Article::all();
});
return response()->json($articles);
}
Now we are caching the articles using the remember()
method we discussed for 22 hours. Run again and observe the time taken. See my screenshot:
# Results & Recommendation
From my standard development PC, the time taken to produce a response when using cache is less compared to when not as seen in the table:
Without Cache
Server Hits | Time |
---|---|
1st | 4478ms |
2nd | 4232ms |
3rd | 2832ms |
4th | 3428ms |
Avg | 3742ms |
With Cache (File driver)
Server Hits | Time |
---|---|
1st | 4255ms |
2nd | 3182ms |
3rd | 2802ms |
4th | 3626ms |
Avg | 3466ms |
With Cache (Memcached driver)
Server Hits | Time |
---|---|
1st | 3626ms |
2nd | 566ms |
3rd | 1462ms |
4th | 1978ms |
Avg | 1908ms :) |
With Cache (Redis driver)
It is required to install predis/predis via composer
Server Hits | Time |
---|---|
1st | 3549ms |
2nd | 1612ms |
3rd | 920ms |
4th | 575ms |
Avg | 1664ms :) |
Awesome enough right? Two things to note:
- The first hits take much more time even when using cache. This is because the cache is still empty during the first hit.
- Memcached and Redis are way faster compared to file. It is advised we use an external cache driver when our project is large.
# Conclusion
The speed difference might not be so obvious with a file/database driver, but if we use external providers, we will see a lot better performance. It pays to invest in caching.
Be the first one to write a response :(
{{ reply.member.name }} - {{ reply.created_at_human_readable }}