Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”.


When it comes to database transactions, it is estimated that the first reaction of many students is ACID. The atomicity of A, which is ranked first in ACID, requires that all operations in A transaction be either completed or not completed. If you are familiar with Redis, you must know that transactions also exist in Redis. Let’s find out.

What are Redis transactions?

Like database transactions, Redis transactions are used to execute multiple commands at once. It is also simple to use. You can start a transaction with MULTI, queue multiple commands into the transaction, and finally trigger the transaction by EXEC command to execute all commands in the transaction. Look at a simple transaction execution example:

127.0.0.1:6379> multi
OK
127.0.0.1:6379> set name Hydra
QUEUED
127.0.0.1:6379> set age 18
QUEUED
127.0.0.1:6379> incr age
QUEUED
127.0.0.1:6379> exec
1) OK
2) OK
3) (integer) 19
Copy the code

It can be seen that all commands are executed successfully after entering EXEC when the data types of instructions and operands are normal.

Do Redis transactions satisfy atomicity?

If you want to verify whether redis transaction meets atomicity, you need to perform it in the case of redis transaction execution exception. Below, we will test two different types of errors respectively.

Grammar mistakes

First, test the syntax error in the command. In this case, the number of parameters in the command is incorrect or the input command itself has errors. Start the transaction and type the following commands in sequence:

127.0.0.1:6379> multi
OK
127.0.0.1:6379> set name Hydra
QUEUED
127.0.0.1:6379> incr
(error) ERR wrong number of arguments for 'incr' command
127.0.0.1:6379> set age 18
QUEUED
Copy the code

No parameter is added after the incr command, which is a syntax error with incorrect command format. In this case, an error message is displayed immediately when the command is entered. Use exec to execute the transaction and see the output:

127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.
Copy the code

In this case, as long as one of the commands in the transaction has a syntax error, an error will be returned directly after exec execution, and all commands, including those with correct syntax, will not be executed. To verify this, check the execution of other instructions in the transaction, check the execution result of the set command, all empty, indicating that the instruction was not executed.

127.0.0.1:6379> get name
(nil)
127.0.0.1:6379> get age
(nil)
Copy the code

In addition, if the command itself is misspelled or a command that does not exist is entered, it is also a syntax error and an error will be reported during the transaction.

Runtime error

A runtime error refers to an error that occurs during the execution of a command when the format of the input command is correct. A typical scenario is that a runtime error occurs when the data type of the input parameter does not meet the requirements of the command. For example, in the following example, an error occurs when performing a list operation on a string value:

127.0.0.1:6379> set key1 value1
OK
127.0.0.1:6379> lpush key1 value2
(error) WRONGTYPE Operation against a key holding the wrong kind of value
Copy the code

This type of error cannot be detected until redis actually executes the command, so the command can be received by the transaction queue and will not be immediately reported as the above syntax error.

In the following transaction, try the incR increment operation on string data:

127.0.0.1:6379> multi OK 127.0.0.1:6379> set name Hydra QUEUED 127.0.0.1:6379> set age QUEUED 127.0.0.1:6379> Incr age QUEUED 127.0.0.1:6379> del name QUEUEDCopy the code

Redis has not reported any errors up to this point.

127.0.0.1:6379> exec
1) OK
2) OK
3) (error) ERR value is not an integer or out of range
4) (integer) 1
Copy the code

Incr age = incr age; incr age = incr age;

127.0.0.1:6379> get name
(nil)
127.0.0.1:6379> get age
"eighteen"
Copy the code

Stage conclusion

Analyze the results of the above transaction:

  • If syntax errors exist, all commands are not executed
  • When a running error occurs, all commands can be executed normally except the incorrect commands

Through the analysis, we know that transactions in Redis do not meet the atomicity, in the case of running errors, and does not provide a similar rollback function in the database. Why does Redis not support rollback?

  • Redis commands fail only with syntax errors or data type errors, which are caused by errors in programming and should be detected in the development environment, not in production
  • Not using rollback makes redis internal design simpler and faster
  • Rollback does not prevent errors in programming logic, and if you want to increase the value of a key by 2 but only increase it by 1, even providing rollback does not help

For these reasons, Redis officials have chosen the simpler, faster method and do not support error rollback. Thus, if atomicity is required in our business scenario, the developer is required to ensure the success or failure of all command execution by other means, such as checking parameter types before executing a command or compensating for transaction execution errors in a timely manner.

For example, Lua scripts are commonly used in distributed locks. So, does Lua script really guarantee atomicity?

Simple introduction to Lua scripts

Before we can verify the atomicity of lua scripts, we need to take a brief look at it. Redis supports the execution of Lua scripts since version 2.6. Its functions are very similar to transactions. A Lua script is executed as a single command. Let’s take a look at the following common commands.

The EVAL command

The most common EVAL is used to execute a script in the following format:

EVAL script numkeys key [key ...]  arg [arg ...]Copy the code

A brief explanation of the parameters:

  • scriptIs a Lua script
  • numkeysSeveral subsequent parameters are specifiedkey, such as nokeyIt is zero
  • The key [key]...Represents the redis key used in the script, passed in the lua scriptKEYS[i]Form acquisition of
  • Arg [arg]...Represents additional parameters that pass in lua scriptsARGV[i]To obtain

Look at a simple example:

127.0.0.1:6379> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 value1 vauel2
1) "key1"
2) "key2"
3) "value1"
4) "vauel2"
Copy the code

In the command above, the lua script is in double quotes, followed by 2 to indicate the presence of two keys, key1 and key2, followed by additional arguments value1 and value2.

If you wanted to execute the set command using a lua script, you could write it like this:

127.0.0.1:6379 > EVAL "redis. Call (' SET 'KEYS [1], ARGV [1]);" 1 name Hydra (nil)Copy the code

We used the built-in Redis lua function redis. Call to complete the set command. We printed nil because there was no return value. Return statement.

SCRIPT LOAD and EVALSHA commands

These two commands are put together because they are generally used in pairs. SCRIPT LOAD, which is used to LOAD scripts into the cache, returns the SHA1 checksum. The command is cached, but is not executed immediately.

127.0.0.1:6379> SCRIPT LOAD "return redis. Call ('GET', KEYS[1]);" "228d85f44a89b14a5cdb768a29c4c4d907133f56"Copy the code

This returns a checksum of SHA1, which is then used to execute the script using EVALSHA:

127.0.0.1:6379 > EVALSHA "228 d85f44a89b14a5cdb768a29c4c4d907133f56" 1 name "Hydra"Copy the code

This SHA1 value is equivalent to importing the command cache above, and then concatenating numkeys, key, arg and other parameters, the command can be executed normally.

Other commands

Use the SCRIPT EXISTS command to determine whether a SCRIPT is cached:

127.0.0.1:6379 > SCRIPT EXISTS 228 d85f44a89b14a5cdb768a29c4c4d907133f56 1) 1 (integer)Copy the code

Use the SCRIPT FLUSH command to FLUSH the lua SCRIPT cache in Redis:

127.0.0.1:6379 > SCRIPT FLUSH OK 127.0.0.1:6379 > SCRIPT EXISTS 228 d85f44a89b14a5cdb768a29c4c4d907133f56 1) 0 (integer)Copy the code

As you can see, after SCRIPT FLUSH is executed, the SCRIPT does not exist when you look at the SHA1 value again. Finally, you can also KILL a currently running Lua SCRIPT with the SCRIPT KILL command, but this takes effect only if the SCRIPT has not performed a write operation.

From these actions, the Lua script has the following advantages:

  • Multiple network requests can be completed in one request, reducing network overhead and reducing network latency
  • The script sent by the client is stored in Redis and can be reused by other clients without having to re-code the same logic

Lua scripts are used in Java code

In Java code, you can use the APIS encapsulated in Jedis to execute Lua scripts. Here is an example of executing lua scripts using Jedis:

public static void main(String[] args) {
    Jedis jedis = new Jedis("127.0.0.1".6379);
    String script="redis.call('SET', KEYS[1], ARGV[1]);"
            +"return redis.call('GET', KEYS[1]);";
    List<String> keys= Arrays.asList("age");
    List<String> values= Arrays.asList("eighteen");
    Object result = jedis.eval(script, keys, values);
    System.out.println(result);
}
Copy the code

The console prints the result of the get command after executing the code above:

eighteen
Copy the code

With a simple setup, let’s see if lua scripts can achieve rollback level atomicity. Modify the above code to insert a command that runs the error:

public static void main(String[] args) {
    Jedis jedis = new Jedis("127.0.0.1".6379);
    String script="redis.call('SET', KEYS[1], ARGV[1]);"
            +"redis.call('INCR', KEYS[1]);"
            +"return redis.call('GET', KEYS[1]);";
    List<String> keys= Arrays.asList("age");
    List<String> values= Arrays.asList("eighteen");
    Object result = jedis.eval(script, keys, values);
    System.out.println(result);
}
Copy the code

View the execution result:

Go to the client and execute the get command:

127.0.0.1:6379 > get the age eighteen ""Copy the code

That is, although the program throws an exception, the command before the exception is executed without being rolled back. Try running this command directly from the Redis client:

127.0.0.1:6379> flushall OK 127.0.0.1:6379> eval "redis. Call ('SET', KEYS[1], ARGV[1]); redis.call('INCR', KEYS[1]); return redis.call('GET', KEYS[1])" 1 age eight (error) ERR Error running script (call to f_c2ea9d5c8f60735ecbedb47efd42c834554b9b3b): @user_script:1: ERR value is not an integer or out of range 127.0.0.1:6379> Get age "eight"Copy the code

Also, the instruction before the error is still not rolled back, so what about the Lua script that we often hear about guaranteeing atomic operations?

In fact, in Redis, the same Lua interpreter is used to execute all commands, which ensures that when a lua script is executed, no other scripts or Redis commands will be executed at the same time, ensuring that the operation will not be inserted or disturbed by other instructions, and achieving only this degree of atomicity.

Unfortunately, if the script runs in error and ends, no subsequent operations are performed, but previous writes are not undone, so atomicity like database rollback cannot be achieved even with Lua scripts.

This paper is tested based on Redis 5.0.3

Redis. IO /topics/tran…

The last

If you think it is helpful, you can like it and forward it. Thank you very much

Public number agriculture ginseng, add a friend, do a thumbs-up friend ah