Redis源代码分析
- 格式:docx
- 大小:280.65 KB
- 文档页数:6
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是个跳跃表,该跳跃表中,以槽位号为分数进⾏排序。
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源码解析——双向链表相对于之前介绍的字典和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源码分析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 源代码分析文档审核人文档拟制人文档提交时间胡戊(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源码解析之跳跃表(⼀)跳跃表(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源码笔记(持续更新)1.重置字符串void sdsclear(sds s) {// 取出 sdshdrstruct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr)));// 重新计算属性sh->free += sh->len;sh->len = 0;// 将结束符放到最前⾯(相当于惰性地删除 buf 中的内容)sh->buf[0] = '\0';}2.移动字符串/** 对 sds 左右两端进⾏修剪,清除其中 cset 指定的所有字符** ⽐如 sdsstrim(xxyyabcyyxy, "xy") 将返回 "abc"** 复杂性:* T = O(M*N),M 为 SDS 长度, N 为 cset 长度。
*/sds sdstrim(sds s, const char *cset) {struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr)));char *start, *end, *sp, *ep;size_t len;// 设置和记录指针sp = start = s;ep = end = s+sdslen(s)-1;// 修剪, T = O(N^2)while(sp <= end && strchr(cset, *sp)) sp++;while(ep > start && strchr(cset, *ep)) ep--;// 计算 trim 完毕之后剩余的字符串长度len = (sp > ep) ? 0 : ((ep-sp)+1);// 如果有需要,前移字符串内容// T = O(N)if (sh->buf != sp) memmove(sh->buf, sp, len);// 添加终结符sh->buf[len] = '\0';// 更新属性sh->free = sh->free+(sh->len-len);sh->len = len;// 返回修剪后的 sdsreturn s;}3.截取字符串void sdsrange(sds s, int start, int end) {struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr)));size_t newlen, len = sdslen(s);if (len == 0) return;if (start < 0) {start = len+start;if (start < 0) start = 0;}if (end < 0) {end = len+end;if (end < 0) end = 0;}newlen = (start > end) ? 0 : (end-start)+1;if (newlen != 0) {if (start >= (signed)len) {newlen = 0;} else if (end >= (signed)len) {end = len-1;newlen = (start > end) ? 0 : (end-start)+1;}} else {start = 0;}// 如果有需要,对字符串进⾏移动// T = O(N)if (start && newlen) memmove(sh->buf, sh->buf+start, newlen); // 添加终结符sh->buf[newlen] = 0;// 更新属性sh->free = sh->free+(sh->len-newlen);sh->len = newlen;}。
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源码解析-全览通过阅读 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 学习之SDS 源码分析⼀.SDS 的简单介绍SDS:简单动态字符串(simple dynamic string)1)SDS 是Redis 默认的字符表⽰,⽐如包含字符串值的键值对都是在底层由SDS 实现的2)SDS ⽤来保存数据库中的字符串值3)SDS 被⽤作缓冲区:⽐如AOF 模块的AOF 缓冲区,以及客户端状态中的输⼊缓冲区⼆.SDS 的结构分析:1)free=5:代表空闲空间长度为52)len=5:代表已经使⽤的空间长度为5三.Redis 使⽤SDS 的原因1)常数复杂度获取字符串长度:O(1)C 字符串获取字符串长度时间复杂度为O(N),使⽤SDS 可以确保获取字符串长度的操作不会成为Redis 的性能瓶颈2)杜绝缓冲区溢出C 字符串不记录⾃⾝长度和空闲空间,容易造成缓冲区溢出,使⽤SDS 则不会,SDS 拼接字符串之前会先通过free 字段检测剩余空间能否满⾜需求,不能满⾜需求的就会扩容3)减少修改字符串时带来的内存重分配次数使⽤C 字符串的话:每次对⼀个C 字符串进⾏增长或缩短操作,长度都需要对这个C 字符串数组进⾏⼀次内存重分配,⽐如C 字符串的拼接,程序要先进⾏内存重分配来扩展字符串数组的⼤⼩,避免缓冲区溢出,⼜⽐如C 字符串的缩短操作,程序需要通过内存重分配来释放不再使⽤的那部分空间,避免内存泄漏使⽤SDS 的话:通过SDS 的len 属性和free 属性可以实现两种内存分配的优化策略:空间预分配和惰性空间释放1.针对内存分配的策略:空间预分配在对SDS 的空间进⾏扩展的时候,程序不仅会为SDS 分配修改所必须的空间,还会为SDS 分配额外的未使⽤的空间这样可以减少连续执⾏字符串增长操作所需的内存重分配次数,通过这种预分配的策略,SDS 将连续增长N 次字符串所需的内存重分配次数从必定N 次降低为最多N 次,这是个很⼤的性能提升!2.针对内存释放的策略:惰性空间释放在对SDS 的字符串进⾏缩短操作的时候,程序并不会⽴刻使⽤内存重分配来回收缩短之后多出来的字节,⽽是使⽤free 属性将这些字节的数量记录下来等待将来使⽤,通过惰性空间释放策略,SDS 避免了缩短字符串时所需的内存重分配次数,并且为将来可能有的增长操作提供了优化!4)⼆进制安全为了确保数据库可以⼆进制数据(图⽚,视频等),SDS 的API 都是⼆进制安全的,所有的API 都会以处理⼆进制的⽅式来处理存放在SDS 的buf 数组⾥⾯的数据,程序不会对其中的数据做任何的限制,过滤,数据存进去是什么样⼦,读出来就是什么样⼦,这也是buf 数组叫做字节数组⽽不是叫字符数组的原因,以为它是⽤来保存⼀系列⼆进制数据的通过⼆进制安全的SDS ,Redis 不仅可以保存⽂本数据,还可以保存任意格式是⼆进制数四.SDS 的主要API 及其源码解析1)sdsnew 函数:创建⼀个包含给定字符串的SDS sds sdsnew(const char *init)struct sdshdr {// buf 中已占⽤空间的长度int len;// buf 中剩余可⽤空间的长度int free ;// 字节数组char buf[];};/** 根据给定字符串 init ,创建⼀个包含同样字符串的 sds** 参数* init :如果输⼊为 NULL ,那么创建⼀个空⽩ sds* 否则,新创建的 sds 中包含和 init 内容相同字符串** 返回值* sds :创建成功返回 sdshdr 相对应的 sds* 创建失败返回 NULL** 复杂度* T = O(N)*/sds sdsnew(const char *init) {size_t initlen = (init == NULL) ? 0 : strlen(init);return sdsnewlen(init, initlen);}/** 根据给定的初始化字符串 init 和字符串长度 initlen* 创建⼀个新的 sds** 参数* init :初始化字符串指针* initlen :初始化字符串的长度** 返回值* sds :创建成功返回 sdshdr 相对应的 sds* 创建失败返回 NULL** 复杂度* T = O(N)*/sds sdsnewlen(const void *init, size_t initlen) {struct sdshdr *sh;// 根据是否有初始化内容,选择适当的内存分配⽅式// T = O(N)if (init) {// zmalloc 不初始化所分配的内存sh = zmalloc(sizeof(struct sdshdr) + initlen + 1);}else {// zcalloc 将分配的内存全部初始化为 0sh = zcalloc(sizeof(struct sdshdr) + initlen + 1);}// 内存分配失败,返回if (sh == NULL) return NULL;// 设置初始化长度sh->len = initlen;// 新 sds 不预留任何空间sh->free = 0;// 如果有指定初始化内容,将它们复制到 sdshdr 的 buf 中// T = O(N)if (initlen && init)memcpy(sh->buf, init, initlen);// 以 \0 结尾sh->buf[initlen] = '\0';// 返回 buf 部分,⽽不是整个 sdshdr,因为sds是char指针类型的别名return (char*)sh->buf;}2)sdsempty函数:创建⼀个不包含任何内容的SDSsds sdsempty(void)/** 创建并返回⼀个只保存了空字符串 "" 的 sds** 返回值* sds :创建成功返回 sdshdr 相对应的 sds* 创建失败返回 NULL** 复杂度* T = O(1)*/sds sdsempty(void) {return sdsnewlen("", 0);}3)sdsfree函数:释放给定的SDS/** 释放给定的 sds** 复杂度* T = O(N)*/void sdsfree(sds s) {if (s == NULL) return;zfree(s - sizeof(struct sdshdr));}ps:zfree函数为内存管理模块中的函数,我们在这⾥先不探究,只需要知道它可以释放指定的空间就可以了分析:s - sizeof(struct sdshdr)到底返回的是什么呢?⾸先我们知道SDS的buf数组是柔性数组,也就是这个数组是不占据内存⼤⼩的,所以sizeof(struct sdshdr)为8还有SDS数据类型为char *类型,所以假如存在这样⼀条语句:struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr)))其结构的内存结构图为:所以s-(sizeof(struct sdshdr))就是指向sds的头部的!!4)sdslen函数:返回SDS的已使⽤的空间字节数/** 返回 sds 已经使⽤的空间字节数** T = O(1)*/static inline size_t sdslen(const sds s){struct sdshdr *sh = (void*)(s - (sizeof(struct sdshdr)));return sh->len;}static inline修饰的函数是内联函数,⽬的是解决函数在多次调⽤时候的效率问题!size_t是⽆符号整数,是sizeof操作符返回的结构类型const代表变量只能读,不能被修改5)sdsavail函数:返回SDS的未使⽤的空间字节数/** 返回 sds 为使⽤的空间字节数** T = O(1)*/static inline size_t sdsavail(const sds s){struct sdshdr *sh = (void*)(s - (sizeof(struct sdshdr)));return sh->free;}跟sdslen函数的区别就在于返回的属性不同6)sdsup函数:创建⼀个给定SDS的副本(copy)/** 复制给定 sds 的副本** 返回值* sds :创建成功返回输⼊ sds 的副本* 创建失败返回 NULL** 复杂度* T = O(N)*/sds sdsdup(const sds s) {return sdsnewlen(s, sdslen(s));}调⽤了sdsnewlen函数和sdslen函数,这两个函数在上⾯都有介绍7)sdsclear函数:清空SDS保存的字符串内容/** 在不释放 SDS 的字符串空间的情况下,* 重置 SDS 所保存的字符串为空字符串。
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中,在理想的情况下只需一次就能定位命令的处理函数。
voidpopulateCommandTable(void) {int j;intnumcommands =sizeof(readonlyCommandTable)/sizeof(structredisCommand);for (j = 0; j <numcommands; j++) {structredisCommand *c = readonlyCommandTable+j;intretval;retval = dictAdd(mands, sdsnew(c->name), c);assert(retval == DICT_OK);}}3、对参数的解析,redis-server有一个参数(可以不需要),这个参数是指定配置文件路径,然后由函数loadServerConfig(config.c:28)加载所有配置if (argc == 2) {if (s trcmp(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、客户端链表、公共字符串等。
voidinitServer() {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、程序正常退出。