Improving performance through Lua scripts in Redis using TypeScript
How to use Lua scripts in Redis to decrease network overhead.
Performance is a key characteristic of any architecture and in a microservices architecture, network overhead between services can cause performance issues.
In this post, we'll learn how to leverage a Lua script in Redis to avoid unnecessary network overhead and do evaluation and transformation of data within Redis.
We'll walk through an example Lua script and show how it improves network overhead between a microservice and Redis.
What is Lua?
Lua is a powerful, efficient, lightweight, embeddable scripting language. Its lightweight nature makes it highly applicable to embedded systems such as microcontrollers.
Redis can use scripts written in Lua to add functionality and extend its capabilities beyond a key-value store.
Why use Lua scripts?
Lua scripts allow you to evaluate data within Redis and make decisions based on the parameters you pass and the stored data. This means that logic that would have been executed within your microservice could be instead be executed inside Redis.
Usually, you would fetch all the data required for your computation from Redis and then calculate the result. This causes network overhead as you now need to transmit all that data over the network. With a Lua script you could instead do that computation within Redis, where the data is, and then only return the result.
While Lua scripts provide a performance benefit there are downsides to consider such as testability of your logic and since Redis is single-threaded, complex scripts could slow down your instance and block other calls.
You will have to judge if the use-case warrants higher performance or better testability. If you decide to use Lua scripts ensure that they are lightweight and do not cause excessive load on your Redis instance.
Advantages | Disadvantages |
---|---|
Reduced network overhead | Testability of logic |
Complex scripts could block other calls |
Message-based communication in microservices
Before we dive into how to create a Lua script, let's first describe a scenario where using a Lua script would be useful. This will give us a better understanding of how to apply Lua scripts and contextualize them better.
In our scenario, we have a microservice that receives commands via messages on a message bus. The message contains an object car
that we want to perform an operation on as shown below.
message:
{
"messageId": "231ad29d-b230-4c1e-9fb6-61f77385cafc"
"body": {
"car": {
"id": "53a95943-617e-44ea-8e59-ef9982cadaf6",
"userId": "0e7a55db-cb80-4776-b027-5b1ca4d05185",
"hash": "5fad62a7a45fd1414fcd7553bef484585b555b0ff7251b51..."
}
}
}
Our microservice wants to always operate on the latest car
object and therefore needs to verify that the car
received in this message is the latest version. To do this it requests the car
from a different bounded context.
This is an application of the Data Domain Pattern and allows one service to own the data while others can read the data directly, thereby allowing the two services to scale independently while ownership of data is still governed.
There is a case to be made around eventual consistency and whether our microservice should even care if the car
it received is the latest version but that's a different discussion that is out of scope for this article.
To check whether the car
in the message is the latest version of the object we can compare the hash
property of the car in the message with the hash of the car stored in Redis.
In Redis, we store the car object as a Hash
. The key is the car.Id
and the value is a map with a hash
field containing the hash value of the car and a model
field containing a JSON serialized string of the model.
Microservice request flow without Lua script
In the diagram below we get insights into the request flow our microservice needs to go through without using a Lua script.
To understand which pathway will be the most frequent we have to understand how frequently the data is updated and the chances that the object we received in the message will be the latest version.
Our assumption here is that the object is updated relatively infrequently and therefore most of the time the hash value we receive will match the one in our message, indicating we have the latest version of the object.
For the other cases, where the hash we receive does not match the one in our message, we will have to make an additional request to Redis to fetch the new version of the object. This reduces performance as we have to wait for the response from Redis and possibly face network congestion if our Redis instance is serving other requests. For this case, having a Lua script in Redis helps to optimize our performance.
Microservice request flow with Lua script
In the diagram below we get insights into the request flow our microservice needs to go through when we use a Lua script.
We invoke the Lua script with the object's hash value car.hash
. If the hash matches the one stored in Redis we return true
indicating that we have the latest version of the object, if the hash does not match we instead directly return the model. This allows us to skip the additional request to Redis in the cases where the model we have is not the latest version available. By skipping the additional network request we increase the performance of our microservice as it no longer needs to wait for the model to be received.
By reducing the network requests, we have higher performance and lower load on our Redis instance thereby improving overall scalability of our service.
Now that we understand the scenario where a Lua script can help us, let's go through the actual Lua script and how it's implemented in TypeScript.
Lua script in Redis logical flow
If you would like to test this out on your own machine, you can find the GitHub repository here.
In the diagram, we see the logic of the Lua script. It checks whether the key (`car.Id`) exists and if not returns false. If the key exists it then checks whether the hash we passed in matches the stored hash. If the hash matches, we return true indicating we have the latest version of the object. If the hash does not match, we return the latest version of the model.
In the example below we use the Redis npm package to create a Redis client and initialize a Lua script.
Let's go through the script at a high level:
- We define the name of the script, we'll use this name later on to reference the script.
NUMBER_OF_KEYS
defines how many keys we will be passing to the script.SCRIPT
defines the Lua script that we want to execute. We'll break this script down further in the next section.transformArguments
defines all the the parameters that we will pass when invoking the script.transformReply
is used to perform data manipulation on the result of the script before we send the response to our microservice.
Lua script breakdown
In Lua, indexes start at 1, which is counter-intuitive if you're used to other programming languages like C# and TypeScript.
The KEYS[1]
will get the first key that we passed as an argument to the script and ARGV
represents the additional arguments we passed. Since we defined NUMBER_OF_KEYS
as 1, everything after the first argument will be considered ARGV
parameters.
Next, we examine the Lua script line-by-line:
- On line 1, we use the
redis.call
function to get a hash field of the object using the key and the field name. In our caseKEYS[1]
will be thecar.Id
andARGV[1]
will behash
since that is the name of the field we want to retrieve. - On line 4, we check if the value stored in the
hash
field is the same as the hash value we passed. - On line 7, we check if the hash field had any value. This would happen if the key was not found in Redis ie. the object was not cached.
- On line 12, we use the
redis.call
function to get the model field (which contains the serialized object) using the key and the field name. In our caseKEYS[1]
will be thecar.Id
andARGV[3]
will bemodel
since that is the name of the field we want to retrieve.
To invoke our Lua script we would use the function as shown below.
Should you use Lua scripts?
Lua scripts are extremely powerful and can significantly increase performance, especially for operations that have a very high frequency but they do have their downsides such as testability and potential delays to other Redis operations.
As with all things in software, whether you choose to use a Lua script will be highly context-based but at least now you have a better understanding of how they can help you and how you can create them.