事务
事务的定义
事务是指一系列 “要么都执行,要么都不执行” 的逻辑语句;这些语句可以以SQL语句,代码块等形式出现。
文章摘选自 《Redis实现与设计》
Redis 中的事务
Redis 通过 MULTI
EXEC
WATCH
等命令来实现事务功能。
Redis 中的事务指的是,将 将多个命令视为事务,这些命令要么都实现,要么都不实现
最后由 EXEC
命令将这个事务提交给服务器执行。
redis> MULTI
OK
redis> SET "name" "Practical Common Lisp"
QUEUED
redis> GET "name"
QUEUED
redis> SET "author" "Peter Serbel"
QUEUED
redis> GET "author"
QUEUED
redis> EXEC
1) OK
2) "Practical Common Lisp"
3) OK
4) "Peter Seibel"
事务声明
- 首先,我们使用
MULTI
multi-前缀,定义参与事务的多行代码,即标识着事务的开始,此时,该客户端的状态将从非事务状态切换成事务状态;这一切换是通过在客户端状态的 flags 属性中打开REDIS_MULTI
标识来完成的。最后,事务的结果将会依次返回,伪代码如下:
def MULTI():
client.flags |= REDIS_MULTI
replyOK()
- 在标识事务开始后,紧接着我们将一系列代码入队,在已经切换成事务状态后,这些命令不一定会立即执行: 1)如果客户端发送的命令是
EXEC
DISCARD
WATCH
或者MULTI
四个命令中的其中一个,那么服务器将会立即执行这个命令。2) 如果不是这四个命令中的其中一个,服务端将放入事务队列中,并向客户端返回QUEUED
回复。
以下流程图可以描述出,从客户端传来的命令,服务端会如何处理:
切换的这个事务状态属性,保存在每个 Redis 客户端中:
typedef struct redisClient {
//事务状态
multiState mstate; /* MULTI/EXEC state */
} redisClient;
事务状态包含一个事务队列(命令以先进先出的顺序),以及一个已入队命令的计数器(也可以说是事务队列的长度):
typedef struct multiState {
//事务队列 FIFO 顺序
multiCmd *commands;
//已入队命令计数
int count;
}
这个事务队列 multiCmd
保存了已入队命令的相关信息,包括指向命令实现函数的指针、命令的参数以及参数的数量:
typedef struct multiCmd {
//参数
robj **argv;
//参数数量
int argc;
//命令指针
struct redisCommand *cmd;
} multiCmd;
下图解释了其中的结构:
事务执行
当一个处于事务状态的客户端向服务器发送
EXEC
命令时,这个EXEC
命令将立即被服务器执行。服务器会遍历这个客户端的事务队列,执行队列中保存的所有命令,最后将执行命令所得的结果全部返回给客户端。
伪代码如下:
def EXEC():
reply_queue = []
# 遍历事务队列中的每个项
# 读取命令的参数 参数的个数 以及要执行的命令
for argv, argc, cmd in client.mastate.commands:
# 执行命令并取得命令的返回值
reply = execute_command(cmd, argv, argc)
# 将返回值追加到回复队列末尾
reply_queue.append(reply)
# 标识本次事务即将完成 让客户端回到非事务状态
client.flags &= ~REDIS_MULTI
client.mstate.count = 0
release_transaction_queue(client.mastate.commands)
# 将事务的执行结果返回给客户端
send_reply_to_client(client, reply_queue)
WATCH 监听命令
WATCH 命令是一个乐观锁 (总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,只在更新的时候会判断一下在此期间别人有没有去更新这个数据。)
它可以在 EXEC 命令执行之前,监视任意数量的数据库键,并在 EXEC 命令执行时,检查被监视的键是否至少有一个已经被修改过了,如果是的话,服务器将拒绝执行事务,并返回给客户端 nil 作为空回复。
从图中我们可以看到,在客户端A的事务中,当 EXEC
执行时,已经发现所监听的键 name,已经被客户端B修改了,那么此时本次事务得到的结果,会是nil。
如何实现的呢?
其实,每个 Redis 数据库都保存着一个 watched_keys
字典,这个字典的键是某个被 WATCH 命令监视的数据库键,而字典的值则是一个链表,链表中记录了所有监视相应数据库键的客户端:
typedef struct redisDb {
// 正在被 WATCH 命令监视的键
dict *watched_keys;
}
服务器接收到一个客户端发来的 EXEC
命令时,服务器会根据这个客户端是否打开了 REDIS_DIRTY_CAS
标识来决定是否执行事务:
- 如果
REDIS_DIRTY_CAS
标识已经被打开,那么说明客户端所监视的键中,至少有一个键已经被修改过了,在这种情况下,客户端提交的事务已经不再安全,所以服务器会拒绝执行客户端提交的事务。
当这个客户端 c10086
向服务器发送 MULTI
命令,并将一个 SET
命令放入事务队列。
就在这时,另一个客户端 c999
向服务器发送了一条 SET
命令,将 “name” 键的值设置成了 “john”:
c999> SET "name" "john"
OK
由于SET命令会导致正在监视 “name” 键的所有客户端的 REDIS_DIRTY_CAS 标识被打开,其中包括客户端 c10086。
当 c10086 向服务器发送 EXEC 命令时候,因为 c10086 的REDIS_DIRTY_CAS 标识被打开。
当 c10086 向服务器发送 EXEC 命令时候,因为 c10086 的 REDIS_DIRTY_CAS 标志已经被打开,所以服务器将拒绝执行它提交的事务。