Transactions in Redis
Learn how you can use transactions and optimistic locks in Redis
In this article, we will be seeing how Redis supports transactions and locks.
Similar to other databases, Redis also supports transactions via which we can run multiple Redis operations in a single atomic step. By ensuring atomicity and isolation, Redis ensures that the request sent by another client will never be served in the middle of the execution of a Redis transaction.
Redis also supports an Optimistic locking mechanism via which we can guarantee that the variables used inside the transactions are not updated by any other thread or another execution context.
Redis supports these capabilities using the MULTI
, EXEC
, DISCARD
and WATCH
commands.
In the below section, we will use these commands and operate them over a few Redis key string data.
By the way, if you don't know much about Redis data structures and how we set/get the values from Redis, I would recommend checking out this article before proceeding.
Transactions
Let's consider the below example where we set two Redis keys foo
and bar
with values 1 and 2 respectively via our local redis-cli.
127.0.0.1:6379> SET foo 1
OK
127.0.0.1:6379> SET bar 2
OK
Now let's increment the values of both these variables by 1, but instead of executing in 2 steps, do it as a single atomic transaction.
To start a transaction, we use MULTI
following the set of commands we want to run, and then finally we use EXEC
if we want to execute the transaction.
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> INCR foo
QUEUED
127.0.0.1:6379(TX)> INCR bar
QUEUED
127.0.0.1:6379(TX)> EXEC
1) (integer) 2
2) (integer) 3
127.0.0.1:6379> MGET foo bar
1) "2"
2) "3"
As you might have noticed, once we run any operations inside the MULTI
EXEC
block, the output is stated as QUEUED
. This means that the operation is not executed but is enqueued to be executed once we run EXEC
.
As we run EXEC
command, the operations are executed first-in-first-out (being a queue) and the results are returned in the same order.
Similarly, instead of EXEC
command to execute transactions, we can use the DISCARD
command to discard the operations mentioned inside the transaction block.
Errors in transaction
There could be 2 types of errors in Redis transactions
Errors happening before the
EXEC
command- This could occur due to a syntax error for running the command, like a wrong command name or wrong number of arguments.
Errors happening after the
EXEC
command- This could happen in cases where we operate against a key with the wrong value(for example: calling
INCR
against a key which holds a string value).
- This could happen in cases where we operate against a key with the wrong value(for example: calling
For errors happening after the EXEC
command, Redis does not support the rollback of operations in case one of them fails. Even if one of the executions of the operation fails, all the other commands in the queue are still processed and are not rolled back.
Optimistic Locking on transaction attributes
Let's consider a scenario where there are 2 redis clients which are trying to increment the value foo
at the same time. We can simulate this on the local system by running the redis-cli
on 2 CLIs / terminal tabs.
Currently, we have the value of foo
as "2"
based on the commands executed in the previous sections.
In the first cli tab, run the below command
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> INCR foo
QUEUED
Now in the second cli tab, simply increment the foo value without any transaction like below
127.0.0.1:6379> INCR foo
(integer) 3
Now, if we run EXEC
command on the first cli tab, it will increment the value of foo
again.
127.0.0.1:6379(TX)> EXEC
1) (integer) 4
127.0.0.1:6379> GET foo
"4"
As you can see, it updates the value to 4, since the second tab already incremented it once from 2 to 3. This is known as a race condition, where multiple clients can race to update the value of the same attribute.
Redis supports optimistic locking via WATCH
command. Using this command, we can ensure that the transaction is not executed if the attributes inside it have been updated by some other client.
Run the below commands in the first tab
127.0.0.1:6379> WATCH foo
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> INCR foo
QUEUED
127.0.0.1:6379(TX)> INCR bar
QUEUED
Now, again increment the value of foo from the second tab
127.0.0.1:6379> INCR foo
(integer) 5
Now, back from the first tab, try to run EXEC
command. It will return the value as (nil)
127.0.0.1:6379(TX)> EXEC
(nil)
This means that none of the operations inside the transaction was executed(or we can say that the transaction was aborted) because the value of foo(which was being "watched" by the transaction) was updated by some other client.
This WATCH
command can be called multiple times. Also, we can pass multiple keys to WATCH
in a single command, like WATCH foo bar
. All the keys mentioned in WATCH
command are watched for modifications up to the moment EXEC
is called.
Once EXEC
is called, all keys are UNWATCH
ed, regardless of whether the transaction was aborted or not. Similarly, when a client connection is closed, everything gets UNWATCH
ed.
Since this is an Optimistic locking mechanism, the client has to retry the transactions if it fails, until there is no race condition with other clients.
I hope you find this article helpful. If so, please like, comment and share this article 😃.
Let's connect 💫
You can also subscribe to my newsletter below to get an email notification on my latest posts. 💻