文章目录
18. 事务
Redis 是通过 MULTI
、EXEC
、WATCH
等命令来实现事务功能的。事务提供了一种将多个命令请求打包,然后一次性、按顺序的执行多个命令的机制,并且在事务执行期间,服务器不会中断事务而该去执行其他客户端的命令请求,它会将事务中 的所有命令执行完毕,才去处理其他客户端的命令请求。
18.1. 事务的实现
事务开始到结束三个阶段:
-
事务开始
-
命令入队
-
事务执行
18.1.1. 事务开始
MULTI
命令的执行标志着事务的开始,这个命令可以将执行该命令的客户端从非事务状态切换至事务状态(通过在客户端状态的flags
属性中打开 REDIS_MULTI
标识来完成),这个命令的伪代码如下:
def MULTI():
# 打开事务标识
client.flags |= REDIS_MULTI;
# 返回 OK 回复
replyOK();
18.1.2. 命令入队
当一个客户端切换到事务状态之后,服务器会根据这个客户端发来的不同命令执行不同的操作:
- 如果客户端发送的命令为
EXEC
、DISCARD
、WATCH
、MULTI
四个命令中的一个,那么服务器立即执行这个命令; - 否则,服务器将这个命令放入一个事务队列里面,然后向客户端返回
QUEUE
回复。
18.1.3. 事务队列
每个Redis客户端都有自己的事务状态,这个事务状态保存在客户端状态的master属性里面:
typedef struct redisClient{
// ……
// 事务状态
multiState mstate; /* MULTI/EXEC state */
// ……
}redisClient;
事务状态包含一个事务队列,以及一个已入队命令的计数器(事务队列的长度):
typedef struct multiState{
// 事务队列,FIFO顺序
multiCmd *commands;
//已入队命令计数
int count;
}multiState;
事务队列是一个 multiCmd
类型数组,每个 multiCmd
结构保存着一个已入队命令的相关信息,包括指向命令实现函数的指针、命令的参数,以及参数的数量:
typedef struct multiCmd{
//参数
robj **argv;
//参数数量
int argc;
//命令指针
struct redisCommand *cmd;
}multiCmd;
事务队列先进先出。
18.1.4. 执行事务
当一个处于事务状态的客户端向服务器发送 EXEC
命令时,这个 EXEC
命令将立即被服务器执行。服务器会遍历这个客户端的事务队列,执行队列中保存的所有命令,最后将执行命令所得的结果全部返回客户端。
EXEC
命令的实现伪代码:
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.mstate.count = 0
release_transaction_queue(client.mstate.commands)
# 将事务的实行结果返回给客户端
send_reply_to_client(client,reply_queue)
18.2. WATCH
命令的实现
WATCH
命令是一个乐观锁,他可以在 EXEC
命令执行之前,监视任意数量的数据库键,并在EXEC命令执行时,检查被监视的键是否至少有一个已经被修改过了,如果是的话,服务器将拒绝执行事务,并向客户端返回代表事务执行失败的空回复。
18.2.1. 使用 WATCH
命令监视数据库键
每个Redis数据库都保存着一个 watched_keys
字典,键是某个被监视的数据库键,值是链表(记录额所有监视相应数据库键的客户端):
typedef struct redisDb{
// ...
// 正在被WATCH命令监视的键
dict *watched_keys;
// ...
}redisDb;
通过这个字典,服务器可以清楚地知道哪个数据库键正在被监视,以及哪些客户端正在监视这些数据库键。
通过执行 WATCH
命令,客户端可以在 watched_keys
字典中与被监视的键进行关联。
18.2.2. 监视机制的触发
所有对数据库进行修改的命令,在执行之后都会调用 multi.c/touchWatchKey
函数对 watched_keys
字典进行检查,查看是否有客户端正在监视刚刚被命令修改过的数据库键,如果有的话,那么 touchWatchKey
函数会将监视被修改键的客户端的 REDIS_DIRTY_CAS
表示打开,表示该客户端的事务安全性已经被破坏。
touchWatchKey
函数的定义伪代码:
def toutchWatchKey(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
18.2.3. 判断事务是否安全
当服务器接收到一个客户端发来的 EXEC
命令时,服务器会根据这个客户端是否打开了 REDIS_DIRTY_CAS
标识来决定是否执行事务:
- 如果打开,则服务器会拒绝执行客户端提供的事务
- 没有打开,执行事务
18.3. 事务的ACID性质
用于检验事务功能的可靠性和安全性。
-
原子性
原子性指的是数据库将事务中的多个操作当做一个整体来执行,服务器要就就是执行事务中的所有操作,要么一个操作也不执行。
与传统的关系型数据库事务支持的原子性不同,redis不支持事务回滚机制,即使事务队列中的某个命令在执行期间出错,整个事务也会继续执行下去,直到事务队列中所有的命令都执行完毕
-
一致性
一致性指的是,如果数据库在执行事务之前是一致的,那么在事务执行之后,无论事务是否执行成功,数据库也应该仍然是一致的。
一致 指的是数据符合数据库本身的定义和要求,没有包含非法或者无效的错误数据。
下面是Redis事务可能出错的地方:
-
入队错误
事务入队时,出现了命令不存在或者命令格式不正确等情况,Redis将拒绝执行这个事务。因为服务器会拒绝执行入队过程中出现错误的事务,所以Redis事务的一致性不会被带有入队错误的事务影响。
-
执行错误
-
服务器停机
-
-
隔离性
隔离性是指:即使数据库中有多个事务并发地执行,各个事务之间也不会互相影响,并且在并发状态下执行的事务和串行执行的事务产生的结果完全相同。
-
耐久性
耐久性指的是:当一个事务执行完毕之后,执行这个事务所得的结果已经被保存到永久性存储介质(比如硬盘)里面了,即使服务器在事务执行完毕之后停机,执行事务所得的结果也不会丢失。