redis事务
Redis 通过 MULTI 、 DISCARD 、 EXEC 和 WATCH 四个命令来实现事务功能, 本章首先讨论使用 MULTI 、 DISCARD 和 EXEC 三个命令实现的一般事务, 然后再来讨论带有 WATCH 的事务的实现。
因为事务的安全性也非常重要, 所以本章最后通过常见的 ACID 性质对 Redis 事务的安全性进行了说明。
事务
事务提供了一种“将多个命令打包, 然后一次性、按顺序地执行”的机制, 并且事务在执行的期间不会主动中断 —— 服务器在执行完事务中的所有命令之后, 才会继续处理其他客户端的其他命令。
以下是一个事务的例子, 它先以 MULTI 开始一个事务, 然后将多个命令入队到事务中, 最后由 EXEC 命令触发事务, 一并执行事务中的所有命令:
redis> MULTI
OK
redis> SET book-name "Mastering C++ in 21 days"
QUEUED
redis> GET book-name
QUEUED
redis> SADD tag "C++" "Programming" "Mastering Series"
QUEUED
redis> SMEMBERS tag
QUEUED
redis> EXEC
1) OK
2) "Mastering C++ in 21 days"
3) (integer) 3
4) 1) "Mastering Series"
2) "C++"
3) "Programming"
一个事务从开始到执行会经历以下三个阶段:
- 开始事务。
- 命令入队。
- 执行事务。
下文将分别介绍事务的这三个阶段。
开始事务
MULTI 命令的执行标记着事务的开始:
redis> MULTI
OK
这个命令唯一做的就是, 将客户端的 REDIS_MULTI 选项打开, 让客户端从非事务状态切换到事务状态。
命令入队
当客户端处于非事务状态下时, 所有发送给服务器端的命令都会立即被服务器执行:
redis> SET msg "hello moto"
OK
redis> GET msg
"hello moto"
但是, 当客户端进入事务状态之后, 服务器在收到来自客户端的命令时, 不会立即执行命令, 而是将这些命令全部放进一个事务队列里, 然后返回 QUEUED , 表示命令已入队:
redis> MULTI
OK
redis> SET msg "hello moto"
QUEUED
redis> GET msg
QUEUED
以下流程图展示了这一行为:
事务队列是一个数组, 每个数组项是都包含三个属性:
- 要执行的命令(cmd)。
- 命令的参数(argv)。
- 参数的个数(argc)。
举个例子, 如果客户端执行以下命令:
redis> MULTI
OK
redis> SET book-name "Mastering C++ in 21 days"
QUEUED
redis> GET book-name
QUEUED
redis> SADD tag "C++" "Programming" "Mastering Series"
QUEUED
redis> SMEMBERS tag
QUEUED
那么程序将为客户端创建以下事务队列:
0 |
SET |
["book-name", "Mastering C++ in 21 days"] |
2 |
1 |
GET |
["book-name"] |
1 |
2 |
SADD |
["tag", "C++", "Programming", "Mastering Series"] |
4 |
3 |
SMEMBERS |
["tag"] |
1 |
执行事务
前面说到, 当客户端进入事务状态之后, 客户端发送的命令就会被放进事务队列里。
但其实并不是所有的命令都会被放进事务队列, 其中的例外就是 EXEC 、 DISCARD 、 MULTI 和 WATCH 这四个命令 —— 当这四个命令从客户端发送到服务器时, 它们会像客户端处于非事务状态一样, 直接被服务器执行:
如果客户端正处于事务状态, 那么当 EXEC 命令执行时, 服务器根据客户端所保存的事务队列, 以先进先出(FIFO)的方式执行事务队列中的命令: 最先入队的命令最先执行, 而最后入队的命令最后执行。
比如说,对于以下事务队列:
0 |
SET |
["book-name", "Mastering C++ in 21 days"] |
2 |
1 |
GET |
["book-name"] |
1 |
2 |
SADD |
["tag", "C++", "Programming", "Mastering Series"] |
4 |
3 |
SMEMBERS |
["tag"] |
1 |
程序会首先执行 SET
命令, 然后执行 GET
命令, 再然后执行 SADD
命令, 最后执行 SMEMBERS
命令。
执行事务中的命令所得的结果会以 FIFO 的顺序保存到一个回复队列中。
比如说,对于上面给出的事务队列,程序将为队列中的命令创建如下回复队列:
0 |
status code reply |
OK |
1 |
bulk reply |
"Mastering C++ in 21 days" |
2 |
integer reply |
3 |
3 |
multi-bulk reply |
["Mastering Series", "C++", "Programming"] |
当事务队列里的所有命令被执行完之后, EXEC 命令会将回复队列作为自己的执行结果返回给客户端, 客户端从事务状态返回到非事务状态, 至此, 事务执行完毕。
事务的整个执行过程可以用以下伪代码表示:
def execute_transaction():
# 创建空白的回复队列
reply_queue = []
# 取出事务队列里的所有命令、参数和参数数量
for cmd, argv, argc in client.transaction_queue:
# 执行命令,并取得命令的返回值
reply = execute_redis_command(cmd, argv, argc)
# 将返回值追加到回复队列末尾
reply_queue.append(reply)
# 清除客户端的事务状态
clear_transaction_state(client)
# 清空事务队列
clear_transaction_queue(client)
# 将事务的执行结果返回给客户端