Laravel Queues in Action (2nd edition) is now available!

Introduction to Redis

Updated: Jan 5, 2019 — 7 min Read#redis

Redis is a storage server that persists your data in memory which makes read & write operations very fast, you can also configure it to store the data on disk occasionally, replicate to secondary nodes, and automatically split the data across multiple nodes.

That said, you might want to use Redis when fast access to your data is necessary (caching, live analytics, queue system, etc...), however you'll need another data storage for when the data is too large to fit in memory or you don't really need to have fast access, so a combination of Redis & another relational or non-relational database gives you the power to build large scale applications where you can efficiently store large data but also provide a way to read portions of the data very fast.

Use Case

Think of a cloud-based Point Of Sale application for restaurants, as owner it's extremely important to be able to monitor different statistics related to sales, inventory, performance of different branches, and many other metrics. Let's focus on one particular metric which is Product Sales, as a developer you want to build a dashboard where the restaurant owner can see live updates on what products have more sales throughout the day.

select SUM(order_products.price), products.name
from order_products
join products on order_products.product_id = products.id
where DATE(order_products.created_at) = CURDATE()
where order_products.status = "done"

The sql query above will bring you a list with the sales each product made throughout the day, but it's a relatively heavy query to run when the restaurant serves thousands of orders every day, simply put you can't run this query in a live-updates dashboard that pulls for updates every 60 seconds or something, it'd be cool if you can cache the product sales every time an order is served and be able to read from the cache, something like:

Event::listen('newOrder', function ($order) {
    $order->products->each(function($product){
        SomeStorage::increment("product:{$product->id}:sales:2017-05-22", $product->sales);
    );
});

So now on every new order we'll increment the sales for each product, and we can simply read these numbers later like:

$sales = Product::all()->map(function($product){
    return SomeStorage::get("product:{$product->id}:sales:2017-05-22");
});

Replace SomeStorage with Redis and now you have today's product sales living in your server's memory, and it's super fast to read from memory, so you don't have to run that large query every time you need to update the analytics numbers in the live dashboard.

Another Use Case

Now you want to know the number of unique visitors opening your website every day, we might store it in SQL having a table with user_id & date fields, and later on we can just run a query like:

select COUNT(Distinct user_id) as count from unique_visits where date = "2017-05-22"

So we just have to add a record in that database table every time a user visits the site. But still on a high traffic website adding this extra DB interaction might not be the best idea, wouldn't it be cool if we can just do:

SomeStorage::addUnique('unique_visits:2017-05-22', $user->id);

And later on we can do:

SomeStorage::count('unique_visits:2017-05-22');

Storing this in memory is fast and reading the stored numbers from memory is super fast, that's exactly what we need, so I hope by now you got a feel on when using Redis might be a good idea. And by the way the method names used in the above examples don't exist in redis, I'm just trying to give you a feel about what you can achieve.

The Atomic nature of redis operations

Individual commands in Redis are guaranteed to be atomic, that means nothing will change while executing a command, for example:

$monthSales = Redis::getSet('monthSales', 0);

This command gets the value of the monthSales key and then sets it to zero, it's guaranteed that no other client can change the value or maybe rename the key between the get and set operations, that's due to the single threaded nature of Redis in which a single system process serves all clients at the same time but can only perform 1 operation at a time, it's similar to how you can listen to multiple client alterations on the project at the same time but can only work on 1 alteration at a given moment.

There's also a way to guarantee the atomicity of a group of commands using transactions, more on that later, but briefly let's say you have 2 clients:

Client 1 wants to increment the value
Client 1 wants to read that value
Client 2 wants to increment the value

These commands might run in the following order:

Client 1: Increment value
Client 2: Increment value
Client 1: read value

Which will result the read operation from Client 1 to give unexpected results since the value was altered in the middle, that's when a transaction makes sense.

Redis Commands

Let's store our product sales in a key:

Redis::set('product:1:sales', 1000)
Redis::set('product:1:count', 10)

Now to read it we use:

Redis::get('product:1:sales')

Incrementing and Decrementing counters

A new purchase was made, let's increment the sales:

Redis::incrby('product:1:sales', 100)

Redis::incr('product:1:count')

Here we increment the sales key by 100, and increment the count key by 1.

We can also decrement in the same way:

Redis::decrby('product:1:sales', 100)

Redis::decr('product:1:count')

But when it comes to dealing with floating point numbers we need to use a special command:

Redis::incrbyfloat('product:1:sales', 15.5)

Redis::incrbyfloat('product:1:sales', - 30.2)

There's no decrbyfloat command, but we can pass a negative value to the incrbyfloatcommand to have the same effect.

The incrby, incr, decrby, decr, and incrbyfloat return the value after the operation as a response

Retrieve and update

Now we want to read the latest sales number and reset the counters to zero, maybe we do that at the end of each day:

$value = Redis::getset('product:1:sales', 0)

Here $value will hold the 1000 value, if we read the value of that key after this operation it'll be 0.

Keys Expiration

Let's say we want to send a notification to the owner when inventory is low, but we only want to send that notification once every 1 hour instead of sending it every time a new purchase is made, so maybe we set a flag once and only send the notification when that flag doesn't exist:

Redis::set('user:1:notified', 1, 'EX', 3600);

Now this key will expire after 3600 seconds (1 hour), we can check if the key exists before attempting to set it and send the notification:

if(Redis::get('user:1:notified')){
    return;
}

Redis::set('user:1:notified', 1, 'EX', 3600);

Notifications::send();
Notice: There's no guarantee that the value of user:1:notified won't change between the get and set operations, we'll discuss atomic command groups later, but this example is enough for you to understand how every individual command works.

We can set the expiration of a key in milliseconds as well using:

Redis::set('user:1:notified', 1, 'PX', 3600);

And you may also use the expire command and provide the timeout in seconds:

Redis::expire('user:1:notified', 3600);

Or in milliseconds:

Redis::pexpire('user:1:notified', 3600);

And if you want the keys to expire at a specific time you can use expireat and provide a Unix timestamp:

Redis::expireat('user:1:notified', '1495469730')

Is there a way I can check when a key should expire?

You can use the ttl command (Time To Live), which will return the number of seconds remaining until the key expires.

Redis::ttl('user:1:notified');

That command may return -2 if the key doesn't exist, or -1 if the key has no expiration set.

You can also use the pttl command to get the TTL in milliseconds.

What if I want to cancel expiration?

Redis::persist('user:1:notified');

This will remove the expiration from your key, it'll return 1 if OK or 0 if key doesn't exist or originally had no expiration set.

Keys Existence

Let's say there's only 1 laracon ticket available and we need to close purchasing once that ticket is sold, we can do:

Redis::set('ticket:sold', $user->id, 'NX')

This will only set the key if it doesn't exist, the next script that tries to set the key wll receive null as a response from Redis which means that the key wasn't set.

You can also instruct Redis to set the key only if it exists:

Redis::set('ticket:sold', $user->id, 'XX')

If you want to simply check if a key exists, you can use the exists command:

Redis::exists('ticket:sold')

Reading multiple keys in one go

Sometimes you might need to read multiple keys in one go, you can do this:

Redis::mget('product:1:sales', 'product:2:sales', 'non_existing_key')

The response of this command is an array with the same size of the given keys, if a key doesn't exist its value is going to be null.

As we discussed before, Redis executes individual command atomically, that means nothing can change the value of any in the keys once the operation started, so it's guaranteed that the values returned are not altered in between reading the value of the first key and the last key.

Using mget is better that firing multiple get commands to reduce the RTT (round trip time), which is the time each individual command takes to travel from client to the server and then carry the response back to the client. More on that later.

Deleting Keys

You can also delete multiple keys at once using the del command:

Redis::del('previous:sales', 'previous:return');

Renaming Keys

You can rename a key using the rename command:

Redis::rename('current:sales', 'previous:sales');

An error is returned if the original key doesn't exist, and it overrides the second key if it already exists.

Renaming keys is usually a fast operation unless a key with the desired name exists, in that case Redis will try to delete that existing key first before renaming this one, deleting a key that holds a very big value might be a bit slow.

So I have to check first if the second key exists to prevent overriding?

Yeah you can use exists to check if the second key exists... OR:

Redis::renamenx('current:sales', 'previous:sales');

This will check first if the second key exists, if yes it just returns 0 without doing anything, so it only renames the key if the second key does not exist.

Hey! 👋 If you find this content useful, consider sponsoring me on GitHub.

You can also follow me on Twitter, I regularly post about all things Laravel including my latest video tutorials and blog posts.

By Mohamed Said

Hello! I'm a former Laravel core team member & VP of Engineering at Foodics. In this publication, I share everything I know about Laravel's core, packages, and tools.

You can find me on Twitter and Github.

This site was built using Wink. Follow the RSS Feed.