Two Ways to Implement Atomic Operations in Redis and a Solution for Inventory Management

Translation wujiuye 160 0 2020-03-21

This article is a translation of the original text, which can be found at the following link: https://www.wujiuye.com/article/5a3f3615e9ba4705b90635c91c25c358

Author: wujiuye
Link: https://www.wujiuye.com/article/5a3f3615e9ba4705b90635c91c25c358
Source: 吴就业的网络日记
This article is an original work by the blogger and is not allowed to be reproduced without the blogger's permission.

There are two ways to achieve complex atomic operations for multiple keys. One is Watch+Multi, which is a monitoring plus transactional approach, and the other is to execute a lua script.

The complex atomic operations mentioned here, such as deducting 5 items from a product’s inventory, require first checking if the remaining inventory is sufficient. However, it’s unavoidable that during the judgment, the inventory may be modified by others.

Watch+Multi

Watch can monitor multiple keys, and if any of the monitored keys are modified, all operations will not be executed. Watch cannot be used alone and must be combined with transactions.

Let’s take a look at what happens after Watch and before the transaction is executed, if a key is modified.

This is easy to understand. Even if the order of commands in the transaction is different, due to the single-threaded execution of commands, exec will know which monitored keys have been modified when it is executed, and it will simply not execute the transaction.

Each Redis database has a watched_keys dictionary, which stores the keys being monitored by the Watch command, such as s1 and s2 in the example above. The value of the dictionary is a linked list that records all clients monitoring the key, i.e., a connection.

After executing each write command, Redis checks the watched_keys dictionary to see if any clients are monitoring the key that was just written. If so, it sets the REDIS_DIRTY_CAS flag for the client, indicating that the client’s transaction security has been compromised. When the client submits the transaction for execution, it first checks if the flag is set. If it is, the client’s transaction will be rejected.

What if we execute this in a Cluster environment? I deployed a Cluster on my server a long time ago. Let’s see what happens when executing Watch on multiple keys in a Cluster.

As you can see, an error occurs when executing Watch, and the request does not allow keys to be in different slots. Even if the slots are different but on the same node, it’s not allowed; they must be in the same slot. In the figure below, s4 and s5 are on the same node.

Watch is very strict and does not allow monitoring multiple keys across different nodes. Have you ever wondered why monitoring multiple keys across different nodes is not allowed? In a single node, Redis executes commands in a single thread, ensuring atomicity. However, in a multi-node environment, it’s a multi-process, multi-thread issue, and Watch naturally cannot be used.

Lua Script

Redis executes Lua scripts as atomic operations, just like executing Redis commands. The atomicity of Lua scripts benefits from Redis’s single-threaded command execution. Lua scripts are placed in the command execution queue and executed sequentially, so be careful not to execute too much code in Lua scripts, and avoid writing for loops, which may block and cause Redis nodes to hang.

In a master-slave Redis cluster or a read-write separated Redis cluster, writing Lua scripts does not require considering too many issues, just the script execution time. However, in a sharded Cluster, if we want to implement atomic operations using Lua scripts, we must ensure that all keys operated on by the script are on the same Redis node, i.e., all keys calculate to the same slot.

In fact, if the keys operated on are not on the same node, the command will fail. The figure below shows the error thrown by the Lua script when trying to access a non-local node in the cluster.

Even if multiple Lua scripts are executed in a transaction, if any script operates on keys across different nodes, the result will fail.

It’s not just Lua scripts; transactions also do not support operations on keys across different nodes.

Different slots are also not allowed.

Inventory Management Problem

Regarding inventory management, many video tutorials talk about using distributed locks. You might also think of using distributed locks, but those who have used them know the performance implications.

If the inventory only uses Redis caching and does not need to modify the database inventory, we can use Lua scripts to implement atomic modifications to the inventory. In the Lua script, we can judge whether the inventory is sufficient to deduct the inventory, and if so, update the inventory. This series of operations is atomic.

Lua scripts are not difficult to learn. After looking at conditional statements, branch statements, and judgment statements, I was able to write my own script. Because we don’t need to do complex things with Lua. Here’s an example of an atomic inventory modification Lua script:

local kc=tonumber(redis.call('GET',KEYS[1])); 
if kc==nil 
then 
    return -1; 
end 
local newKc=kc-ARGV[1]; 
if newKc<0 
then 
    return 0; 
else 
    redis.call('SET',KEYS[1],newKc); 
    return 1; 
end