[Redis][19][事务]

第 19 章 事务

Redis通过MULTIEXECWATCH等命令来实现事务功能。事务提供了一种将多个命令请求打包,然后一次性、按顺序地执行多个命令的机制,并且在事务执行期间,服务器不会中断事务而改去执行其他客户端的命令请求。

以下是一个事务执行的过程,该事务首先以一个MULTI命令为开始,接着将多个命令放入事务当中,最后由EXEC命令将这个事务提交给服务器执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
redis> MULTI
OK

redis> SET "name" "Practical Common Lisp"
QUEUED

redis> GET "name"

redis> SET "author" "Peter Seibel"
QUEUED

redis> GET "author"
QUEUED

redis> EXEC
1) OK
2) "Practical Common Lisp"
3) OK
4) "Peter Seibel"

19.1 事务的实现

一个事务从开始到结東通常会经历以下三个阶段

  1. 事务开始
  2. 命令人队
  3. 事务执行

19.1.1 事务开始

MULTI命令的执行标志着事务的开始

MULTI命令可以将执行该命令的客户端从非事务状态切换至事务状态,这一切换是通过在客户端状态的flags属性中打开REDIS MULTI标识来完成的,如下伪代码

1
2
3
4
5
6
7
def MULTI():

# 打开事务标志
client.flags |= REDIS_MULTI

# 返回OK回复
replyOK()

19.1.2 命令入队

当一个客户端处于非事务状态时,这个客户端发送的命令会立即被服务器执行,当一个客户端切换到事务状态之后,则不一定,它会根据不同的命令见机行事:

  • 如果客户端发送的命令为EXECDISCARDWATCHMULTI四个命令的其中一个,那么服务器立即执行这个命令。
  • 与此相反,如果客户端发送的命令是EXECDISCARDWATCHMULTI四个命令以外的其他命令,那么服务器并不立即执行这个命令,而是将这个命令放入一个事务队列里面,然后向客户端返回QUEUED回复。

如下流程图所示

19.1.3 事务队列

每个Redis客户端都有自己的事务状态,这个事务状态保存在客户端状态的mstate属性中,它包括一个事务队列和一个已入队命令的计数器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef struct redisClient{
//...

//事务状态
multiState mstate;

//...
}
typedef struct multiState{

//事务队列,FIFO顺序
multiCmd *command;

//已入队命令计数
int count;
}multiState;

事务队列以先进先出的方式保存入队的命令,较先人队的命令会被放到数组的前面,而较后入队的命令则会被放到数组的后面,例如下图

19.1.4 执行事务

当一个处于事务状态的客户端向服务器发送EXEC命令时,这个EXEC命令将立即被服务器执行。服务器会遍历这个客户端的事务队列,执行队列中保存的所有命令,最后将执行命令所得的结果全部返回给客户端

EXEC命令的伪代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def EXEC()
# 创建空白的回复队列
reply_queue = []

# 遍历事务队列中的每个项
# 读取命令的参数,参数的个数,以及要执行的命令
for argv, argc, cmd in client.mstate.commands:

# 执行命令,并取得命令的返回值
reply = execute_command(cmd, argv, argc)

# 将返回值追加到回复队列末尾
reply_queue.append(reply)

# 移除REDIS_MULTI标识,让客户端回到非事务状态
client.flags &= ~REDIS_MULTI

# 清空客户端的事务状态,包括
# 1)清零入队命令计数器
# 2)释放事务队列

client.state.count = 0
release_transaction_queue(client.mstate.commands)

# 将事务的执行结果返回给客户端
send_reply_to_client(client, reply_queue)

19.2 WATCH命令的实现

WATCH命令是一个乐观锁,它可以在EXEC命令执行之前,监视任意数量的数据库键,并在EXEC命令执行时,检查被监视的键是否至少有一个已经被修改过了,如果是的话,服务器将拒绝执行事务,并向客户端返回代表事务执行失败的空回复。

1
redis> WATCH "name"

19.2.1 使用WATCH命令监视数据库键

每个Redis数据库都保存着一个watched_keys字典,这个字典的键是某个被WATCH命令监视的数据库键,而字典的值则是一个链表,链表中记录了所有监视相应数据库键的客户端

1
2
3
4
5
6
7
8
typedef struct redisDb{
//...

//正在被WATCH命令监视的键
dict *watched_keys

//...
} redisDb;

通过watched_keys字典,服务器可以清楚地知道哪些数据库键正在被监视,以及哪些客户端正在监视这些数据库键。

下图就是一个watched_keys字典的例子

19.2.2 监视机制的触发

所有对数据库进行修改的命令,在执行之后都会调用multi.c/touchWatchKey函数对watched_keys字典进行检查,查看是否有客户端正在监视刚刚被命令修改过的数据库键,如果有的话,那么touchWatchKey函数会将监视被修改键的客户端的REDIS_DIRTY_CAS标识打开,表示该客户端的事务安全性已经被破坏。

伪代码描述如下

1
2
3
4
5
6
7
8
9
10
def touchWatchKey(db, key):
# 如果键key存在于数据库的watched_keys字典中
# 那么说明至少有一个客户端在监视这个key
if key in db.watched_keys:

# 遍历所有监视键key的客户端
for client in db.watched_keys[key]

# 打开标志
client.flags |= REDIS_DIRTY_CAS

19.2.3 判断事务是否安全

当服务器接收到一个客户端发来的EXEC命令时,服务器会根据这个客户端是否打开了REDIS_DIRTY_CAS标识来决定是否执行事务:

19.3 事务的ACID性质

在Redis中,事务总是具有原子性、一致性和隔离性,并且当Redis运行在某种特定的持久化模式下时,事务也具有耐久性

19.3.1 原子性

对于Redis的事务功能来说,事务队列中的命令要么就全部都执行,要么就一个都不执行,因此,Redis的事务是具有原子性的。

Redis的事务和传统的关系型数据库事务的最大区别在于,Redis不支持事务回滚机制,即使事务队列中的某个命令在执行期间出现了错误,整个事务也会继续执行下去,直到将事务队列中的所有命令都执行完毕为止

19.3.2 一致性

事务具有一致性指的是,如果数据库在执行事务之前是一致的,那么在事务执行之后,无论事务是否执行成功,数据库也应该仍然是一致的。

19.3.3 隔离性

事务的隔离性指的是,即使数据库中有多个事务并发地执行,各个事务之间也不会互相影响,并且在并发状态下执行的事务和串行执行的事务产生的结果完全相同。

因为Redis使用单线程的方式来执行事务,并且服务器保证,在执行事务期间不会对事务进行中断,因此,Redis的事务总是以串行的方式运行的,并且事务也总是具有隔离性的。

19.3.4 耐久性

只有当服务器运行在AOF持久性模式下,并且appendfsync选项的值为always时,程序总会在执行命令之后调用同步函数,将命令数据真正的保存到硬盘里面,因此这种配置下的事务是具有耐久性的