Redis享知享学:单机数据库实现:watched_keys和id属性

关于Redis单机数据库的实现,就要接近尾声,本篇将解析剩下的属性watched_keys和id。

watch命令能在事务开启后执行吗?watch后,事务失败与否,对于一个key需要重新watch么?本篇文章将尝试给出解答。

watched_keys

和之前解析的属性一样,watched_keys仍然是一个字典,它的出现是为了实现Redis事务的相关功能。

考虑这样一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 客户端01:
redis 127.0.0.1:6379> watch cgrw
OK
redis 127.0.0.1:6379> multi
OK
redis 127.0.0.1:6379> set cgrw cg01
QUEUED
// 客户端02:
redis 127.0.0.1:6379> set cgrw cg02
OK
// 继续客户端01:
redis 127.0.0.1:6379> exec
(nil)

客户端01在multi之前,watch了cgrw,之后设置了cgrw;如果在客户端01执行exec之前,客户端02又设置了cgrw,此时再在客户端执行exec,只会返回空。也就是说,通过watch命令,可以保证事务的安全性。

watch触发准备

数据库中的watched_keys扮演了什么角色呢?

形式上,它和blocking_keys属性一样,一个键对应一个链表,两者构成一个键值对并添加到字典,链表里都是watch了这个键的客户端。

watch命令会调用watchCommand(),底层实现的是watchForKey函数,该函数内数据库负责的部分:

1
2
3
4
5
6
7
8
9
10
11
// key 未被监视
// 根据 key ,将客户端加入到 DB 的监视 key 字典中
/* This key is not already watched in this DB. Let's add it */
// O(1)
clients = dictFetchValue(c->db->watched_keys,key);
if (!clients) {
clients = listCreate();
dictAdd(c->db->watched_keys,key,clients);
incrRefCount(key);
}
listAddNodeTail(clients,c);

和之前解析blocking_keys相关的部分异曲同工:无则创建链表,有则添加进链表。

同时上篇类似,客户端存在一个watched_keys的同名属性,它的表元素记录key和db,在watchForKey()中还会执行:

1
2
3
4
5
6
7
8
9
watchedKey *wk;
...
wk = zmalloc(sizeof(*wk));
wk->key = key;
wk->db = c->db;
incrRefCount(key);
listAddNodeTail(c->watched_keys,wk);

watchedKey可以类比readyList,上一篇已经很详细,就不从头赘述相关代码。

有一点提一下,在watchCommand()内:

1
2
3
4
5
// 不能在事务中使用
if (c->flags & REDIS_MULTI) {
addReplyError(c,"WATCH inside MULTI is not allowed");
return;
}

这解释了为什么watch不能在事务中调用。

watch触发

触发源于对touchWatchedKey()的调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void touchWatchedKey(redisDb *db, robj *key) {
list *clients;
listIter li;
listNode *ln;
// 如果数据库中没有任何 key 被监视,那么直接返回
if (dictSize(db->watched_keys) == 0) return;
// 取出数据库中所有监视给定 key 的客户端
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 */
// 打开所有监视这个 key 的客户端的 REDIS_DIRTY_CAS 状态
// O(N)
listRewind(clients,&li);
while((ln = listNext(&li))) {
redisClient *c = listNodeValue(ln);
c->flags |= REDIS_DIRTY_CAS;
}
}

前半部分没什么可说的,后面是遍历watch了key的客户端链表,将表示客户端的对象的flags属性设置为REDIS_DIRTY_CAS状态。

touchWatchedKey()封装在了signalModifiedKey()中,能进行watch触发的命令都会调用signalModifiedKey(),这样的命令太多,基本上只要是能修改key的命令都包括了。

watch触发后响应

客户端输入exec,会调用execCommand(),它会进行如下判断:

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
27
28
29
/* Check if we need to abort the EXEC because:
* 以下情况发生时,取消事务
*
* 1) Some WATCHed key was touched.
* 某些被监视的键已被修改(状态为 REDIS_DIRTY_CAS)
*
* 2) There was a previous error while queueing commands.
* 有命令在入队时发生错误(状态为 REDIS_DIRTY_EXEC)
*
* A failed EXEC in the first case returns a multi bulk nil object
* (technically it is not an error but a special behavior), while
* in the second an EXECABORT error is returned.
*
* 第一种情况返回多个空白 NULL 对象,
* 第二种情况返回一个 EXECABORT 错误。
*/
if (c->flags & (REDIS_DIRTY_CAS|REDIS_DIRTY_EXEC)) {
// 根据状态,决定返回的错误的类型
addReply(c, c->flags & REDIS_DIRTY_EXEC ? shared.execaborterr :
shared.nullmultibulk);
// 以下四句可以用 discardTransaction() 来替换
freeClientMultiState(c);
initClientMultiState(c);
c->flags &= ~(REDIS_MULTI|REDIS_DIRTY_CAS|REDIS_DIRTY_EXEC);
unwatchAllKeys(c);
goto handle_monitor;
}

在执行exec后,会判断当前客户端的flags的状态,如果状态为REDIS_DIRTY_CAS,则事务安全性破坏,最后会调用unwatchAllKeys()。当然,即使状态不为REDIS_DIRTY_CAS,exec命令最终仍会调用unwatchAllKeys(),用来清空该客户端之前watch的记录。

id

id就是数据库编号啊。

参考

Redis 设计与实现:国内解析Redis的开源资料;