Transactions in Redis

Transactions in Redis

Learn how you can use transactions and optimistic locks in Redis

ยท

5 min read

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

  1. 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.
  2. 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).

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 UNWATCHed, regardless of whether the transaction was aborted or not. Similarly, when a client connection is closed, everything gets UNWATCHed.

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. ๐Ÿ’ป

ย