There are two ways to achieve complex atomic operations for multiple key
s. 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 key
s, and if any of the monitored key
s 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 key
s 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 key
s 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