Demystifying Redis Transactions

Posted September 9, 2017 - 4 min read
Topics:  

In the world of key-value databases, Redis is one of the most popular projects which has taken key-value databases to the next level by introducing in-memory data structures.

Data structures like lists, hashes, or sorted sets are very useful for storing, retrieving, and managing efficiently application data.

Additionally to data structures, one of the most important things to consider about a key/value database is data consistency and atomicity when running many operations.

In other words, sometimes, you may want to run a set of operations as an atomic operation.

Thankfully, in regard to this matter, Redis also provides transactions.

Within this post, we will discuss what are Redis transactions, see how they differ from traditional RDMS transactions, and learn how to use them.

Redis Transactions

A Redis transaction is a set of commands which are queued and executed sequentially as a single isolated operation which means that either all the commands are executed or none of them.

During the executions of a Redis transaction, the side effects of the queued commands are not visible to other clients until the transaction is committed.

In fact, when a transaction is being executed, all other clients connections are put on hold and no command from outside the transaction is allowed to be executed.

A Redis transaction can not be rolled back as a traditional RDMS transaction based on some condition.

As the commands are being queued, decision-making procedures or logical operations can not be made.

However, Redis does allow to cancel a transaction and flush the commands that has been enqueued with the help of DISCARD.

MULTI/EXEC

A typical Redis transaction lifecycle consists of 3 steps:

  1. Transaction opening: to start a transaction, the client must issue the MULTI command.
  2. Enqueuing commands: after MULTI is executed, all subsequent commands get queued and not executed.
  3. Executing the transaction (committing) or discarding (cancelling): all queued commands get executed once the EXEC command is issued. The DISCARD command cancels the transaction and clears the command queue.
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> HSET user:652 last-login "17 Mar 2025 11:25:26"
QUEUED
127.0.0.1:6379(TX)> SADD online-users 652
QUEUED
127.0.0.1:6379(TX)> SCARD online-users
QUEUED
127.0.0.1:6379(TX)> EXEC
1) (integer) 1
2) (integer) 1
3) (integer) 15

WATCH and MULTI/EXEC

After a transaction has been initialized using the MULTI command and before it is executed, data involved within the transaction can still be updated and changed from another client connection.

In many use cases, before commiting a transaction, we may want to be sure that transaction data has not been changed by another Redis client.

To satisfy this requirement, Redis comes with the WATCH command which can be combined with MULTI/EXEC to guaranty that the transaction will be executed only when the watched keys have not been changed since the time WATCH command was issued.

This mechanism, sometimes referred to as CAS (CHECK AND SET), can be used to deal with concurrent operations.

The following diagram demonstrates how the WATCH command can be useful:


 ┌───────┐                       ┌──────┐                     ┌───────┐
 │Session│                       │Redis │                     │Session│
 │   A   │                       │server│                     │   B   │
 └───┬───┘                       └──┬───┘                     └───┬───┘
     │                              │                             │
     │                              │                             │
     │       WATCH usr-blc:43       │                             │
     ├──────────────────────────────►                             │
     │                              │                             │
     │                              │                             │
     │            MULTI             │                             │
     ├──────────────────────────────►                             │
     │                              │                             │
     │   INCRBY usr-blc:43 78.5     │                             │
     ├──────────────────────────────►                             │
     │                              │                             │
     │                              │                             │
     │                              │   INCRBY usr-blc:43 78.5    │
     │                              ◄─────────────────────────────┤
     │                              │                             │
     │                              │                             │
     │             EXEC             │                             │
     ├──────────────────────────────►                             │
     │                              │                             │
     │                              │                             │

When the EXEC command is issued, Redis detects that the usr-blc:43 has been changed, cancels the transaction, and returns a NULL reply.

LUA Scripts vs MULTI/EXEC

Lua scripts allow extending Redis with more features.

In fact Redis itself uses LUA to run all the built-in commands.

A Lua script is executed as a single atomic transaction.

When a script is being executed, no other command is allowed to be executed. So it behaves the same way as MULTI/EXEC.

With regard to transactions, the main advantage of Lua scripting is that it allows using an intermediate result of one command in another command within the same transaction.

Lua scripting is a powerful approach of running transactions, which makes use of the Lua programing language.

You can use loops, conditions, variables, etc.

In the following example, the expiration of key some_key is set only when its value is equal to some_id:

127.0.0.1:6379> EVAL 'if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("PEXPIRE", KEYS[1], ARGV[2]) end return 0' 1 'some_key' 'some_id' '1000'

See Redis Lua Scripting: How To Get Started With? to learn more about Lua scripting.

Conclusion

As we have seen Redis provides two approaches for using transactions: Lua scripts and Multi/Exec.

Both approaches block the Redis server while executing commands.

Therefore, you should never put a command within a transaction that may take a long time to execute.

Additionally, when using Lua scripts, you should always return a response as fast as possible and avoid long-running operations like looping through a large array.