redis源码分析之事务Transaction(上)

这周学习了一下redis事务功能的实现原理,本来是想用一篇文章进行总结的,写完以后发现这块内容比较多,而且多个命令之间又互相依赖,放在一篇文章里一方面篇幅会比较大,另一方面文章组织结构会比较乱,不容易阅读。因此把事务这个模块整理成上下两篇文章进行总结。

原文地址:http://www.jianshu.com/p/acb97d620ad7

这篇文章我们重点分析一下redis事务命令中的两个辅助命令:watch跟unwatch。

一、redis事务辅助命令简介

依然从server.c文件的命令表中找到相应的命令以及它们对应的处理函数。

//watch,unwatch两个命令我们把它们叫做redis事务辅助命令
{"watch",watchCommand,-2,"sF",0,NULL,1,-1,1,0,0},
{"unwatch",unwatchCommand,1,"sF",0,NULL,0,0,0,0,0},
  1. watch,用于客户端关注某个key,当这个key的值被修改时,整个事务就会执行失败(注:该命令需要在事务开启前使用)。
  2. unwatch,用于客户端取消已经watch的key。

用法举例如下:

clientA

127.0.0.1:6379> watch a
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set b b
QUEUED
//在执行前插入clientB的操作如下,事务就会执行失败
127.0.0.1:6379> exec
(nil)
127.0.0.1:6379>

clientB

127.0.0.1:6379> set a aa
OK
127.0.0.1:6379>

二、redis事务辅助命令源码分析

在看具体执行函数之前首先了解几个数据结构:

//每个客户端对象中有一个watched_keys链表来保存已经watch的key
typedef struct client {
    list *watched_keys;
}
//上述链表中每个节点的数据结构
typedef struct watchedKey {
    //watch的key
    robj *key;
    //指向的DB,后面细说
    redisDb *db;
} watchedKey;

关于事务的几个命令所对应的函数都放在了multi.c文件中。

一起看下watch命令对应处理函数的源码:

void watchCommand(client *c) {
    int j;
    //如果客户端处于事务状态,则返回错误信息
    //由此可以看出,watch必须在事务开启前使用
    if (c->flags & CLIENT_MULTI) {
        addReplyError(c,"WATCH inside MULTI is not allowed");
        return;
    }
    //依次watch客户端的各个参数(这里说明watch命令可以一次watch多个key)
    //注:0表示命令本身,所以参数从1开始
    for (j = 1; j < c->argc; j++)
        watchForKey(c,c->argv[j]);
    //返回结果
    addReply(c,shared.ok);
}

//具体的watch操作,代码较长,慢慢分析
void watchForKey(client *c, robj *key) {
    list *clients = NULL;
    listIter li;
    listNode *ln;
    //上面已经提到了数据结构
    watchedKey *wk;

    //首先判断key是否已经被客户端watch
    //listRewind这个函数在发布订阅那篇文章里也有,就是把客户端的watched_keys赋值给li
    listRewind(c->watched_keys,&li);
    while((ln = listNext(&li))) {
        wk = listNodeValue(ln);
        //这里一个wk节点中有db,key两个字段
        if (wk->db == c->db && equalStringObjects(key,wk->key))
            return;
    }
    //开始watch指定key
    //整个watch操作保存了两套数据结构,一套是在db->watched_keys中的字典结构,如下:
    clients = dictFetchValue(c->db->watched_keys,key);
    //如果是key第一次出现,则进行初始化
    if (!clients) {
        clients = listCreate();
        dictAdd(c->db->watched_keys,key,clients);
        incrRefCount(key);
    }
    //把当前客户端加到该key的watch链表中
    listAddNodeTail(clients,c);
    //另一套是在c->watched_keys中的链表结构:如下
    wk = zmalloc(sizeof(*wk));
    //初始化各个字段
    wk->key = key;
    wk->db = c->db;
    incrRefCount(key);
    //加入到链表最后
    listAddNodeTail(c->watched_keys,wk);
}

整个watch的数据结构比较复杂,我这里画了一张图方便理解:

简单解释一下上面的图,首先redis把每个客户端连接包装成了一个client对象,上图中db,watch_keys就是其中的两个字段(client对象里面还有很多其他字段,包括上篇文章中提到的pub/sub)。

  1. db字段指向给该client对象分配的储存空间,db对象中也含有一个watched_keys字段,是字典类型(也就是哈希表),以想要watch的key做key,存储的链表则是所有watch该key的客户端。
  2. watch_keys字段则是一个链表类型,每个节点类型为watch_key,其中包含两个字段,key表示watch的key,db则指向了当前client对象的db字段,如上图。

看完watch命令的源码以后,再来看一下unwatch命令,如果搞明白了上面提到的两套数据结构,那么看unwatch的源码应该会比较容易,毕竟就是删除数据结构中对应的内容。

void unwatchCommand(client *c) {
    //取消watch所有key
    unwatchAllKeys(c);
    //修改客户端状态
    c->flags &= (~CLIENT_DIRTY_CAS);
    addReply(c,shared.ok);
}

//取消watch的key
void unwatchAllKeys(client *c) {
    listIter li;
    listNode *ln;
    //如果客户端没有watch任何key,则直接返回
    if (listLength(c->watched_keys) == 0) return;
    //注意这里操作的是链表字段
    listRewind(c->watched_keys,&li);
    while((ln = listNext(&li))) {
        list *clients;
        watchedKey *wk;
        //遍历取出该客户端watch的key
        wk = listNodeValue(ln);
        //取出所有watch了该key的客户端,这里则是字典(即哈希表)
        clients = dictFetchValue(wk->db->watched_keys, wk->key);
        //空指针判断
        serverAssertWithInfo(c,NULL,clients != NULL);
        //从watch列表中删除该客户端
        listDelNode(clients,listSearchKey(clients,c));
        //如果key只有一个当前客户端watch,则删除
        if (listLength(clients) == 0)
            dictDelete(wk->db->watched_keys, wk->key);
        //从当前client的watch列表中删除该key
        listDelNode(c->watched_keys,ln);
        //减少引用数
        decrRefCount(wk->key);
        //释放内存
        zfree(wk);
    }
}

最后我们考虑一下watch机制的触发时机,现在我们已经把想要watch的key加入到了watch的数据结构中,可以想到触发watch的时机应该是修改key的内容时,通知到所有watch了该key的客户端。

感兴趣的用户可以任意选一个修改命令跟踪一下源码,例如set命令,我们发现所有对key进行修改的命令最后都会调用touchWatchedKey()函数,而该函数源码就位于multi.c文件中,该函数就是触发watch机制的关键函数,源码如下:

//这里入参db就是客户端对象中的db,上文已经提到,不赘述
void touchWatchedKey(redisDb *db, robj *key) {
    list *clients;
    listIter li;
    listNode *ln;
    //保存watchkey的字典为空,则返回
    if (dictSize(db->watched_keys) == 0) return;
    //注意这里操作的是字典(即哈希表)数据结构
    clients = dictFetchValue(db->watched_keys, key);
    //如果没有客户端watch该key,则返回
    if (!clients) return;
    //把client赋值给li
    listRewind(clients,&li);
    //遍历watch了该key的客户端,修改他们的状态
    while((ln = listNext(&li))) {
        client *c = listNodeValue(ln);
        c->flags |= CLIENT_DIRTY_CAS;
    }
}

跟我们猜测的一样,就是每当key的内容被修改时,则遍历所有watch了该key的客户端,设置相应的状态为CLIENT_DIRTY_CAS。

三、redis事务辅助命令总结

上面就是redis事务命令中watch,unwatch的实现原理,其中最复杂的应该就是watch对应的那两套数据结构了,跟之前的pub/sub类似,都是使用链表+哈希表的结构存储,另外也是通过修改客户端的状态位FLAG来通知客户端。

代码比较多,而且C++代码看上去会比较费劲,需要慢慢读,反复读。

时间: 2024-12-30 09:12:23

redis源码分析之事务Transaction(上)的相关文章

redis源码分析之事务Transaction(下)

接着上一篇,这篇文章分析一下redis事务操作中multi,exec,discard三个核心命令. 原文地址:http://www.jianshu.com/p/e22615586595 看本篇文章前需要先对上面文章有所了解: redis源码分析之事务Transaction(上) 一.redis事务核心命令简介 redis事务操作核心命令: //用于开启事务 {"multi",multiCommand,1,"sF",0,NULL,0,0,0,0,0}, //用来执行事

[Android]Fragment源码分析(三) 事务

Fragment管理中,不得不谈到的就是它的事务管理,它的事务管理写的非常的出彩.我们先引入一个简单常用的Fragment事务管理代码片段: FragmentTransaction ft = this.getSupportFragmentManager().beginTransaction(); ft.add(R.id.fragmentContainer, fragment, "tag"); ft.addToBackStack("<span style="fo

redis源码分析4---结构体---跳跃表

redis源码分析4---结构体---跳跃表 跳跃表是一种有序的数据结构,他通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的: 跳跃表支持平均O(logN),最坏O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点.性能上和平衡树媲美,因为事先简单,常用来代替平衡树. 在redis中,只在两个地方使用了跳跃表,一个是实现有序集合键,另一个是在集群节点中用作内部数据结构. 1 跳跃表节点 1.1 层 层的数量越多,访问其他节点的速度越快: 1.2 前进指针 遍历举例

redis源码分析3---结构体---字典

redis源码分析3---结构体---字典 字典,简单来说就是一种用于保存键值对的抽象数据结构: 注意,字典中每个键都是独一无二的:在redis中,内部的redis的数据库就是使用字典作为底层实现的: 1 字典的实现 在redis中,字典是使用哈希表作为底层实现的,一个hash表里面可以有多个hash表节点,而每个hash表节点就保存了字典中的一个键值对: hash表定义 table属性是一个数组,数组中的每个元素都是一个指向dictEntry结构的指针,每个dictEntry结构保存着一个键值

redis 源码分析(一) 内存管理

一,redis内存管理介绍 redis是一个基于内存的key-value的数据库,其内存管理是非常重要的,为了屏蔽不同平台之间的差异,以及统计内存占用量等,redis对内存分配函数进行了一层封装,程序中统一使用zmalloc,zfree一系列函数,其对应的源码在src/zmalloc.h和src/zmalloc.c两个文件中,源码点这里. 二,redis内存管理源码分析 redis封装是为了屏蔽底层平台的差异,同时方便自己实现相关的函数,我们可以通过src/zmalloc.h 文件中的相关宏定义

redis源码分析之内存布局

redis源码分析之内存布局 1. 介绍 众所周知,redis是一个开源.短小.高效的key-value存储系统,相对于memcached,redis能够支持更加丰富的数据结构,包括: 字符串(string) 哈希表(map) 列表(list) 集合(set) 有序集(zset) 主流的key-value存储系统,都是在系统内部维护一个hash表,因为对hash表的操作时间复杂度为O(1).如果数据增加以后,导致冲突严重,时间复杂度增加,则可以对hash表进行rehash,以此来保证操作的常量时

基于TCP网络通信的自动升级程序源码分析-客户端请求服务器上的升级信息

每次升级,客户端都会获取服务器端存放在upgradefile文件夹下的需要升级的文件和升级信息配置文件(即upgradeconfig.xml文件) 我们来看一下代码 //升级信息配置文件相对应的类 ( 升级信息配置文件是由这个类转化成的) private UpgradeConfig upgradeConfig = null; //客户端存储升级配置文件的地址 是放在客户端根目录下的 (就是把服务器 upgradefile/upgradeconfig.xml下载到客户端存放的位置) string

Redis源码分析(一)--Redis结构解析

从今天起,本人将会展开对Redis源码的学习,Redis的代码规模比较小,非常适合学习,是一份非常不错的学习资料,数了一下大概100个文件左右的样子,用的是C语言写的.希望最终能把他啃完吧,C语言好久不用,快忘光了.分析源码的第一步,先别急着想着从哪开始看起,先浏览一下源码结构,可以模块式的渐入,不过比较坑爹的是,Redis的源码全部放在在里面的src目录里,一下90多个文件统统在里面了,所以我选择了拆分,按功能拆分,有些文件你看名字就知道那是干什么的.我拆分好后的而结果如下: 11个包,这样每

redis源码分析(1)--makefile和目录结构分析

一.redis源码编译 redis可以直接在官网下载(本文使用版本 3.0.7):https://redis.io/download 安装: $ tar xzf redis-3.0.7.tar.gz $ cd redis-3.0.7 $ make make执行以后主要编译产物在src/redis-server src/redis-cli 如果想把redis-server直接install到可执行目录/usr/local/bin,还需要执行: $ make install Run Redis wi