16 in Depth Analysis of Redis Transactions

16 In-Depth Analysis of Redis Transactions #

How does Redis handle and use transactions, which are a very important basic feature in relational databases?

Introduction #

A transaction refers to a mechanism that packages multiple commands and executes them sequentially in one go. It ensures that the server continues to process other commands from the client only after executing all the commands in the transaction.

Transactions are also essential basic features in other relational databases. For example, in a payment scenario, the account balance should only be deducted after a successful transaction. However, without transactional support, it is possible that the balance gets deducted even if the transaction fails. I believe no one would accept such a situation, so transactions are crucial in databases.

Basic Usage of Transactions #

In other programming languages, transactions generally consist of the following three phases:

  • Begin transaction
  • Execute business code and commit the transaction
  • Rollback the transaction if an exception occurs during business processing

Taking Java as an example of transaction execution:

// Begin transaction
begin();
try {
    // ......
    // Commit transaction
    commit();
} catch(Exception e) {
    // Rollback transaction
    rollback();
}

In Redis, a transaction goes through three phases from start to finish:

  • Begin transaction
  • Commands are queued
  • Execute transaction/abandon transaction

Among them, the multi command is used to begin a transaction, the exec command is used to execute the transaction, and the discard command is used to abandon the transaction.

Begin Transaction #

The multi command is used to begin a transaction. The implementation code is as follows:

> multi
OK

The multi command allows the client to switch from non-transaction mode to transaction mode. The following image illustrates this:

img

Note: The multi command cannot be nested. If a transaction has already been started and the multi command is executed again, the following error message will be displayed:

(error) ERR MULTI calls can not be nested

The execution result is as shown in the following code:

127.0.0.1:6379> multi
OK
127.0.0.1:6379> multi
(error) ERR MULTI calls can not be nested

When the client is in non-transaction mode and the multi command is used, the client will return OK. If the client is already in transaction mode and the multi command is executed, an error indicating that MULTI calls cannot be nested will be displayed. However, the client will still remain in the transaction mode. The following image illustrates this:

img

Commands are Queued #

After the client enters transaction mode, all regular Redis operation commands (commands that do not trigger transaction execution or abandonment or commands that cause queueing exceptions) will be queued in order. After a command is successfully queued, QUEUED will be returned, as shown in the following code:

> multi
OK
> set k v
QUEUED
> get k
QUEUED

The execution process is shown in the following image:

img

Note: Commands are queued in the order of first in, first out (FIFO). This means that the transaction will be executed in the order of command queueing, from front to back.

Execute Transaction/Abandon Transaction #

The exec command is used to execute a transaction, while the discard command is used to abandon a transaction.

Example code for executing a transaction:

> multi
OK
> set k v2
QUEUED
> exec
1) OK
> get k
"v2"

Example code for discarding a transaction:

> multi
OK
> set k v3
QUEUED
> discard
OK
> get k
"v2"

The execution process is shown in the following image:

img

Transaction Errors and Rollback #

Errors that occur during transaction execution can be divided into the following three categories:

  • Errors that occur during execution (referred to as execution errors)
  • Errors that occur during queueing, which do not terminate the entire transaction
  • Errors that occur during queueing, which terminate the entire transaction

Execution Errors #

Example code:

> get k
"v"
> multi
OK
> set k v2
QUEUED
> expire k 10s
QUEUED
> exec
1) OK
2) (error) ERR value is not an integer or out of range
> get k
"v2"

The execution of the commands is explained in the following image:

img

From the above result, it can be seen that even if an error occurs during the execution of a command in the transaction queue, the transaction will continue to execute until all the commands in the transaction queue are executed.

Queuing Errors That Do Not Terminate a Transaction #

Example code:

> get k
"v"
> multi
OK
> set k v2
QUEUED
> multi
(error) ERR MULTI calls can not be nested
> exec
1) OK
> get k
"v2"

The execution of the commands is explained in the following image:

img

It can be seen that when the multi command is executed repeatedly, it will result in a queuing error, but it will not terminate the transaction. The final query result shows that the transaction was successful. In addition to executing the multi command repeatedly, executing the watch command while in transaction mode will have the same effect, as explained in detail in the following section.

Queuing Errors That Terminate a Transaction #

Example code:

> get k

“v2”

multi OK set k v3 QUEUED set k (error) ERR wrong number of arguments for ‘set’ command exec (error) EXECABORT Transaction discarded because of previous errors. get k “v2”


The command execution explanation is shown in the image below:

![img](../images/fd00c6a0-5de2-11ea-9d6f-15d639c92acd)

#### **Why doesn't Redis support transaction rollback?**

The official Redis documentation explains the reason as follows:

> If you have a relational databases background, the fact that Redis commands can fail during a transaction, but still Redis will execute the rest of the transaction instead of rolling back, may look odd to you.
> 
> However there are good opinions for this behavior:
> 
>   * Redis commands can fail only if called with a wrong syntax (and the problem is not detectable during the command queueing), or against keys holding the wrong data type: this means that in practical terms a failing command is the result of a programming errors, and a kind of error that is very likely to be detected during development, and not in production.
>   * Redis is internally simplified and faster because it does not need the ability to roll back.
> 

> 
> An argument against Redis point of view is that bugs happen, however it should be noted that in general the roll back does not save you from programming errors. For instance if a query increments a key by 2 instead of 1, or increments the wrong key, there is no way for a rollback mechanism to help. Given that no one can save the programmer from his or her errors, and that the kind of errors required for a Redis command to fail are unlikely to enter in production, we selected the simpler and faster approach of not supporting roll backs on errors.

In essence, the author's reasons for not supporting transaction rollback are as follows:

  * He believes that errors in Redis transactions are typically caused by programming errors, which are usually detected in the development environment rather than in the production environment. Therefore, he believes that it is not necessary to support transaction rollback in Redis.
  * The reason for not supporting transaction rollback is that this complex functionality does not align with Redis' goal of simplicity and efficiency.


The lack of support for transaction rollback means that runtime errors in a transaction cannot be rolled back.

### Monitoring

The watch command provides optimistic locking (CAS, Check And Set) for transactions in concurrent client scenarios. It allows a transaction to monitor one or more variables, and if any of the monitored items are modified during the transaction, the entire transaction will be aborted.

The basic syntax of the watch command is as follows:

watch key [key …]


Example usage of `watch` command:

watch k OK multi OK set k v2 QUEUED exec (nil) get k “v”


**Note**: In the above transaction, we simulate concurrent operations on variable `k` by multiple clients during the transaction. Only when the transaction is executed, will the result be shown as above with the `exec` command returning nil.

As can be seen, when the result of `exec` is nil, it means that one of the watched objects has been modified during the transaction execution. This is also confirmed by the result of `get k`, as the value set in the transaction `set k v2` did not take effect.

The execution flow is shown in the following image:

![img](../images/191ad650-5de3-11ea-9e57-957b6467a3fc)

**Note**: The watch command can only be executed before starting a transaction, executing it within a transaction will result in an error, but will not cause the entire transaction to fail, as shown in the following code:

multi OK set k v3 QUEUED watch k (error) ERR WATCH inside MULTI is not allowed exec

  1. OK

get k “v3”


The execution command explanation is shown in the image below:

![img](../images/4097f000-5de3-11ea-9e57-957b6467a3fc)

The unwatch command is used to clear all previously monitored objects (key-value pairs).

Example usage of `unwatch` command:

set k v OK watch k OK multi OK unwatch QUEUED set k v2 QUEUED exec

  1. OK
  2. OK

get k “v2”


As we can see, even if the value of `k` is modified during the execution of the transaction, the transaction will still be executed successfully because the unwatch command was called.

### Code Example

The following is an example of using transactions in Java:

```java
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;

public class TransactionExample {
    public static void main(String[] args) {
        // Create Redis connection
        Jedis jedis = new Jedis("xxx.xxx.xxx.xxx", 6379);
        // Set Redis password
        jedis.auth("xxx");
        // Set key-value
        jedis.set("k", "v");
        // Start watching
        jedis.watch("k");
        // Start transaction
        Transaction tx = jedis.multi();
        // Add commands to transaction queue
        tx.set("k", "v2");
        // Execute transaction
        tx.exec();
        System.out.println(jedis.get("k"));
        jedis.close();
    }
}

Knowledge Check #

What will be the result of these two clients executing commands alternately? #

Client 1, execute these commands:

> set k v
OK
> watch k
OK
> multi
OK
> set k v2
QUEUED

Client 2, execute these commands:

> set k v
OK

Client 1, then execute this command:

> exec

What will be the value of k at this point?

Answer: The value of k is v, not v2.

Explanation: This question tests whether the watch command still considers an object modified even if its value is reassigned to the original object.

Summary #

Transactions provide a mechanism to execute multiple commands in a single, ordered sequence. There are five Redis commands related to transactions:

  • multi: Start a transaction
  • exec: Execute a transaction
  • discard: Discard a transaction
  • watch: Provide optimistic locking for transactions
  • unwatch: Cancel monitoring (optimistic locking) of objects in a transaction

In normal situations, a Redis transaction consists of three stages: starting a transaction, adding commands to the transaction queue, and executing the transaction. Redis transactions do not support transaction rollback for runtime errors, but they do provide rollback functionality for certain enqueueing errors, such as set key or modifications to watched items.