简述:
redis 通过
multi 事务开始
exec 执行
watch 乐观锁
discard 取消事务
等命令来实现事务功能。事务提供了一种将多个命令请求打包,然后一次性,按顺序地执行多个命令的机制。
1 事务的实现
一个事务从开始到结束通常经历三个阶段
1. 事务开始
2. 命令入队
3. 事务执行
1.1 事务开始
multi 命令的执行标志着事务的开始。
multi 可以将执行该命令的客户端从非事务状态切至事务状态。通过客户端状态的 flags 属性中打开 redis_multi 标识来完成的。
1.2 命令入队
当一个客户端切换到事务状态,redis客户端会根据这个客服端发来的不通命令执行不通操作:
立即执行:exec,discard,watch,multi
放到事务队列里面:非exec,discard,watch,multi

1.3 事务队列
每个redis客服端都有自己的事务状态,这个事务状态保存在客服端状态的mstate 属性。
typedef struct redisClient {
//...
// 事务状态
multiState mstate; /* MULTI/EXEC state */
list *watched_keys; /* 正在被WATCH命令监视的键 */
} redisClient;
事务状态包含一个事务队列,以及一个已入队命令计数器(也可以说事务队列的长度)
typedef struct multiState {
/* 事务队列,FIFO 顺序 */
multiCmd *commands; /* Array of MULTI commands */
int count; /* 已入队命令计数 */
int minreplicas; /* MINREPLICAS for synchronous replication */
time_t minreplicas_timeout; /* MINREPLICAS timeout as unixtime. */
} multiState;
事务队列是一个multiCmd 类型的数组, 数组中每个multiCmd结构都保存了一个已入队命令的相关信息,包括执行命令实现函数的 指针,命令的参数,以及参数的数量。
/*
* 事务命令
*/
typedef struct multiCmd {
/* 参数 */
robj **argv;
/* 参数数量 */
int argc;
/* 命令指针 */
struct redisCommand *cmd;
} multiCmd;
事务对了以先进先出 的方式保存入队的命令,较先入队的命令会被放到数组前面。
举例:


1.4 执行事务
发送 exec,立即被服务器执行,服务器会遍历这个客服端的事务队列,执行队列中保存的所有命令,最后将执行命令所得结果全部返回给客服端。
例子返回:


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

客服端A事务会执行失败。
2.1 使用watch 命令监视数据库键
typedef struct redisDb {
dict *dict; // 数据库键空间,保存着数据库中的所有键值对
dict *expires; // 键的过期时间,字典的键为键,字典的值为过期事件 UNIX 时间戳
dict *watched_keys; // 正在被watch命令监视的键
int id; // 数据库号码
} redisDb;
通过 watched_keys 字典,服务器可以清楚知道哪些数据库键正在被监视,以及哪些客服端正在监视这些数据库键。
2.2 监视机制的触发
所有对数据库进行修改的命令, 比如set,lpush,等等,在执行之后都会调用multi.c、touchWatchKey 函数对 watched_keys 字典进行检查,查看是否有客服端正在监视刚刚修改过的数据库键,如果有的话,那么 touchWatchKey 函数会将监视被修改键的客服端的 redist_dirty_cas 表示打开,表示该客服端的事务安全性已经被破坏。

/*
* “触碰”一个键,如果这个键正在被某个/某些客户端监视着,
* 那么这个/这些客户端在执行 EXEC 时事务将失败。
*/
void touchWatchedKey(redisDb *db, robj *key) {
list *clients;
listIter li;
listNode *ln;
/* 字典为空,没有任何键被监视 */
if (dictSize(db->watched_keys) == 0) return;
/* 获取所有监视这个键的客户端 */
clients = dictFetchValue(db->watched_keys, key);
if (!clients) return;
/* Mark all the clients watching this key as REDIS_DIRTY_CAS */
/* Check if we are already watching for this key */
/* 遍历所有客户端,打开他们的 REDIS_DIRTY_CAS 标识 */
listRewind(clients, &li);
while ((ln = listNext(&li))) {
redisClient *c = listNodeValue(ln);
c->flags |= REDIS_DIRTY_CAS;
}
}
2.3 判断事务是否安全
当服务器接受到一个客服端发来的exec 命令,服务器会根据这个客服端是否打开了 redis_dirty_cas 标识来决定是否执行了事务。
