Redis | 事物源码阅读 —— watch

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介: Redis | 事物源码阅读 —— watch

      上次阅读事务的源码时,主要阅读了 multi、exec 和 discard 相关的命令,文章地址如下:Redis | 事务源码阅读,这次把上次没有看完的部分看接着看一下。

Redis 的 watch 数据结构

       关于 watch 存在于几个数据结构当中,基本上在 redisServer、redisCient 和 redisDb 当中,它们大致的关系如下:

       该示意图是一个大致的示意图,不太保证准确。其中,dict 用于保存监视的 key,而 dict 的 value 是一个 list 数据结构,list 中保存了监视指定 key 的客户端。

Redis 的 watch 命令

      Redis 的 watch 命令用于监控指定的 key,它的代码在 multi.c 文件中,其代码如下。

/**
 * watch命令
 */
void watchCommand(redisClient *c) {
    int j;
    if(c->flags & REDIS_MULTI) {
        addReplyError(c,"WATCH inside MULTI is not allowed");
        return;
    }
    for(j =1; j < c->argc; j++)
        watchForKey(c,c->argv[j]);
    addReply(c,shared.ok);
}

       我们知道,watch 命令不能在 multi 命令后面执行。因此在 watchCommand 函数中首先判断是否执行过 multi 命令,如果执行了 multi 命令则返回错误。


       然后调用 watchForKey 来把指定的 key 进行添加,watchForKey 的代码如下:

/**
 * 监控指定的键
 */
void watchForKey(redisClient *c, robj *key) {
    list *clients =NULL;
    listIter li;
    listNode *ln;
    watchedKey *wk;
    /* Check if we are already watching for this key */
    // 检查指定的键是否已经被监控
    listRewind(c->watched_keys,&li);
    while((ln =listNext(&li))) {
        wk =listNodeValue(ln);
        if(wk->db == c->db &&equalStringObjects(key,wk->key))
            return; /* Key already watched */
    }
    /* This key is not already watched in this DB. Let's add it */
    // 指定的键没有被监控
    clients =dictFetchValue(c->db->watched_keys,key);
    if(!clients) { 
        clients =listCreate();
        // 将key加入指定dict
        dictAdd(c->db->watched_keys,key,clients);
        incrRefCount(key);
    }
    listAddNodeTail(clients,c);
    /* Add the new key to the list of keys watched by this client */
    wk =zmalloc(sizeof(*wk));
    wk->key = key;
    wk->db = c->db;
    incrRefCount(key);
    // 将key加入指定的list
    listAddNodeTail(c->watched_keys,wk);
}

       看上面的代码,首先会查找指定的 keys 是否加入监视列表,如果没有加入则进行加入。其中在调用 listAddNodeTail 函数时,第二个参数 wk 的类型是 watchedKey 结构体,该结构体的定义如下:

/* In the client->watched_keys list we need to use watchedKey structures
 * as in order to identify a key in Redis we need both the key name and the
 * DB */
typedef struct watchedKey {
    robj *key;
    redisDb *db;
} watchedKey;

       可以看到,该结构体中有指定的 key 和所属的数据库(一个 Redis 实例默认有 16 个 DB,我们可以通过 select 命令来切换数据库)。

Redis 的 unwatch 命令

      Redis 的 unwatch 命令比较简单,就是取消指定的被监控的 key,该代码如下。

/**
 * unwatch命令
 */
void unwatchCommand(redisClient *c) {
    unwatchAllKeys(c);
    c->flags &=(~REDIS_DIRTY_CAS);
    addReply(c,shared.ok);
}

      首先需要调用 unwatchAllKeys 函数,取消监控相关的 key,然后将 redisClient 中的 flags 的 REDIS_DIRTY_CAS 位进行复位。unwatchAllKeys 的源码如下:

/* Unwatch all the keys watched by this client. To clean the EXEC dirty
 * flag is up to the caller. */
void unwatchAllKeys(redisClient *c) {
    listIter li;
    listNode *ln;
    if(listLength(c->watched_keys) ==0) return;
    listRewind(c->watched_keys,&li);
    while((ln =listNext(&li))) {
        list *clients;
        watchedKey *wk;
        /* Lookup the watched key -> clients list and remove the client
         * from the list */
        wk =listNodeValue(ln);
        clients =dictFetchValue(wk->db->watched_keys, wk->key);
        redisAssertWithInfo(c,NULL,clients !=NULL);
        listDelNode(clients,listSearchKey(clients,c));
        /* Kill the entry at all if this was the only client */
        if(listLength(clients) ==0)
            // 函数dict中的key
            dictDelete(wk->db->watched_keys, wk->key);
        /* Remove this watched key from the client->watched list */
        // 删除list中的key
        listDelNode(c->watched_keys,ln);
        decrRefCount(wk->key);
        zfree(wk);
    }
}

       代码中将 list 和 dict 中对应的 key 进行了删除。其实并不是删除 key ,而是删除监视的 key 对应的客户端,如果删除的后没有客户端再监控该 key 了,则把对应的 dict 也进行删除。

如何监控变量是否被改变

       在 Redis 中使用 watch 命令对 key 进行监控后,Redis 如何知道哪个被监控的 key 被修改了呢?在 multi.c 文件中有一个 touchWatchedKey 的函数,该函数用来告诉客户端,它监控的 key 被修改了。代码如下:

/* "Touch" a key, so that if this key is being WATCHed by some client the
 * next EXEC will fail. */
void touchWatchedKey(redisDb *db, robj *key) {
    list *clients;
    listIter li;
    listNode *ln;
    // 判断该DB是否有监视的Key
    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 */
    listRewind(clients,&li);
    // 迭代所有的有该key的client,并为它们的flags标志位增加REDIS_DIRTY_CAS
    while((ln =listNext(&li))) {
        redisClient *c =listNodeValue(ln);
        c->flags |= REDIS_DIRTY_CAS;
    }
}

    在 touchWatchedKey 函数中,先判断当前操作的 key 是否是被监控的 key,如果是监控的 key,就对 redisClient 的 flags 标志的 REDIS_DIRTY_CAS 位进行置位。那么哪些操作会调用 touchWatchedKey 函数呢?我们来接着看代码,在 db.c 文件中有一个 signalModifiedKey 函数,该函数调用了 touchWatchedKey 函数,那么又是谁调用了 signalModifiedKey 函数呢?看下面的截图。

      从图中可以看到,调用 signalModifiedKey 函数的地方很多,几乎所有对 key 进行操作的函数都会调用 signalModifiedKey 函数,比如 setKey、delCommand、hsetCommand 等。也就所有修改 key 的值的函数,都会去调用 signalModifiedKey 来检查是否修改了被 watch 的 key,只要是修改了被 watch 的 key,就会对 redisClient 的 flags 设置 REDIS_DIRTY_CAS 位。当执行execCommand 函数时,就会检查是否存 redisClient 的 flags 的 REDIS_DIRTY_CAS 位是否被置位,如果置位则不执行事务。关于 exeCommand 函数的介绍在上篇文章中介绍过,稍后简单的进行回顾。

       同样的,除了存在 signalModifiedKey 函数以外,还有一个 signalFlushedDb 函数,该函数与 signalModifiedKey 函数的功能类似。就不再赘述了。

回顾 execCommand 函数

       上篇文章介绍了 execCommand 函数,这里我们进行一个简单的回顾,关键是看执行 execCommand 函数时的对于 flags 标志位的判断。代码如下:

       上面的关键代码 if 中就是对 REDIS_DIRTY_CAS 的判断,除了判断是否对 REDIS_DIRTY_CAS 置位外,还对 REDIS_DIRTY_EXEC 进行了判断,当开启事务后,如果有输入错误的命令,或者命令使用不正确时会将 redisClient 的 flags 标志位的 REDIS_DIRTY_EXEC 置位。那么,当提交了 exec 后就会给出相应的提示。

总结

       关于 Redis 事务的源码就基本上看完了。主要包括 multi、exec、discard 三个事务命令的源码,还有 watch、unwatch 两个用于监视 key 的乐观锁命令的源码,包括它们数据结构,还包括一些和事务相关的标志位,分别是 REDIS_MULTI、REDIS_DIRTY_CAS 和 REDIS_DIRTY_EXEC,第一个是用于事务相关的,后两个是用于影响 exec 命令执行的标志。


       在看源码的时候,我发现我阅读源码的顺序是有问题的。因为一些关键的数据结构我并没有去看,因此在读源码的时候不知道一些数据结构之间的关系,需要返回去看相关的数据结构。但是我觉得这样看好像也没有什么问题,有些数据结构就算我提前去看,也未必能够明白其用意。

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
4月前
|
存储 NoSQL 算法
Redis系列-13.Redis经典五大类型源码及底层实现(一)(上)
Redis系列-13.Redis经典五大类型源码及底层实现(一)
77 0
|
4月前
|
存储 NoSQL Redis
redis源码学习
redis源码学习
|
2月前
|
存储 缓存 NoSQL
【Redis技术进阶之路】「底层源码解析」揭秘高效存储模型与数据结构底层实现(字典)(一)
【Redis技术进阶之路】「底层源码解析」揭秘高效存储模型与数据结构底层实现(字典)
41 0
|
2月前
|
存储 NoSQL 算法
【Redis技术进阶之路】「底层源码解析」揭秘高效存储模型与数据结构底层实现(字典)(二)
【Redis技术进阶之路】「底层源码解析」揭秘高效存储模型与数据结构底层实现(字典)
54 0
|
23天前
|
人工智能 前端开发 Java
Java语言开发的AI智慧导诊系统源码springboot+redis 3D互联网智导诊系统源码
智慧导诊解决盲目就诊问题,减轻分诊工作压力。降低挂错号比例,优化就诊流程,有效提高线上线下医疗机构接诊效率。可通过人体画像选择症状部位,了解对应病症信息和推荐就医科室。
185 10
|
2月前
|
存储 机器学习/深度学习 NoSQL
作者推荐 |【Redis技术进阶之路】「底层源码解析」揭秘高效存储模型与数据结构底层实现(链表)(二)
作者推荐 |【Redis技术进阶之路】「底层源码解析」揭秘高效存储模型与数据结构底层实现(链表)
24 0
|
2月前
|
存储 缓存 NoSQL
作者推荐 |【Redis技术进阶之路】「底层源码解析」揭秘高效存储模型与数据结构底层实现(链表)(一)
作者推荐 |【Redis技术进阶之路】「底层源码解析」揭秘高效存储模型与数据结构底层实现(链表)
30 0
|
2月前
|
存储 NoSQL 网络协议
读懂Redis源码,我总结了这7点心得
读懂Redis源码,我总结了这7点心得
|
4月前
|
缓存 NoSQL 关系型数据库
Redis 7.0 源码调试环境搭建与源码导读技巧
Redis 7.0 源码调试环境搭建与源码导读技巧
63 0
|
4月前
|
NoSQL 算法 Redis
redis7.0源码阅读(五):跳表(skiplist)
redis7.0源码阅读(五):跳表(skiplist)
67 1