Redis源码剖析(经典版)

合集下载

Redis的字典(dict)rehash过程源代码解析

Redis的字典(dict)rehash过程源代码解析

Redis的字典(dict)rehash过程源代码解析Redis的内存存储结构是个⼤的字典存储,也就是我们通常说的哈希表。

Redis⼩到能够存储⼏万记录的CACHE,⼤到能够存储⼏千万甚⾄上亿的记录(看内存⽽定),这充分说明Redis作为缓冲的强⼤。

Redis的核⼼数据结构就是字典(dict),dict在数据量不断增⼤的过程中。

会遇到HASH(key)碰撞的问题,假设DICT不够⼤,碰撞的概率增⼤,这样单个hash 桶存储的元素会越来愈多,查询效率就会变慢。

假设数据量从⼏千万变成⼏万,不断减⼩的过程。

DICT内存却会造成不必要的浪费。

Redis的dict在设计的过程中充分考虑了dict⾃⼰主动扩⼤和收缩,实现了⼀个称之为rehash的过程。

使dict出发rehash的条件有两个:1)总的元素个数除 DICT桶的个数得到每⼀个桶平均存储的元素个数(pre_num),假设 pre_num > dict_force_resize_ratio,就会触发dict 扩⼤操作。

dict_force_resize_ratio = 5。

2)在总元素 * 10 < 桶的个数,也就是,填充率必须<10%,DICT便会进⾏收缩。

让total / bk_num 接近 1:1。

dict rehash扩⼤流程:源码函数调⽤和解析:dictAddRaw->_dictKeyIndex->_dictExpandIfNeeded->dictExpand,这个函数调⽤关系是须要扩⼤dict的调⽤关系,_dictKeyIndex函数代码:static int _dictKeyIndex(dict *d, const void *key){unsigned int h, idx, table;dictEntry *he;// 假设有须要。

对字典进⾏扩展if (_dictExpandIfNeeded(d) == DICT_ERR)return -1;// 计算 key 的哈希值h = dictHashKey(d, key);// 在两个哈希表中进⾏查找给定 keyfor (table = 0; table <= 1; table++) {// 依据哈希值和哈希表的 sizemask// 计算出 key 可能出如今 table 数组中的哪个索引idx = h & d->ht[table].sizemask;// 在节点链表⾥查找给定 key// 由于链表的元素数量通常为 1 或者是⼀个⾮常⼩的⽐率// 所以能够将这个操作看作 O(1) 来处理he = d->ht[table].table[idx];while(he) {// key 已经存在if (dictCompareKeys(d, key, he->key))return -1;he = he->next;}// 第⼀次进⾏执⾏到这⾥时,说明已经查找完 d->ht[0] 了// 这时假设哈希表不在 rehash 其中。

redisson-lock源码探析

redisson-lock源码探析

redisson-lock源码探析分布式锁⼀般情况下, 实现redis的分布式锁, 本质都是保证在⼀段时间内,当前线程对资源的独占.(这个⼀定时间, 是为了容错性)安全和活性失效保障最简单的算法只需具备3个特性就可以实现⼀个最低保障的分布式锁。

安全属性(Safety property): 独享(相互排斥)。

在任意⼀个时刻,只有⼀个客户端持有锁。

活性A(Liveness property A): ⽆死锁。

即便持有锁的客户端崩溃(crashed)或者⽹络被分裂(gets partitioned),锁仍然可以被获取。

活性B(Liveness property B): 容错。

只要⼤部分Redis节点都活着,客户端就可以获取和释放锁.单实例redis分布式锁的姿势单Redis实例实现分布式锁的正确⽅法在尝试克服上述单实例设置的限制之前,让我们先讨论⼀下在这种简单情况下实现分布式锁的正确做法,实际上这是⼀种可⾏的⽅案,尽管存在竞态,结果仍然是可接受的,另外,这⾥讨论的单实例加锁⽅法也是分布式加锁算法的基础。

获取锁使⽤命令:SET resource_name my_random_value NX PX 30000这个命令仅在不存在key的时候才能被执⾏成功(NX选项),并且这个key有⼀个30秒的⾃动失效时间(PX属性)。

这个key的值是“my_random_value”(⼀个随机值),这个值在所有的客户端必须是唯⼀的,所有同⼀key的获取者(竞争者)这个值都不能⼀样。

value的值必须是随机数主要是为了更安全的释放锁,释放锁的时候使⽤脚本告诉Redis:只有key存在并且存储的值和我指定的值⼀样才能告诉我删除成功。

可以通过以下Lua脚本实现:if redis.call("get",KEYS[1]) == ARGV[1] thenreturn redis.call("del",KEYS[1])elsereturn 0endRedisson lockredissonLock,实现了java的Lock接⼝, 可以像操作jdk中的lock接⼝⼀样操作分布式锁RLock对象完全符合Java的Lock规范。

Redis(一):服务启动及基础请求处理流程源码解析

Redis(一):服务启动及基础请求处理流程源码解析

Redis(⼀):服务启动及基础请求处理流程源码解析 redis是⽤c语⾔的写的缓存服务器,有⾼性能和多种数据类型⽀持的特性,⼴受互联⽹公司喜爱。

我们要分析其启动过程,⾸先就要先找到其⼊⼝。

当然我们应该是要先分析 Makefile ⽂件,然后找到最终编译成的⽂件,然后再顺势找到C语⾔⼊⼝ main(); 这⾥咱们就不费那事了,⼀是这事很枯燥,⼆是我也不知道找不找到得到。

所以,就直接找到⼊⼝吧: 在 src/server.c 中,main() 函数就是了。

引⽤⽹上⼤⽜的话归纳⼀下,main 函数执⾏的过程分以下⼏步:1. Redis 会设置⼀些回调函数,当前时间,随机数的种⼦。

回调函数实际上什么?举个例⼦,⽐如 Q/3 要给 Redis 发送⼀个关闭的命令,让它去做⼀些优雅的关闭,做⼀些扫尾清楚的⼯作,这个⼯作如果不设计回调函数,它其实什么都不会⼲。

其实 C 语⾔的程序跑在操作系统之上,Linux 操作系统本⾝就是提供给我们事件机制的回调注册功能,所以它会设计这个回调函数,让你注册上,关闭的时候优雅的关闭,然后它在后⾯可以做⼀些业务逻辑。

2. 不管任何软件,肯定有⼀份配置⽂件需要配置。

⾸先在服务器端会把它默认的⼀份配置做⼀个初始化。

3. Redis 在 3.0 版本正式发布之前其实已经有筛选这个模式了,但是这个模式,我很少在⽣产环境在⽤。

Redis 可以初始化这个模式,⽐较复杂。

4. 解析启动的参数。

其实不管什么软件,它在初始化的过程当中,配置都是由两部分组成的。

第⼀部分,静态的配置⽂件;第⼆部分,动态启动的时候,main,就是参数给它的时候进去配置。

5. 把服务端的东西拿过来,装载 Config 配置⽂件,loadServerConfig。

6. 初始化服务器,initServer。

7. 从磁盘装载数据。

8. 有⼀个主循环程序开始⼲活,⽤来处理客户端的请求,并且把这个请求转到后端的业务逻辑,帮你完成命令执⾏,然后吐数据,这么⼀个过程。

Redis源码解析:26集群(二)键的分配与迁移

Redis源码解析:26集群(二)键的分配与迁移

Redis源码解析:26集群(⼆)键的分配与迁移Redis集群通过分⽚的⽅式来保存数据库中的键值对:⼀个集群中,每个键都通过哈希函数映射到⼀个槽位,整个集群共分16384个槽位,集群中每个主节点负责其中的⼀部分槽位。

当数据库中的16384个槽位都有节点在处理时,集群处于上线状态;相反,如果数据库中有任何⼀个槽没有得到处理,那么集群处于下线状态。

所谓键的分配,实际上就是指槽位在集群节点中的分配;所谓键的迁移,实际上指槽位在集群节点间的迁移。

⼀:数据结构在集群最主要的数据结构,记录集群状态的clusterState结构体中,与槽位相关的属性有:clusterNode *slots[16384];clusterNode *migrating_slots_to[16384];clusterNode *importing_slots_from[16384];zskiplist *slots_to_keys;slots数组记录了16384个槽位,分别由哪个集群节点负责:⽐如server->cluster.slots[0] = node,这说明0号槽位由node节点负责;migrating_slots_to数组记录了16384个槽位中,当前节点所负责的槽位正在迁出到哪个节点。

⽐如server.cluster->migrating_slots_to[0] = node,这说明当前节点负责的0号槽位,正在迁出到node节点;importing_slots_from数组记录了16384个槽位中,当前节点正在从哪个节点将某个槽位迁⼊到本节点中;⽐如server.cluster->importing_slots_from[0] = node,这说明当前节点正在从node节点处迁⼊0号槽位;通过以上这些属性,可以快速得到某个槽位由哪个节点负责,以及该槽位正在迁出或迁⼊到哪个节点。

slots_to_keys是个跳跃表,该跳跃表中,以槽位号为分数进⾏排序。

(转)Redis运行流程源码解析

(转)Redis运行流程源码解析

(转)Redis运⾏流程源码解析本⽂分析基于版本。

概述通过定义⼀个 struct redisServer 类型的全局变量server 来保存服务器的相关信息(⽐如:配置信息,统计信息,服务器状态等等)。

启动时通过读取配置⽂件⾥边的信息对server进⾏初始化(如果没有指定配置⽂件,将使⽤默认值对sever进⾏初始化),初始化的内容有:起监听端⼝,绑定有新连接时的回调函数,绑定服务器的定时函数,虚拟内存初始化,log初始化等等。

启动初始化服务器配置先来看看redis 的main函数的⼊⼝Redis.c:1694int main(int argc, char **argv) {time_t start;initServerConfig();if (argc == 2) {if (strcmp(argv[1], "-v") == 0 ||strcmp(argv[1], "--version") == 0) version();if (strcmp(argv[1], "--help") == 0) usage();resetServerSaveParams();loadServerConfig(argv[1]);} else if ((argc > 2)) {usage();} else {...}if (server.daemonize) daemonize();initServer();...initServerConfig初始化全局变量 server 的属性为默认值。

如果命令⾏指定了配置⽂件, resetServerSaveParams重置对落地备份的配置(即重置为默认值)并读取配置⽂件的内容对全局变量server 再进⾏初始化,没有在配置⽂件中配置的将使⽤默认值。

如果服务器配置成后台执⾏,则对服务器进⾏ daemonize。

initServer初始化服务器,主要是设置信号处理函数,初始化事件轮询,起监听端⼝,绑定有新连接时的回调函数,绑定服务器的定时函数,初始化虚拟内存和log等等。

Redis源码解析-全览

Redis源码解析-全览

Redis源码解析-全览通过阅读 Redis 源码,可以学习和掌握到的计算机系统设计思想根据 Redis 不同的功能特性,分线条学习每个功能特性上涉及的关键技术和设计思想对于Redis的代码架构,需要掌握以下两类内容代码的⽬录结构和作⽤划分,⽬的是理解 Redis 代码的整体架构,以及所包含的代码功能类别;系统功能模块与对应代码⽂件,⽬的是了解 Redis 实例提供的各项功能及其相应的实现⽂件,以便后续深⼊学习。

对于阅读 Redis 源码来说,要先从整体上掌握源码的结构,所以需要先形成⼀幅 Redis 源码的全景图()deps ⽬录这个⽬录主要包含了 Redis 依赖的第三⽅代码库,包括 Redis 的 C 语⾔版本客户端代码 hiredis、jemalloc 内存分配器代码(⽤来替换 glibc 库的内存分配器)、readline 功能的替代代码 linenoise,以及 lua 脚本代码。

这部分代码的⼀个显著特点,就是它们可以独⽴于 Redis src ⽬录下的功能源码进⾏编译,也就是说,它们可以独⽴于 Redis 存在和发展。

src ⽬录这个⽬录⾥⾯包含了 Redis 所有功能模块的代码⽂件,也是 Redis 源码的重要组成部分。

src ⽬录下只有⼀个 modules ⼦⽬录,其中包含了⼀个实现Redis module 的⽰例代码。

剩余的源码⽂件都是在 src ⽬录下。

tests ⽬录这个⽬录⾥⾯是⽤于功能模块测试和单元测试的代码。

Redis 实现的测试代码可以分成四部分,分别是单元测试(对应 unit ⼦⽬录),Redis Cluster 功能测试(对应 cluster ⼦⽬录)、哨兵功能测试(对应 sentinel ⼦⽬录)、主从复制功能测试(对应 integration ⼦⽬录)。

这些⼦⽬录中的测试代码使⽤了 T cl 语⾔(通⽤的脚本语⾔)进⾏编写,主要⽬的就是⽅便进⾏测试。

utils ⽬录这个⽬录⾥⾯是在 Redis 开发过程中的⼀些辅助性功能,包括⽤于创建 Redis Cluster 的脚本、⽤于测试 LRU 算法效果的程序,以及可视化 rehash 过程的程序。

Redis源码剖析--对象object

Redis源码剖析--对象object

Redis源码剖析--对象objectRedis在这些数据结构的基础上构建了对用户可见的五种类型,分别是string、hash、list、set 和zset,为了更方便的使用这五种数据类型,Redis定义了RedisObject结构体来表示它们。

今天,我们就一起来看看RedisObject是如何构建的!(如果底层结构不熟悉的,可以点击上述)RedisObject数据结构在server.h文件中,给出了RedisObject的结构体定义,我们一起来看看。

typedef struct redisObject {unsigned type:4;unsigned encoding:4;unsigned lru:LRU_BITS; // LRU_BITS为24int refcount;void *ptr;} robj;其中,ptr指向对象中实际存放的值,这里不需要过多解释,针对其他四个结构体参数,作如下说明:类型typeRedis的对象有五种类型,分别是string、hash、list、set和zset,type属性就是用来标识着五种数据类型。

type占用4个bit位,其取值和类型对应如下:#define OBJ_STRING 0#define OBJ_LIST 1#define OBJ_SET 2#define OBJ_ZSET 3#define OBJ_HASH 4编码类型encodingRedis对象的编码方式由encoding参数指定,也就是表示ptr指向的数据以何种数据结构作为底层实现。

该字段也占用4个bit位。

其取值和对应类型对应如下:#define OBJ_ENCODING_RAW 0 /* Raw representation */#define OBJ_ENCODING_INT 1 /* Encoded as integer */#define OBJ_ENCODING_HT 2 /* Encoded as hash table */#define OBJ_ENCODING_ZIPMAP 3 /* Encoded as zipmap */#define OBJ_ENCODING_LINKEDLIST 4 /* Encoded as regular linked list */#define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */#define OBJ_ENCODING_INTSET 6 /* Encoded as intset */#define OBJ_ENCODING_SKIPLIST 7 /* Encoded as skiplist */#define OBJ_ENCODING_EMBSTR 8 /* Embedded sds string encoding */#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */在Redis3.2.5版本中,zipmap已不再使用,此处也不再讨论。

Redis Sentinel源码分析(一)

Redis Sentinel源码分析(一)

Redis Sentinel源码分析(一)Redis Sentinel源码分析(一)2014-03-31 18:59 5454人阅读评论(0)收藏举报分类:开源学习分享(24)Redis(12)版权声明:本文为博主原创文章,未经博主允许不得转载。

Base 2.8.7在代码分析前,先总体介绍下sentinel 的机制。

1. 下线定义sentinel对下线有两种定义:a.主观下线(sdown):sentinel实例本身对服务实例的判断b.客观下线(odown):多个sentinel实例对同一个服务SDOWN的状态做出协商后的判断,只有master才可能在odown状态简单的说,一个sentinel单独做出的判断只能是sdown,是没有任何官方效力的,只有多个sentinel大家商量好,得到一致,才能将某个master状态置为odown,只有确定master odown状态后,才能做后续fail over的操作2. 通信sentinel与maste/slave的交互主要包括:a.PING:sentinel向其发送PING以了解其状态(是否下线):sentinel向其发送INFO以获取replication相关的信息c.PUBLISH:sentinel向其监控的master/slave发布本身的信息及master相关的配置d.SUBSCRIBE:sentinel通过订阅master/slave的”__sentinel__:hello“频道以获取其它正在监控相同服务的sentinelsentinel与sentinel的交互主要包括:a.PING:sentinel向slave发送PING以了解其状态(是否下线)b.SENTINEL is-master-down-by-addr:和其他sentinel协商master状态,如果master odown,则投票选出leader 做fail over3. fail over一次完整的fail over包括以下步骤:a. sentinel发现master下线,则标记master sdownb. 和其他sentinel协商以确定master状态是否odownc. 如果master odown,则选出leaderd. 当选为leader的sentinel选出一个slave做为master,并向该slave发送slaveof no one命令以转变slave角色为mastere. 向已下线的master及其他slave发送slaveof xxxx命令使其作为新当选master的slave本篇介绍Redis Sentinel 初始化及定时事件注册相关的代码,监控&amp;fail over相关的代码在下一篇中介绍redis sentinle可以执行./redis-sentinel sentinel.conf或./redis-server --sentinel sentinel.con运行sentinel程序其初始化流程和redis-server执行一致,程序初始化会判断是否处于sentinel模式,如果处于sentinel模式,则会完成一些sentinel相关的初始化工作,主要包括:1)读取sentinel相关配置2)初始化sentinel状态,并添加master、sentinel及slave 到相应的字典3)注册sentinel相关的time event(周期性执行)redis-sentinel和redis-server是同一个程序,查看Makefile 文件可知:$(REDIS_SENTINEL_NAME): $(REDIS_SERVER_NAME)$(REDIS_INSTALL) $(REDIS_SERVER_NAME) $(REDIS_SENTINEL_NAME) main函数[cpp] view plain copy int main(int argc, char **argv){ ...... //checkForSentinelMode判断是否以sentinel模式启动//运行程序名为redis-sentinel,或者带参数--sentinel运行则认为以sentinel模式运行server.sentinel_mode = checkForSentinelMode(argc,argv); initServerConfig(); //sentinel模式下需要完成的初始化工作if (server.sentinel_mode){ initSentinelConfig();initSentinel(); } if (argc &gt;= 2){ ...... //导入配置loadServerConfig(configfile,options);sdsfree(options); } ...... //注册定时器initServer(); ...... //判断config文件是否存在及是否可写(sentinel模式需要写config文件)if(!server.sentinel_mode) { ...... } else{ sentinelIsRunning(); } //以下开始进入事件处理循环aeSetBeforeSleepProc(server.el,beforeSleep);aeMain(server.el); aeDeleteEventLoop(server.el); return 0; }main函数中的loadServerConfig调用了loadServerConfigFromString函数,而loadServerConfigFromString在sentinel模式下会调用sentinelHandleConfiguration函数此处先说明sentinel用到的几个核心数据结构:sentinelState为“顶级”数据结构,描述了当前运行的sentinel 实例所包含的所有状态,其master字段指向包含了该sentinel结点监控的所有master(sentinelRedisInstance )实例状态的字典sentinelRedisInstance 描述了sentinel监控的“master”的状态[cpp] view plain copy struct sentinelState { uint64_t current_epoch; //当前处在第几个世纪(每次fail over,current_epoch+1)dict *masters; /* master实例字典(一个sentinle可监控多个master)*/ int tilt; /*是否在TITL模式中,后面详细介绍TITL模式*/ int running_scripts; /* 当前正在执行的脚本*/mstime_t tilt_start_time; /* TITL模式开始的时间*/ mstime_t previous_time; /* 上次执行sentinel周期性执行任务的时间,用以判断是否进入TITL模式*/ list *scripts_queue; /* 待执行脚本队列*/ } sentinel; typedef struct sentinelRedisInstance { ...... /* Master specific. */ dict *sentinels; /* 监控该master实例的其他sentinel结点字典*/ dict *slaves; /* 该master实例说包含的slave结点字典*/ ...... } sentinelRedisInstance;sentinelHandleConfiguration函数会handle sentinel相关的配置,创建master、slave、sentinel等实例,并根据配置初始化一些参数,包括发现redis instance多长时间后判断其为down其中创建slave和sentinel的配置可以在sentinel运行过程中生成,也可以用户配置,like:sentinel known-slave mymaster 10.2.1.53 6379sentinel known-sentinel mymaster 10.2.60.50 3637933b297cfba3a7aece69fc999bb7150fa227e4fe7[cpp] view plain copy char*sentinelHandleConfiguration(char **argv, int argc){ sentinelRedisInstance *ri; //Handle 类似“sentinel monitor mymaster 10.2.60.50 6379 2”的配置//调用createSentinelRedisInstance创建master实例(SRI_MASTER) if (!strcasecmp(argv[0],"monitor") &amp;&amp; argc == 5) { /* monitor&lt;name&gt; &lt;host&gt; &lt;port&gt; &lt;quorum&gt; */ int quorum = atoi(argv[4]); if (quorum &lt;= 0) return "Quorum must be 1 or greater."; if (createSentinelRedisInstance(argv[1],SRI_MASTER,argv[ 2],atoi(argv[3]),quorum,NULL) == NULL){ switch(errno) { case EBUSY: return "Duplicated master name.";case ENOENT: return "Can't resolve master instance hostname."; case EINVAL: return "Invalidport number"; } } .....//调用createSentinelRedisInstance创建slave实例(SRI_SLAVE) } else if(!strcasecmp(argv[0],"known-slave") &amp;&amp; argc == 4) { sentinelRedisInstance *slave; /* known-slave &lt;name&gt; &lt;ip&gt; &lt;port&gt; *///根据master name获取对应的master实例ri = sentinelGetMasterByName(argv[1]); if (!ri) return "No such master with specified name."; if ((slave =createSentinelRedisInstance(NULL,SRI_SLAVE,argv[2], atoi(argv[3]), ri-&gt;quorum, ri)) == NULL){ return "Wrong hostname or port for slave."; } //调用createSentinelRedisInstance创建sentinel实例(SRI_SENTINEL) } else if(!strcasecmp(argv[0],"known-sentinel") &amp;&amp; (argc == 4 || argc == 5)) { sentinelRedisInstance *si; // known-sentinel &lt;name&gt; &lt;ip&gt;&lt;port&gt; [runid] //根据master name获取对应的master实例ri = sentinelGetMasterByName(argv[1]); //对于slave和sentinel实例,定义其hostname为ip:port if (flags &amp; (SRI_SLAVE|SRI_SENTINEL)){ snprintf(slavename,sizeof(slavename),strchr(hostname,':') ? "[%s]:%d" : "%s:%d",hostname,port); name = slavename; }//根据实例类型不同,添加到不同的table if (flags&amp; SRI_MASTER) table = sentinel.masters; else if (flags &amp; SRI_SLAVE) table = master-&gt;slaves; else if (flags &amp; SRI_SENTINEL) table =master-&gt;sentinels; sdsname = sdsnew(name);if (dictFind(table,sdsname)) { sdsfree(sdsname); errno = EBUSY; return NULL; } ...... dictAdd(table, ri-&gt;name, ri); return ri; }main函数调用的initSentinel函数初始化了sentinel能够支持的客户端命令:[cpp] view plain copy void initSentinel(void) { int j;//空command字典dictEmpty(mands,NULL); //添加sentinal模式下支持的命令,sentinelcmds包括:ping、sentinel、subscribe、unsubscribe、psubscribe、info、shutdown for (j = 0; j &lt;sizeof(sentinelcmds)/sizeof(sentinelcmds[0]); j++){ int retval; struct redisCommand*cmd = sentinelcmds+j; retval =dictAdd(mands, sdsnew(cmd-&gt;name), cmd); redisAssert(retval == DICT_OK); } ...... }main函数调用的initServer会完成一些初始化工作,并注册定时器[cpp] view plain copy void initServer() { ...... //注册定时器,定时时间1msif(aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) { redisPanic("Can't create the serverCron time event.");exit(1); } ...... }serverCron服务启动后1ms第一次执行,以后每隔100ms 执行一次,serverCron会执行sentinel相关任务[cpp] view plain copy int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) { ...... /*如果在sentinel模式下,则执行sentinel相关的周期性任务*/ run_with_period(100) { //100ms执行一次if (server.sentinel_mode) sentinelTimer(); }server.cronloops++; return 1000/server.hz; //hz默认值为10(在sentinelTimer会被修改),此处返回100ms 会被其它函数扑捉到,并重新注册为定时函数}sentinelTimer内部包含sentinel模式需要定期执行的操作,包括check master、slave、sentinel的状态,并根据配置的条件判断是否需要fail over。

redis之字符串命令源代码解析(二)

redis之字符串命令源代码解析(二)

redis之字符串命令源代码解析(⼆)在redis之字符串命令源代码解析(⼀)中讲了get的简单实现,并没有对怎样取到数据做深⼊分析,这⾥将深⼊。

1、redisObject 数据结构。

以及Redis 的数据类型(⼀)中说set test "hello redis",“hello redis”会终于保存在robj中,redisObject是Redis的核⼼,数据库的每⼀个键、值,以及Redis本⾝处理的參数都表⽰为这样的数据类型,其结构例如以下:/* The actual Redis Object *//** Redis 对象*/#define REDIS_LRU_BITS 24#define REDIS_LRU_CLOCK_MAX ((1<<REDIS_LRU_BITS)-1) /* Max value of obj->lru */#define REDIS_LRU_CLOCK_RESOLUTION 1000 /* LRU clock resolution in ms */typedef struct redisObject {// 类型,冒号后⾯跟数字,表⽰包括的位数。

这样更节省内存unsigned type:4;// 编码unsigned encoding:4;// 对象最后⼀次被訪问的时间unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */// 引⽤计数int refcount;<span style="color:#ff0000;">// 指向实际值的指针,指向上节的sdshdr->buf,⽽不是sdshdr,这还要归因于sds.c中的⽅法sdsnewlen返回的buf部分,⽽不是整个sdshdr</span>void *ptr;} robj;对象类型有:#define REDIS_STRING 0 // 字符串#define REDIS_LIST 1 // 列表#define REDIS_SET 2 // 集合#define REDIS_ZSET 3 // 有序集#define REDIS_HASH 4 // 哈希表对象编码有:#define REDIS_ENCODING_RAW 0 // 编码为字符串#define REDIS_ENCODING_INT 1 // 编码为整数#define REDIS_ENCODING_HT 2 // 编码为哈希表#define REDIS_ENCODING_ZIPMAP 3 // 编码为zipmap#define REDIS_ENCODING_LINKEDLIST 4 // 编码为双端链表#define REDIS_ENCODING_ZIPLIST 5 // 编码为压缩列表#define REDIS_ENCODING_INTSET 6 // 编码为整数集合#define REDIS_ENCODING_SKIPLIST 7 // 编码为跳跃表2、内部数据结构之dict(俗称字典)1.1 dict结构Redis使⽤的是⾼效且实现简单的哈希作为字典的底层实现。

Redis(八):zsetzaddzrangezrembyscore命令源码解析

Redis(八):zsetzaddzrangezrembyscore命令源码解析

Redis(⼋):zsetzaddzrangezrembyscore命令源码解析 前⾯⼏篇⽂章,我们完全领略了redis的string,hash,list,set数据类型的实现⽅法,相信对redis已经不再神秘。

本篇我们将介绍redis的最后⼀种数据类型: zset 的相关实现。

本篇过后,我们对redis的各种基础功能,应该不会再有疑惑。

有可能的话,我们后续将会对redis的⾼级功能的实现做解析。

(如复制、哨兵模式、集群模式) 回归本篇主题,zset。

zset ⼜称有序集合(sorted set),即是序版本的set。

经过上篇的介绍,⼤家可以看到,redis的读取功能相当有限,许多是基于随机数的⽅式进⾏读取,其原因就是set是⽆序的。

当set有序之后,查询能⼒就会得到极⼤的提升。

1. 可以根据下标进⾏定位元素; 2. 可以范围查询元素; 这是有序带来的好处。

那么,我们不妨先思考⼀下,如何实现有序?两种⽅法:1. 根据添加顺序定义,1、2、3... ; 2. ⾃定义排序值; 第1种⽅法实现简单,添加时复杂度⼩,但是功能受限;第2种⽅法相对⾃由,对于每次插⼊都可能涉及重排序问题,但是查询相对稳定,可以不必完全受限于系统实现; 同样,我们以功能列表,到数据结构,再功能实现的思路,来解析redis的zset有序集合的实现⽅式吧。

零、redis zset相关操作⽅法 zset: Redis 有序集合是string类型元素的集合,且不允许重复的成员。

每个元素都会关联⼀个double类型的分数,通过分数来为集合中的成员进⾏从⼩到⼤的排序。

使⽤场景如: 保存任务队列,该队列由后台定时扫描; 排⾏榜; 从官⽅⼿册上查到相关使⽤⽅法如下:1> ZADD key score1 member1 [score2 member2]功能: 向有序集合添加⼀个或多个成员,或者更新已存在成员的分数返回值: 添加成功的元素个数(已存在的添加不成功)2> ZCARD key功能: 获取有序集合的成员数返回值: 元素个数或03> ZCOUNT key min max功能: 计算在有序集合中指定区间分数的成员数返回值: 区间内的元素个数4> ZINCRBY key increment member功能: 有序集合中对指定成员的分数加上增量 increment返回值: member增加后的分数5> ZINTERSTORE destination numkeys key [key ...]功能: 计算给定的⼀个或多个有序集的交集并将结果集存储在新的有序集合 key 中返回值: 交集元素个数6> ZLEXCOUNT key min max功能: 在有序集合中计算指定字典区间内成员数量返回值: 区间内的元素个数7> ZRANGE key start stop [WITHSCORES]功能: 通过索引区间返回有序集合指定区间内的成员返回值: 区间内元素列表8> ZRANGEBYLEX key min max [LIMIT offset count]功能: 通过字典区间返回有序集合的成员返回值: 区间内元素列表9> ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT]功能: 通过分数返回有序集合指定区间内的成员返回值: 区间内元素列表10> ZRANK key member功能: 返回有序集合中指定成员的索引返回值: member的排名或者 nil11> ZREM key member [member ...]功能: 移除有序集合中的⼀个或多个成员返回值: 成功移除的元素个数12> ZREMRANGEBYLEX key min max功能: 移除有序集合中给定的字典区间的所有成员返回值: 成功移除的元素个数13> ZREMRANGEBYRANK key start stop功能: 移除有序集合中给定的排名区间的所有成员返回值: 成功移除的元素个数14> ZREMRANGEBYSCORE key min max功能: 移除有序集合中给定的分数区间的所有成员返回值: 成功移除的元素个数15> ZREVRANGE key start stop [WITHSCORES]功能: 返回有序集中指定区间内的成员,通过索引,分数从⾼到低返回值: 区间内元素列表及分数16> ZREVRANGEBYSCORE key max min [WITHSCORES]功能: 返回有序集中指定分数区间内的成员,分数从⾼到低排序返回值: 区间内元素列表及分数17> ZREVRANK key member功能: 返回有序集合中指定成员的排名,有序集成员按分数值递减(从⼤到⼩)排序返回值: member排名或者 nil18> ZSCORE key member功能: 返回有序集中,成员的分数值返回值: member分数功能: 计算给定的⼀个或多个有序集的并集,并存储在新的 key 中返回值: 存储到新key的元素个数20> ZSCAN key cursor [MATCH pattern] [COUNT count]功能: 迭代有序集合中的元素(包括元素成员和元素分值)返回值: 元素列表21> ZPOPMAX/ZPOPMIN/BZPOPMAX/BZPOPMIN⼀、zset 相关数据结构 zset 的实现,使⽤了 ziplist, zskiplist 和 dict 进⾏实现。

Redis源码剖析之内存淘汰策略(Evict)

Redis源码剖析之内存淘汰策略(Evict)

Redis源码剖析之内存淘汰策略(Evict)Redis作为⼀个成熟的数据存储中间件,它提供了完善的数据管理功能,⽐如之前我们提到过的和今天我们要讲的数据淘汰(evict)策略。

在开始介绍Redis数据淘汰策略前,我先抛出⼏个问题,帮助⼤家更深刻理解Redis的数据淘汰策略。

1. 何为数据淘汰,Redis有了数据过期策略为什么还要有数据淘汰策略?2. 淘汰哪些数据,有什么样的数据选取标准?3. Redis的数据淘汰策略是如何实现的?何为Evict我先来回答第⼀个问题,Redis中数据淘汰实际上是指的在内存空间不⾜时,清理掉某些数据以节省内存空间。

虽然Redis已经有了过期的策略,它可以清理掉有效期外的数据。

但想象下这个场景,如果过期的数据被清理之后存储空间还是不够怎么办?是不是还可以再删除掉⼀部分数据?在缓存这种场景下这个问题的答案是可以,因为这个数据即便在Redis中找不到,也可以从被缓存的数据源中找到。

所以在某些特定的业务场景下,我们是可以丢弃掉Redis中部分旧数据来给新数据腾出空间。

如何Evict第⼆个问题,既然我们需要有淘汰的机制,你们在具体执⾏时要选哪些数据淘汰掉?具体策略有很多种,但思路只有⼀个,那就是总价值最⼤化。

我们⽣在⼀个不公平的世界⾥,同样数据也是,那么多数据⾥必然不是所有数据的价值都是⼀样的。

所以我们在淘汰数据时只需要选择那些低价值的淘汰即可。

所以问题⼜来了,哪些数据是低价值的?这⾥不得不提到⼀个贯穿计算机学科的原理局部性原理,这⾥可以明确告诉你,局部性原理在缓存场景有这样两种现象,1. 最新的数据下次被访问的概率越⾼。

2. 被访问次数越多的数据下次被访问的概率越⾼。

这⾥我们可以简单认为被访问的概率越⾼价值越⼤。

基于上述两种现象,我们可以指定出两种策略 1. 淘汰掉最早未被访问的数据。

2. 淘汰掉访被访问频次最低的数据,这两种策略分别有个洋⽓的英⽂名LRU(Least Recently Used)和LFU(Least Frequently Used)。

RedisTemplate源码分析以及Redis常见问题

RedisTemplate源码分析以及Redis常见问题

RedisTemplate源码分析以及Redis常见问题1. RedisTemplate 默认配置下底层实现使⽤jedis(spring-boot 1.x)或者lettuce(spring-boot 2.x)操作redis的spring-boot 1.5.7spring-data-redis 1.8.7配置⽂件# redisspring.redis.host=172.168.32.145spring.redis.password=spring.redis.port=6379spring.redis.database=7单元测试代码,获取1000次字符类型的redis数值,以下代码为并⾏执⾏代码@Testpublic void getStringValue() throws Exception {final String key = "cloud_search_using_record";for (int i = 0; i < 1000; i++) {new Thread(()->{stringRedisTemplate.opsForValue().get(key);}).start();}Thread.sleep(1000000);}DefaultValueOperations.class , 25⾏public V get(Object key) {return this.execute(new AbstractOperations<K, V>.ValueDeserializingRedisCallback(key) {protected byte[] inRedis(byte[] rawKey, RedisConnection connection) {return connection.get(rawKey);}}, true);}AbstractOperations.class , 56⾏<T> T execute(RedisCallback<T> callback, boolean b) {return this.template.execute(callback, b);}RedisTemplate.class,126⾏conn = RedisConnectionUtils.getConnection(factory);RedisConnectionUtils.class, 61⾏RedisConnection conn = factory.getConnection();debug调试 JedisConnectionFactory.class 247⾏,进⾏断点查看jedis对象内存地址,当串⾏执⾏时,⼀般的会出现每次都是⼀样的,并⾏执⾏时jedis 地址是不断在⼀定范围变化的。

Redis源码解析——双向链表

Redis源码解析——双向链表

Redis源码解析——双向链表相对于之前介绍的字典和SDS字符串库,Redis的双向链表库则是非常标准的、教科书般简单的库。

但是作为Redis源码的一部分,我决定还是要讲一讲的。

基本结构首先我们看链表元素的结构。

因为是双向链表,所以其基本元素应该有一个指向前一个节点的指针和一个指向后一个节点的指针,还有一个记录节点值的空间[cpp] view plain copy 在CODE上查看代码片派生到我的代码片typedef struct listNode {struct listNode *prev;struct listNode *next;void *value;} listNode;因为双向链表不仅可以从头开始遍历,还可以从尾开始遍历,所以链表结构应该至少有头和尾两个节点的指针。

但是作为一个可以承载各种类型数据的链表,还需要链表使用者提供一些处理节点中数据的能力。

因为这些数据可能是用户自定义的,所以像复制、删除、对比等操作都需要用户来告诉框架。

在《Redis源码解析——字典结构》一文中,我们看到用户创建字典时需要传入的dictType结构体,就是一个承载数据的上述处理方法的载体。

但是Redis在设计双向链表时则没有使用一个结构来承载这些方法,而是在链表结构中定义了[cpp] view plain copy 在CODE上查看代码片派生到我的代码片typedef struct list {listNode *head;listNode *tail;void *(*dup)(void *ptr);void (*free)(void *ptr);int (*match)(void *ptr, void *key);unsigned long len;} list;至于链表结构中为什么要存链表长度字段len,我觉得从必要性上来说是没有必要的。

有len字段的一个优点是不用每次计算链表长度时都要做一次遍历操作,缺点便是导出需要维护这个变量。

Redis源码剖析之数据过期(expire)

Redis源码剖析之数据过期(expire)

Redis源码剖析之数据过期(expire)我之前统计过我们线上某redis数据被访问的时间分布,⼤概90%的请求只会访问最新15分钟的数据,99%的请求访问最新1⼩时的数据,只有不到千分之⼀的请求会访问超过1天的数据。

我们之前这份数据存了两天(近500g内存数据),如果算上主备的话⽤掉了120多个Redis实例(⼀个实例8g内存),光把过期时间从2天改成1天就能省下60多个redis实例,⽽且对原业务也没有啥太⼤影响。

当然Redis已经实现了数据过期的⾃动清理机制,我所要做的只是改下数据写⼊时的过期时间⽽已。

假设Redis没有数据过期的机制,我们要怎么办?⼤概⼀想就知道很⿇烦,仔细想的话还得考虑很多的细节。

所以我觉得过期数据在缓存系统中是不起眼但⾮常重要的功能,除了省事外,它也能帮我们节省很多成本。

接下来我们看下Redis中是如何实现数据过期的。

实时清理众所周知,Redis核⼼流程是单线程执⾏的,它基本上是处理完⼀条请求再出处理另外⼀条请求,处理请求的过程并不仅仅是响应⽤户发起的请求,Redis也会做好多其他的⼯作,当前其中就包括数据的过期。

Redis在读写某个key的时候,它就会调⽤expireIfNeeded()先判断这个key是否已经过期了,如果已过期,就会执⾏删除。

int expireIfNeeded(redisDb *db, robj *key) {if (!keyIsExpired(db,key)) return 0;/* 如果是在slave上下⽂中运⾏,直接返回1,因为slave的key过期是由master控制的,* master会给slave发送数据删除命令。

** 如果返回0表⽰数据不需要清理,返回1表⽰数据这次标记为过期 */if (server.masterhost != NULL) return 1;if (checkClientPauseTimeoutAndReturnIfPaused()) return 1;/* 删除key */server.stat_expiredkeys++;propagateExpire(db,key,zyfree_lazy_expire);notifyKeyspaceEvent(NOTIFY_EXPIRED,"expired",key,db->id);int retval = zyfree_lazy_expire ? dbAsyncDelete(db,key) :dbSyncDelete(db,key);if (retval) signalModifiedKey(NULL,db,key);return retval;}判断是否过期也很简单,Redis在dictEntry中存储了上次更新的时间戳,只需要判断当前时间戳和上次更新时间戳之间的gap是否超过设定的过期时间即可。

Redis(11)——主从源码解析

Redis(11)——主从源码解析

Redis(11)——主从源码解析Brief简单来说就是主redis(master)接到写命令后,会把命令发给slave redis,这样保证主从⼀致性先来看⼀下,当⼀个全新的slave连接上master时,会发⽣什么先有个印象,后续看代码会⽅便理解主从复制,交互简图左边是slave,右边是master,为了⽅便就不再重复标注箭头是传输⽅向,左边或右边的⼤写字母为发送的命令slave <----------------------> masterPING 看⼀下master在不在 -------> //这句的意思就是slave发送PING命令给master,以下类似<--------------- 回复 +PONGREPLCONF listening-port 7000 告诉master端⼝ ---------------><--------------- 回复 +OKREPLCONF capa eof capa psync2 告诉master PSYNC版本---------><--------------- 回复 +OKPSYNC ? -1 告诉master,我是⼀个全新的slave ----------------------><--------------- +FULLRESYNC [master id] [offset] 告诉slave,master的标识id和偏移offset(master后台开启RDB,slave准备接收RDB⽂件)(master对于正在等待BGSAVE的slave,会定期发送⼀个\n过去,slave不需要回复)<-------------- master 发送⼀个字符\n 给slaveACK ----------------------------->(重复N次)(master完成RDB,发送给slave)<---------- $[RDB⽂件⼤⼩]<----------- RDB⽂件内容(传输完成,slave 定期发送 REPLCONF ACK 给master,master不回复)REPLCONF ACK [offset] ⼼跳机制,告诉master当前的offset --------->(master每隔server.repl_ping_slave_period秒,发送PING给所有slave,slave不回复)<---------- PING(master有新命令的时候,会同步给slave,保证主从数据⼀致)<---------- SET WHAT AAA (举个例⼦)源码notice: 本⽂假设读者已经了解过redis的主从同步机制先看slave机,以配置⽂件salveof为例,在读取配置⽂件的时候,就会把server.repl_state置为REPL_STATE_CONNECT slave连接master真正的连接在定时任务中server.cint serverCron() {/* Replication cron function -- used to reconnect to master,* detect transfer failures, start background RDB transfers and so forth. */run_with_period(1000) replicationCron();}replicationCron函数中,检测到repl_state为REPL_STATE_CONNECT,就会调⽤connectWithMaster函数connectWithMaster做了2件事:和master建⽴tcp连接,⾮阻塞fd;并且注册到多路复⽤,监听可读可写设置状态,server.repl_transfer_lastio=unixtime,repl_transfer_s=fd,repl_state=REPL_STATE_CONNECTING(由于slave需要在连接成功后做⼀些操作,因此监听可写)(可写意味着连接成功)连接成功在连接成功的时候,fd会变为可写,回调函数 replication.c -> void syncWithMaster(aeEventLoop *el, int fd, void *privdata, int mask)这个函数⾸先检查fd有没有socket错误如果状态为REPL_STATE_CONNECTING,把可写监听删除,向master发送 PING 命令状态变为REPL_STATE_RECEIVE_PONGsendSynchronousCommand这⾥要提⼀下发送命令的函数 char *sendSynchronousCommand(int flags, int fd, ...)根据函数声明,最后是可变参数,第⼀个参数⽬前只要READ和WRITE两种发送命令并不是像客户端那样,以空格分割;⽽是使⽤aof那种⽅式,*c $num然后,这个写是可以有重试超时时间,server.repl_syncio_timeout(默认5秒)因为write可能返回-1,EAGAIN;可能⼀次不能write全部数据,需要分多次write这时候就要等fd再次writeable时再写这个超时时间其实是对应于这个等待的时间PSYNC发送ping之后,还要进⾏⼏个命令,校验和配置,⽐较简单;这个并不是重点,所以直接跳到psync命令psync命令,是slave连接上master之后,请求同步的含义psync命令格式:psync [master_run_id] [offset]如果是全新的slave,psync ? -1master收到这个命令,实现函数在replication.c -> void syncCommand(client*)由于是slave连接上之后,只需要请求⼀次这个命令,这个命令会把client -> flags加上 CLIENT_SLAVE,并且加⼊到server.slaves这个list中所以这个函数⼀开头,判断如果client已经有这个flag,直接returnmaster的核⼼⼯作在 replication.c -> void syncCommand(client *c)master第⼀次接到同步的准备⼯作如果server.slaves长度为1并且server.repl_backlog为NULL,那说明是第⼀次第⼀次的话,master需要⽣成replicationId,server.replid⽣成随机字符server.replid2全部置0,server.second_replid_offset置为-1⽣成repl_backlogserver.repl_backlog = zmalloc(server.repl_backlog_size); 默认值 1Mserver.repl_backlog_histlen = 0;server.repl_backlog_idx = 0;server.repl_backlog_off = server.master_repl_offset+1;我们知道,redis的主从同步有全同步和跟随同步之分,如果是⼀个全新的slave,那肯定就是全同步先来看全同步全同步全同步即 master⽣成RDB发送给slave会把client的replstate置为SLAVE_STATE_WAIT_BGSAVE_START;然后开启Nagle算法(在epoll接收到客户端连接时,会禁⽤Nagle算法,但是在主从同步时,⼜会开启,应该是因为这个并不需要实时响应,合并TCP报⽂有利于⽹络传输)现在假设没有RDB或者AOF在执⾏,执⾏int startBgsaveForReplication(int mincapa) 函数int startBgsaveForReplication(int mincapa) {rdbSaveInfo rsi, *rsiptr;rsiptr = rdbPopulateSaveInfo(&rsi);if (rsiptr) {if (socket_target)retval = rdbSaveToSlavesSockets(rsiptr);elseretval = rdbSaveBackground(server.rdb_filename,rsiptr);}.........}rdbPopulateSaveInfo点进去看⼀下rdbSaveInfo *rdbPopulateSaveInfo(rdbSaveInfo *rsi) {rdbSaveInfo rsi_init = RDB_SAVE_INFO_INIT;*rsi = rsi_init;//master这时repl_backlog已经创建,会进⼊if/* If the instance is a master, we can populate the replication info* only when repl_backlog is not NULL. If the repl_backlog is NULL,* it means that the instance isn't in any replication chains. In this* scenario the replication info is useless, because when a slave* connects to us, the NULL repl_backlog will trigger a full* synchronization, at the same time we will use a new replid and clear* replid2. */if (!server.masterhost && server.repl_backlog) {/* Note that when server.slaveseldb is -1, it means that this master* didn't apply any write commands after a full synchronization.* So we can let repl_stream_db be 0, this allows a restarted slave* to reload replication ID/offset, it's safe because the next write* command must generate a SELECT statement. */rsi->repl_stream_db = server.slaveseldb == -1 ? 0 : server.slaveseldb;return rsi;}.......}对于master来说,这⾥就是设置了rsi->repl_stream_db这个值表⽰master当前在哪个db(redis⽀持多个db),server.slavesldb同样的含义server.slavesldb初始值为-1,那这个值在哪⾥设置?其实是在执⾏cmd的时候,发送命令给slave机之前。

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

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

redis源码分析4---结构体---跳跃表redis源码分析4---结构体---跳跃表跳跃表是⼀种有序的数据结构,他通过在每个节点中维持多个指向其他节点的指针,从⽽达到快速访问节点的⽬的;跳跃表⽀持平均O(logN),最坏O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点。

性能上和平衡树媲美,因为事先简单,常⽤来代替平衡树。

在redis中,只在两个地⽅使⽤了跳跃表,⼀个是实现有序集合键,另⼀个是在集群节点中⽤作内部数据结构。

1 跳跃表节点1.1 层层的数量越多,访问其他节点的速度越快;1.2 前进指针遍历举例步骤1.3 跨度跨度记录两个节点之间的距离1.4 后退指针⽤于从表尾向表头放⼼访问节点;1.5 分值和成员2 跳跃表结构3 跳跃表API4 源代码分析源代码中,结构体定义在redis.h中,实现在t_zset.c4.1 建⽴和释放/** 创建⼀个层数为 level 的跳跃表节点,* 并将节点的成员对象设置为 obj ,分值设置为 score 。

** 返回值为新创建的跳跃表节点** T = O(1)*/zskiplistNode *zslCreateNode(int level, double score, robj *obj) {// 分配空间zskiplistNode *zn = zmalloc(sizeof(*zn)+level*sizeof(struct zskiplistLevel)); // 设置属性zn->score = score;zn->obj = obj;return zn;}/** 创建并返回⼀个新的跳跃表** T = O(1)*/zskiplist *zslCreate(void) {int j;zskiplist *zsl;// 分配空间zsl = zmalloc(sizeof(*zsl));// 设置⾼度和起始层数zsl->level = 1;zsl->length = 0;// 初始化表头节点// T = O(1)zsl->header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL);for (j = 0; j < ZSKIPLIST_MAXLEVEL; j++) {zsl->header->level[j].forward = NULL;zsl->header->level[j].span = 0;}zsl->header->backward = NULL;// 设置表尾zsl->tail = NULL;return zsl;}/** 释放给定的跳跃表节点** T = O(1)*/void zslFreeNode(zskiplistNode *node) {decrRefCount(node->obj);zfree(node);}/** 释放给定跳跃表,以及表中的所有节点** T = O(N)*/void zslFree(zskiplist *zsl) {zskiplistNode *node = zsl->header->level[0].forward, *next;// 释放表头zfree(zsl->header);// 释放表中所有节点// T = O(N)while(node) {next = node->level[0].forward;zslFreeNode(node);node = next;}// 释放跳跃表结构zfree(zsl);}/* Returns a random level for the new skiplist node we are going to create.** 返回⼀个随机值,⽤作新跳跃表节点的层数。

Redis源代码分析

Redis源代码分析

Redis源代码分析⽂章引⽤⾃:⼀直有打算写篇关于redis源代码分析的⽂章,⼀直很忙,还好最近公司终于闲了⼀点,总算有点时间学习了,于是终于可以兑现承诺了,废话就到此吧,开始我们的源代码分析,在⽂章的开头我们把所有服务端⽂件列出来,并且标⽰出其作⽤:adlist.c //双向链表ae.c //事件驱动ae_epoll.c //epoll模型ae_kqueue.c //kqueue模型( freebsd)ae_select.c //select模型anet.c //⽹络处理aof.c //处理AOF⽂件config.c //配置⽂件解析db.c //DB处理dict.c //hash表intset.c //转换为数字类型数据multi.c //事务,多条命令⼀起打包处理networking.c //读取、解析和处理客户端命令object.c //各种对像的创建与销毁,string、list、set、zset、hashrdb.c //redis数据⽂件处理redis.c //程序主要⽂件replication.c //数据同步master-slavesds.c //字符串处理sort.c //⽤于list、set、zset排序t_hash.c //hash类型处理t_list.c //list类型处理t_set.c //set类型处理t_string.c //string类型处理t_zset.c //zset类型处理ziplist.c //节省内存⽅式的list处理zipmap.c //节省内存⽅式的hash处理zmalloc.c //内存管理上⾯基本是redis最主要的处理⽂件,部分没有列出来,如VM之类的,就不在这⾥讲了。

⾸先我们来回顾⼀下redis的⼀些基本知识:1、redis有N个DB(默认为16个DB),并且每个db有⼀个hash表负责存放key,同⼀个DB不能有相同的KEY,但是不同的DB可以相同的KEY;2、⽀持的⼏种数据类型:string、hash、list、set、zset;3、redis可以使⽤aof来保存写操作⽇志(也可以使⽤快照⽅式保存数据⽂件)对于数据类型在这⾥简单的介绍⼀下(⽹上有图,下⾯我贴上图⽚可能更容易理解)1、对于⼀个string对像,直接存储内容;2、对于⼀个hash对像,当成员数量少于512的时候使⽤zipmap(⼀种很省内存的⽅式实现hash table),反之使⽤hash表(key存储成员名,value 存储成员数据);3、对于⼀个list对像,当成员数量少于512的时候使⽤ziplist(⼀种很省内存的⽅式实现list),反之使⽤双向链表(list);4、对于⼀个set对像,使⽤hash表(key存储数据,内容为空)5、对于⼀个zset对像,使⽤跳表(skip list),关于跳表的相关内容可以查看本blog的跳表学习笔记;下⾯正式进⼊源代码的分析1、⾸先是初始化配置,initServerConfig(redis.c:759)void initServerConfig() {server.port = REDIS_SERVERPORT;server.bindaddr = NULL;server.unixsocket = NULL;server.ipfd = -1;server.sofd = -1;2.在初始化配置中调⽤了populateCommandTable(redis.c:925)函数,该函数的⽬地是将命令集分布到⼀个hash table中,⼤家可以看到每⼀个命令都对应⼀个处理函数,因为redis⽀持的命令集还是蛮多,所以如果要靠if分⽀来做命令处理的话即繁琐效率还底,因此放到hash table 中,在理想的情况下只需⼀次就能定位命令的处理函数。

Redis源代码分析

Redis源代码分析

Redis 源代码分析文档审核人文档拟制人文档提交时间胡戊(huwu)邹雨晗(yuhanzou) Jun 17, 2011文档修改记录文档更新时间变更内容变更提出部门变更理由Jun 17, 2011初稿目录1.Redis介绍!42.基本功能!42.1.链表(adlist.h/adlist.c)!42.2.字符串(sds.h/sds.c)!52.3.哈希表(dict.h/dict.c)!62.4.内存(zmalloc.h/zmalloc.h)!93.服务器模型!113.1.事件处理(ae.h/ae.c)!113.2.套接字操作(anet.h/anet.c)!153.3.客户端连接(networking.h/networking.c, redis.c/redis.h)!153.4.命令处理!174.虚拟内存!194.1.数据读取过程!204.2.数据交换策略!235.备份机制!245.1.Snapshot!245.2.AOF!266.主从同步!276.1.建立连接!276.2.指令同步!306.3.主从转换!311.Redis介绍Redis(http://redis.io)是一个开源的Key-Value存储引擎。

它支持string、hash、list、set和sorted set等多种值类型。

2.基本功能2.1.链表(adlist.h/adlist.c)链表(list)是Redis中最基本的数据结构,由adlist.h和adlist.c定义。

基本数据结构listNode是最基本的结构,标示链表中的一个结点。

结点有向前(next)和向后(prev)连个指针,链表是双向链表。

保存的值是void*类型。

typedef struct listNode {struct listNode *prev;struct listNode *next;void *value;} listNode;链表通过list定义,提供头(head)、尾(tail)两个指针,分别指向头部的结点和尾部的结点;提供三个函数指针,供用户传入自定义函数,用于复制(dup)、释放(free)和匹配(match)链表中的结点的值(value);通过无符号整数len,标示链表的长度。

Redis源码解析之跳跃表(一)

Redis源码解析之跳跃表(一)

Redis源码解析之跳跃表(⼀)跳跃表(skiplist)有序集合(sorted set)是Redis中较为重要的⼀种数据结构,从名字上来看,我们可以知道它相⽐⼀般的集合多了⼀个有序。

Redis的有序集合会要求我们给定⼀个分值(score)和元素(element),有序集合将根据我们给定的分值对元素进⾏排序。

Redis共有两种编码来实现有序集合,⼀种是压缩列表(ziplist),另⼀种是跳跃表(skiplist),也是本章的主⾓。

下⾯,让笔者带领⼤家稍微了解下有序集合的使⽤。

假设某软件公司统计了公司内的程序员所掌握的编程语⾔,掌握Java的⼈数有90⼈、掌握C的⼈数有20⼈、掌握Python的⼈数有57⼈、掌握Go的⼈数有82⼈、掌握PHP的⼈数有61⼈、掌握Scala的⼈数有28⼈、掌握C++的⼈数有33⼈。

我们⽤key为worker-language的有序集合来存储这⼀结果。

127.0.0.1:6379> ZADD worker-language 90 Java(integer) 1127.0.0.1:6379> ZADD worker-language 20 C(integer) 1127.0.0.1:6379> ZADD worker-language 57 Python(integer) 1127.0.0.1:6379> ZADD worker-language 82 Go(integer) 1127.0.0.1:6379> ZADD worker-language 61 PHP(integer) 1127.0.0.1:6379> ZADD worker-language 28 Scala(integer) 1127.0.0.1:6379> ZADD worker-language 33 C++(integer) 1将上⾯的统计结果形成⼀个有序集合后,我们可以对有序集合进⾏⼀些业务上的操作,⽐如⽤:ZCARD key返回集合的长度:127.0.0.1:6379> ZCARD worker-language(integer) 7可以把集合当成⼀个数组,使⽤ZRANGE key start stop [WITHSCORES]命令指定索引区间返回区间内的成员,⽐如我们指定start为0,stop 为-1,则会返回从索引0到集合末尾所有的元素,即[0,6],如果有序集合⾥⾯有10个元素,则[0,-1]也代表[0,9],带上WITHSCORES选项,除了返回元素本⾝,也会返回元素的分值。

redis源码结构及各文件功能概要

redis源码结构及各文件功能概要

redis源码结构及各⽂件功能概要/*** redis 3.0*/adlist.c // 双向链表结构,⽤于定义listae.c // ⽤于事件的处理ae_epoll.c // 处理epoll事件ae_evport.c // 通过event ports实现处理接⼝ae_kqueue.c // 实现消息队列的处理ae_select.c // 处理select事件anet.c // ⽹络处理aof.c // 实现AOF模式bio.c // 在后台通过线程模式实现IO处理bitops.c // 与SETBIT,GETBIT相关的位操作blocked.c // ⽀持类似BLPOP,WAIT的阻塞操作cluster.c // 与集群创建,通信相关的实现config.c // 配置⽂件的解析crc16.c // 基于CCITT标准的过滤算法crc64.cdb.c // C-level数据库API实现debug.c // 调试库与⽇志输出dict.c // 字典实现endianconv.c // 主机字节序编解码, redis致⼒于使⽤little endian来编码intset.c // int集合类型实现lzf_c.c // lzf压缩算法lzf_d.cmemtest.c // 内存测试multi.c // 批量命令操作的原⼦实现networking.c // ⽹络通信实现noti.c // 通过Pub/Sub实现的keyspace事件通知object.c // 对象的存储类型pqsort.c // ⼀种快速排序实现pubsub.c // 发布/订阅模式rand.c // 随机序列的⽣成rdb.c // rdb数据的load与dumpredis.c // redis数据库的主程序⼊⼝redis-benchmark.c // redis基准测试⼊⼝redis-check-aof.c // aof⽂件检查⼊⼝redis-check-dump.c // dump⽂件检查⼊⼝redis-cli.c // redis的shell客户端release.c // 版本发布信息replication.c // 数据的主从备份rio.c // 流式I/O的读写接⼝scripting.c // redis相关协议转换到lua环境下执⾏sds.c // 强⼤的字符串处理机制sentinel.c // redis(集群)的监护程序setproctitle.c // 程序名信息相关?sha1.c // sha1加密算法slowlog.c // 记录最近⼀段时间的查询等操作sort.c // 排序算法及辅助函数syncio.c // 同步io的实现t_hash.c // hash结构t_list.c // list结构t_set.c // set结构t_string.c // string结构t_zset.c // ⽤两种数据结构存储同⼀个数据体util.c // 辅助函数ziplist.c // ziplist结构zipmap.c // zipmap结构zmalloc.c // 在malloc上进⾏封装,增加内存开销统计。

  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

Redis源码剖析(经典版)为了熟悉Redis源码的执行流程,我们首先要熟悉它每个源码文件的作用,这样能起到事半功倍的效果,在文章的开头我们把所有服务端文件列出来,并且标示出其作用:adlist.c //双向链表ae.c //事件驱动ae_epoll.c //epoll接口, linux用ae_kqueue.c //kqueue接口, freebsd用ae_select.c //select接口, windows用anet.c //网络处理aof.c //处理AOF文件config.c //配置文件解析db.c //DB处理dict.c //hash表intset.c //转换为数字类型数据multi.c //事务,多条命令一起打包处理networking.c //读取、解析和处理客户端命令object.c //各种对像的创建与销毁,string、list、set、zset、hashrdb.c //redis数据文件处理redis.c //程序主要文件replication.c //数据同步master-slavesds.c //字符串处理sort.c //用于list、set、zset排序t_hash.c //hash类型处理t_list.c //list类型处理t_set.c //set类型处理t_string.c //string类型处理t_zset.c //zset类型处理ziplist.c //节省内存方式的list处理zipmap.c //节省内存方式的hash处理zmalloc.c //内存管理上面基本是redis最主要的处理文件,部分没有列出来,如VM之类的,就不在这里讲了。

首先我们来回顾一下redis的一些基本知识:1、redis有N个DB(默认为16个DB),并且每个db有一个hash表负责存放key,同一个DB不能有相同的KEY,但是不同的DB可以相同的KEY;2、支持的几种数据类型:string、hash、list、set、zset;3、redis可以使用aof来保存写操作日志(也可以使用快照方式保存数据文件)对于数据类型在这里简单的介绍一下(网上有图,下面我贴上图片可能更容易理解)1、对于一个string对像,直接存储内容;2、对于一个hash对像,当成员数量少于512的时候使用zipmap(一种很省内存的方式实现hash table),反之使用hash表(key存储成员名,value存储成员数据);3、对于一个list对像,当成员数量少于512的时候使用ziplist(一种很省内存的方式实现list),反之使用双向链表(list);4、对于一个set对像,使用hash表(key存储数据,内容为空)5、对于一个zset对像,使用跳表(skip list),关于跳表的相关内容可以查看本blog的跳表学习笔记;下面正式进入源代码的分析1、首先是初始化配置,initServerConfig(redis.c:759)void initServerConfig() {server.port = REDIS_SERVERPORT;server.bindaddr = NULL;server.unixsocket = NULL;server.ipfd = -1;server.sofd = -1;2.在初始化配置中调用了populateCommandTable(redis.c:925)函数,该函数的目地是将命令集分布到一个hash table中,大家可以看到每一个命令都对应一个处理函数,因为redis支持的命令集还是蛮多,所以如果要靠if分支来做命令处理的话即繁琐效率还底,因此放到hash table中,在理想的情况下只需一次就能定位命令的处理函数。

void populateCommandTable(void) {int j;int numcommands = sizeof(readonlyCommandTable)/sizeof(struct redisCommand);for (j = 0; j < numcommands; j++) {struct redisCommand *c = readonlyCommandTable+j;int retval;retval = dictAdd(mands, sdsnew(c->name), c);assert(retval == DICT_OK);}}3、对参数的解析,redis-server有一个参数(可以不需要),这个参数是指定配置文件路径,然后由函数loadServerConfig(config.c:28)加载所有配置if (argc == 2) {if (strcmp(argv[1], “-v”) == 0 ||strcmp(argv[1], “–version”) == 0) version();if (strcmp(argv[1], “–help”) == 0) usage(); resetServerSaveParams();loadServerConfig(argv[1]);4、初始化服务器initServer(redis.c:836), 该函数初始化一些服务器信息,包括创建事件处理对像、db、socket、客户端链表、公共字符串等。

void initServer() {int j;signal(SIGHUP, SIG_IGN);signal(SIGPIPE, SIG_IGN);setupSignalHandlers();//设置信号处理if (server.syslog_enabled) {openlog(server.syslog_ident, LOG_PID | LOG_NDELAY | LOG_NOWAIT, server.syslog_facility);}5、在上面初始化服务器中有一段代码是创建事件驱动,aeCreateTimeEvent是创建一个定时器,下面创建的定时器将会每毫秒调用 serverCron函数,而aeCreateFileEvent是创建网络事件驱动,当server.ipfd和server.sofd有数据可读的情况将会分别调用函数acceptTcpHandler和acceptUnixHandler。

aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL);if (server.ipfd > 0 &&aeCreateFileEvent(server.el,server.ipfd,AE_READABLE, acceptTcpHandler,NULL) == AE_ERR) oom(“creating file event”);if (server.sofd > 0 &&aeCreateFileEvent(server.el,server.sofd,AE_READABLE, acceptUnixHandler,NULL) == AE_ERR) oom(“creating file event”);6、接下来就是初始化数据,如果开启了AOF,那么会调用loadAppendOnlyFile(aof.c:216)去加载AOF文件,在AOF 文件中存放了客户端的命令,函数将数据读取出来然后依次去调用命令集去处理,当AOF文件很大的时候势必为影响客户端的请求,所以每处理1000条命令就会去尝试接受和处理客户端的请求,其代码在aof.c第250行; 但是如果没有开启AOF并且有rdb 的情况,会调用rdbLoad(redis.c:873)尝试去加载rdb文件,理所当然的在加载rdb文件的内部也会考虑文件太大而影响客户端请求,所以跟AOF一样,每处理1000条也会尝试去接受和处理客户端请求。

7、当所有初始化工作做完之后,服务端就开始正式工作了aeSetBeforeSleepProc(server.el,beforeSleep);aeMain(server.el);8、大家都知道redis是单线程模式,所有的请求、处理都是在同一个线程里面进行,也就是一个无限循环,在这个无限循环的内部有两件事要做,第一件就是调用通过aeSetBeforeSleepProc函数设置的回调函数,第二件就是开始接受客户端的请求和处理,所以我们可以在第7节看到设置了回调函数为beforeSleep,但是这个beforeSleep到底有什么作用呢?我们在第9节再详细讲述。

对于aeMain(ae.c:375)就是整个程序的主要循环。

void aeMain(aeEventLoop *eventLoop) {eventLoop->stop = 0;while (!eventLoop->stop) {if (eventLoop->beforesleep != NULL)eventLoop->beforesleep(eventLoop);aeProcessEvents(eventLoop, AE_ALL_EVENTS);}}9、在beforeSleep内部一共有三部分,第一部分对vm进行处理(即第一个if 块),这里我们略过;第二部分是释放客户端的阻塞操作,在 redis里有两个命令BLPOP和BRPOP需要使用这些操作(弹出列表头或者尾,实现方式见t_list.c:862行的 blockingPopGenericCommand函数),当指定的key不存在或者列表为空的情况下,那么客户端会一直阻塞,直到列表有数据时,服务端就会去执行lpop或者rpop并返回给客户端,那么什么时候需要用到BLPOP和BRPOP呢?大家平时肯定用redis做过队列,最常见的处理方式就是使用llen 去判断队列有没有数据,如果有数据就去取N条,然后处理,如果没有就sleep(3),然后继续循环,其实这里就可以使用BLPOP或者 BRPOP来轻松实现,而且可以减少请求,具体怎么实现留给大家思考;第三部分就是flushAppendOnlyFile(aof.c:60),这个函数主要目的是将aofbuf的数据写到文件,那aofbuf是什么呢?他是AOF的一个缓冲区,所以客户端的命令都会在处理完后把这些命令追加到这个缓冲区中,然后待一轮数据处理完之后统一写到文件(所以aof也是不能100%保证数据不丢失的,因为如果当redis正在处理这些命令的情况下服务就挂掉,那么这部分的数据是没有保存到硬盘的),大家都知道写数据到文件并不是立即写到硬盘,只是保存到一个文件缓冲区中,什么情况下会把缓冲区的数据转到硬盘呢?只要满足如下三种条件的一种就能将数据真正存到硬盘:1、手动调用刷新缓冲区;2、缓冲区已满;3、程序正常退出。

相关文档
最新文档