DEV Community

Reece M
Reece M

Posted on

How to Separate Redis Cache for Tenants

One problem you may face when storing data into your cache is that you may have pollution of data between different apps that touch the same cache store in Laravel, I know as it has happened before and its a pain to debug.

In Redis servers there is the option to select 1 of 16 different databases within the same redis instance. This tutorial will also work on Laravel 6.x or 5.x

This is quite useful and awesome as you are able to split information incase they have the same keys or, if you want to split your main application from a trial versions data.

Laravel though when it is running normally only allows one redistribution database when connecting. This is fine, unless you want to change it during run time. No, here comes the question, how?

The use of the word 'trial' in this article is just because it is a nice way to reference a reason for switching the database, it could be anything or reason you want. But for the purpose of understanding it is 'trial' :)

Right, let's begin

Initial configuration

We will need to create a new class that extends the Laravel Illuminate\Cache\CacheManager. This is the only way I found to get this to work in a way that can be modified further on for other things.

Also it is easier as the createRedisDriver() function is protected, so it isn't easily extended.

Now you can make the class anywhere you want, for example purposes I am going to place this class in an Extensions folder, if you have preferred place to stick a class you may put it there.

Create the file: app\Extensions\CacheManager.php.

Place the following code in as your class:


<?php

namespace App\Extensions;

use Illuminate\Cache\CacheManager as BaseCacheManager;

class CacheManager extends BaseCacheManager
{
    //
}
Enter fullscreen mode Exit fullscreen mode

Good, now we have the base class that extends the Laravel CacheManager as BaseCacheManager.

Configure your choice of the redis database.

For this example, we are going to use a configured setting in the config\database.php file as that way we could have the Redis instance on a separate server. (Another way is explained at the end.)

You can place a duplicated entry of the default redis connection from the database file, something similar to the following :

    'trial' => [
        'host' => env('REDIS_HOST', '127.0.0.1'),
        'password' => env('REDIS_PASSWORD', null),
        'port' => env('REDIS_PORT', 6379),
        'database' => env('REDIS_TRIAL_DB', 3), // <- note this line here.
    ],
Enter fullscreen mode Exit fullscreen mode

You will notice that the above is very similar to the default one, except the 'database' one, this has a separate env variable and also a different to the default in the redis connection.

This is what changes the connection to a different database inside the same redis instance.

Changing the Connection Dynamically

Here is the fun and kinda simple part.

Inside the class that we made we are going to override the parent function createRedisDriver(), add some extra logic and then hand off the rest of the work back to the base CacheManager class.

Our function:

    /**
     * Create an instance of the Redis cache driver.
     *
     * @param  array  $config
     * @return \Illuminate\Cache\Repository
     */
    protected function createRedisDriver(array $config)
    {

        if ($someThingThatTriggersTheSwitch === true) {
            $config['connection'] = 'trial';
        }

        return parent::createRedisDriver($config);
    }
Enter fullscreen mode Exit fullscreen mode

You probably noticed that there isn't actually a variable called $someThingThatTriggersTheSwitch !? and thinking I'm dumb for putting that there..., but close.

That logic can actually vary depending on what you want to do or how you are determining the reason for switching. We can make a function to handle that, or we can just straight inline it with an auth()->user()->is_trialing check.

We are going to do two similar things. The first will be the easiest for beginners to implement or for a really simple application. The other would be if you have a dedicated method to check for tenants.

Option 1.

Using a column on the users table in the database, we can use the Laravel auth() helper to get our user, and to check if we should apply the cache call.

Lets switch out the $someThingThatTriggersTheSwitch variable to a internal function, lets call it shouldSwitch().

    /**
     * This function will determine if we can change the connection.
     *
     * @return bool
     */
    protected function shouldSwitch(): bool
    {
        if (! auth()->check()) {
            return false;
        }

        return auth()->user()->is_trialing == true;
    }
Enter fullscreen mode Exit fullscreen mode

Because there is the possibility that you may call the cache when there are guest users, we will first check if the user is authorized, if not just return false and use the default otherwise apply our logic check (very simple in this case).

Your if statement should look something like if ($this->shouldSwitch() === true).

Option 2 (If you have a custom tenant class)

Our method will be similar to the above style of using the shouldSwitch() function. The only difference is that we will be looking at a different source of truth if we should be switching the connection.

For example, if you have a tenant() helper or Facade that has some method of checking for tenants, you can have a function similar to the below method:

    /**
     * This function will determine if we can change the connection.
     *
     * @return bool
     */
    protected function shouldSwitch(): bool
    {
        return Tenant::isTrialing();
    }
Enter fullscreen mode Exit fullscreen mode

That is relatively painless too.

If you want to look at some resources for multiple tenants and checking for them, I was able to get one instance running by using some info from Ollie Read.

Adding our dynamic Cache to the ServiceProvider

We need to let our application know that we have a new CacheManager for it to use.

So go ahead and open up your AppServiceProvider.php class.

Inside the boot() function of the provider we will extend the Laravel cache instance and override the CacheManager with our own one.

It isn't in the register() function as you cannot access the user in the auth() helper as they don't exist ... 🤷‍♂️ they somewhere else at that time.

        $this->app->extend('cache', function () {
            return new \App\Extensions\CacheManager($this->app);
        });
Enter fullscreen mode Exit fullscreen mode

This overrides the already registered cache instance inside the Laravel container with our extended CacheManager class. Not that much happening.

Checking that it works.

Spin up your application. And also open up your terminal and start the redid-cli.

This part I can't really tell you what to do, but cache something as your regular user, then cache something again as the trial/ or other type of user

Once you have cached something you can see that if it did what its supposed to do from the redis cli.

From your terminal you run redis-cli, if you have it installed on a local machine or your test server.

Check the normal Laravel redis database

The normal one is database 0, in the terminal type SELECT 0 and press return (didn't say hit...)

You can then type key * and get a list of all things cached, you should see the cache key you used near the end.

Checking your 'trial' redis database

IF you left the database stuff default and didn't define a env variable the database will be 3. So in the terminal type SELECT 3, this will switch to that database.

Type key * again. You may or may not see very few entries, but one of 'em will be the new key you entered as a 'trial' user.


The extra way you could split the data.

Mentioned earlier is that you can change the way of switching other than the connection.

You can edit the prefix. In the CacheManager of laravel, it checks the actual $config variable for a prefix, this means we can override our default prefix defined in the cache config.

In our function createRedisDriver() we will change the line $config['connection'] = 'trial'; to the following:

$config['prefix'] = 'trial_cache:';

This means that we won't actually change the connection, but merely the prefix that is used when caching the data. This means that it will all be in the same redis database but with a different prefix to the cache key.

That's all 👋

Thanks for reading, leave any comments or questions. And suggestions if I can improve this article.

(A lesson learned from this: autocorrect doesn't like the word redis)

Buy Me A Coffee

Discussion (0)