Redis Journey — What kind of problem is Lua going to solve for us?
Have you ever heard anything about concurrency problems? If you’re a developer you might be encountered with this problem a lot. I faced this problem when I was working with Redis and today guys I want to share with you how I solved this with the help of Lua.
I assume you are not familiar with Lua. Honestly you don’t need to dive into this language for now but let me admit that Lua is super simple. Anyway if you want to learn more about this language, check this course. Let’s jump into it! 🙃
The story of Lua
“Lua is a lightweight, high-level, multi-paradigm programming language designed primarily for embedded use in applications. It’s also used as a general-purpose scripting language. Lua is known for its simplicity, efficiency, portability, and ease of embedding into other applications.”
Thanks to ChatGPT. But what is an embedded language anyway? Well, embedded languages are mainly designed to be able to use them within another langues! This integration allows developers to leverage the features and capabilities of the embedded language within the host environment.
Lua is known for its fast execution speed and low memory footprint. It has a smaller ecosystem comparing to JavaScript for example, which has a vast ecosystem with extensive libraries and frameworks.
The problem I’ve faced
As I mentioned earlier I encountered concurrency problem and to see how this problem occurred, I provided this pseudo code to be understandable for you:
products = Redis.GET("Products")
if (products > 0) {
Redis.INCRBY("Products", -1)
Redis.LPUSH("buyers", userId)
} else {
throw new Error("Cannot buy!")
}
You may think of what’s the problem right? Because it seems work well and to be honest with you my friend, this code works correctly! however when two or more requests are made simultaneously, problem comes in to play.
Imagine our Redis instance holds the value of Products
as 5. Now, let’s consider a scenario where 10 clients simultaneously make requests. Each request triggers the execution of a specific code segment responsible for checking the availability of products. Due to the concurrent nature of these requests, the code runs concurrently 10 times.
In this scenario, each execution of the code segment will query the value of Products
from the Redis instance. Since all requests are processed concurrently, each execution sees the same initial value of Products
, which is 5. Consequently, the code condition, which checks if there are enough products available, may appear to be satisfied for all executions because they all observe the initial state of Products
as 5.
This is a big problem because, even though there are only 5 products available, the code doesn’t enforce any locking or atomic operations to ensure that no more than 5 requests are allowed to proceed.
But the important question here is how Lua is going to solve this problem? Well, first of all Redis is single thread. Now when you run a command, all the other operations will block until the command runs. So as you may guess if I try to run a script for 10 times simultaneously, because Redis is single thread, so it handles them one by one.
Let’s run our first script on Redis
There are two primary methods to execute a script on your Redis server. The first method involves using the EVAL
command. With EVAL
, you need to transmit the entire script each time you want to execute it. Alternatively, you can use the SCRIPT LOAD
command, which caches your script and returns a SHA1 digest. Then you can invoke your script using this digest.
Note: by using
SCRIPT LOAD
, Redis only caches loaded script. And after restarting server or callingSCRIPT FLUSH
your scripts can become lost. Your application is responsible for reloading scripts when they are missing.
Now, I’m going to rewrite our above pseudo code to Lua.
--[[
Inputs:
1 => userId
Error Codes:
1 => success
0 => key is not exists
-1 => faild
Return:
[ error code, error ]
]]
local userId = ARGV[1]
-- Check key exists
local isExists = tonumber(redis.call('EXISTS', 'Products'))
if isExists ~= 1 then
return { 0, 'Key is not exists' }
end
-- Check there is enough stocks
local stocks = tonumber(redis.call('GET', 'Products'))
if stocks <= 0 then
return { -1, 'There is not enough product stock' }
end
-- Process
redis.call('INCRBY', 'Products', '-1')
redis.call('LPUSH', 'buyers', tostring(userId))
return { 1, 'OK' }
Let’s see what is going on; First I check the Products
key exists in database (I know it’s an extra step for us but It’s good to care about the details). Then I check there is enough stock or not as you see. And at the end If everything is okay, I process the request.
If you want to see all the available APIs, this link will help you.
Let’s do this together! I have a Redis instance available on my laptop and I will show you how you can run your script on Redis. The only important prerequisite is your Redis version which should be 2.6.0 or greater (my Redis is 7.2.4).
At the first step, we have to load our script on Redis and to do that run this command.
redis-cli -x SCRIPT LOAD < [your script path]
This command will return a Sha1 string. Now you have to set a key named Products
with a positive value like this.
redis-cli SET Products 5
Now you can run the script.
redis-cli EVALSHA 0f32d3694156e4275e6889ae63ed22eae4e2e6d6 1 "1234"
Congratulation! You’ve just ran your first script and as you see it was so easy.
I hope it was useful for you. Please leave a comment if I made a mistake somewhere or you found it useful. It is a big help and thanks in advance.
See you later, Goodbye 👋😁