主要参考:
Redis设计与实现 链接: http://pan.baidu.com/s/1jH45VMI 密码: nxhb
http://debugo.com/python-redis/
http://my.oschina.net/fuckphp/blog/270258
http://www.heychinaski.com/blog/2013/10/14/a-look-at-the-redis-source-code/
http://my.oschina.net/fuckphp/blog/277407
git clone -b 3.0 https://github.com/antirez/redis.git
Redis数据库中每个键值对儿的键总是一个字符串,而值可以为:字符串、列表、哈希、集合、有序集合。
typedef char *sds;
struct sdshdr {
unsigned int len; // 当前长度
unsigned int free; // 可用空间大小
char buf[];
};
static inline size_t sdslen(const sds s) {
struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr))); // 通过相对偏移计算s所在sdshdr结构体的首地址 ^_^
return sh->len;
}
// sds.c
int sdscmp(const sds s1, const sds s2) {
size_t l1, l2, minlen;
int cmp;
l1 = sdslen(s1);
l2 = sdslen(s2);
minlen = (l1 < l2) ? l1 : l2;
cmp = memcmp(s1,s2,minlen);
if (cmp == 0) return l1-l2;
return cmp;
}
// 双端链表的节点定义
typedef struct listNode {
struct listNode *prev;
struct listNode *next;
void *value;
} listNode;
// 迭代器,这是比较有意思的地方
typedef struct listIter {
listNode *next;
int direction; // 迭代方向
} listIter;
typedef struct list {
listNode *head;
listNode *tail;
void *(*dup)(void *ptr); // 通过函数指针实现对不同类型的支持,多态,在lighttpd中也有体现
void (*free)(void *ptr);
int (*match)(void *ptr, void *key);
unsigned long len;
} list;
字典在Redis中应用相当广泛,Redis的数据库的底层实现就是使用的字典。
// 哈希表中的节点:键值对
typedef struct dictEntry {
void *key; // 键
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v; // 值
struct dictEntry *next; // 用来解决键冲突,键相同的节点组成一个链表。(还记得解决hash冲突的常用方法吗:开放定址法,链地址法。)
} dictEntry;
// 哈希表
typedef struct dictht {
dictEntry **table; // 哈希表数组,数组中的每一项是一个指向dictEntry的指针,dictEntry中保存的是一个键值对儿。
unsigned long size; // 哈希表大小
unsigned long sizemask; // 用于计算索引值的掩码,总是等于size-1
unsigned long used; // 已有节点数量
} dictht;
typedef struct dictType {
unsigned int (*hashFunction)(const void *key);
void *(*keyDup)(void *privdata, const void *key);
void *(*valDup)(void *privdata, const void *obj);
int (*keyCompare)(void *privdata, const void *key1, const void *key2);
void (*keyDestructor)(void *privdata, void *key);
void (*valDestructor)(void *privdata, void *obj);
} dictType;
// 字典
typedef struct dict {
dictType *type; // 类型特定函数,hash函数
void *privdata;
dictht ht[2]; // 哈希表,默认使用0,rehash时使用1
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
int iterators; /* number of iterators currently running */
} dict;
Redis使用MurmurHash2算法来计算键的hash值。(还有djb hash)
随着操作不断进行,哈希表中保存的键值对逐渐地增大或减少,为了让哈希表的负载因子(load factor)维持在一个合理的范围之内,当哈希表保存的键值对数量太多或太少时,程序需要对哈希表进行扩展或收缩(通过rehash来完成)。
load factor = ht[0].used / ht[0].size
rehash是渐进式的,这是为了避免对服务器性能造成影响,所以分多次、渐进的将ht[0]里面的键值对慢慢地rehash到ht[1]。
http://blog.csdn.net/haidao2009/article/details/8206856
Skip List是一种随机化的数据结构,基于并联的链表,其效率可比拟于二叉查找树(对于大多数操作需要O(log n)平均时间)。基本上,跳跃列表是对有序的链表增加上附加的前进链接,增加是以随机化的方式进行的,所以在列表中的查找可以快速的跳过部分列表(因此得名)。所有操作都以对数随机化的时间进行。Skip List可以很好解决有序链表查找特定值的困难。
它是一种有序数据结构,通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。支持评价O(logN), 最坏O(N)复杂度的节点查找,大部分情况下效率可以与平衡树媲美,并且因为实现更为简单,所以有不少程序使用跳跃表来代替平衡树。
整数集合是集合的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis就会使用整数集合作为集合的底层实现。
127.0.0.1:6379> sadd num 1 2 3 4
(integer) 4
127.0.0.1:6379> TYPE num
set
127.0.0.1:6379> OBJECT ENCODING num
"intset"
127.0.0.1:6379> sadd stuff 1 "hello"
(integer) 2
127.0.0.1:6379> OBJECT encoding stuff
"hashtable"
可以看到当结合中都是整数元素时,底层是使用intset实现的;否则为hashtable。
这里贴出一个intset的APIintsetSearch
。
可以看到该函数的注释清晰、逻辑严谨,代码命名和排版精美,简直就是艺术品。^_^
/* Search for the position of "value". Return 1 when the value was found and * sets "pos" to the position of the value within the intset. Return 0 when * the value is not present in the intset and sets "pos" to the position * where "value" can be inserted. */
static uint8_t intsetSearch(intset *is, int64_t value, uint32_t *pos) {
int min = 0, max = intrev32ifbe(is->length)-1, mid = -1;
int64_t cur = -1;
/* The value can never be found when the set is empty */
if (intrev32ifbe(is->length) == 0) {
if (pos) *pos = 0;
return 0;
} else {
/* Check for the case where we know we cannot find the value, * but do know the insert position. */
if (value > _intsetGet(is,intrev32ifbe(is->length)-1)) {
if (pos) *pos = intrev32ifbe(is->length);
return 0;
} else if (value < _intsetGet(is,0)) {
if (pos) *pos = 0;
return 0;
}
}
/* 折半查找 */
while(max >= min) {
mid = ((unsigned int)min + (unsigned int)max) >> 1;
cur = _intsetGet(is,mid);
if (value > cur) {
min = mid+1;
} else if (value < cur) {
max = mid-1;
} else {
break;
}
}
if (value == cur) {
if (pos) *pos = mid; /* 找到了则返回该元素的位置 */
return 1;
} else {
if (pos) *pos = min; /* 没找到则返回可以插入的位置 */
return 0;
}
}
该函数首先对于特殊条件下的解进行了处理,然后进行折半查找。
ziplist是列表和哈希的底层实现之一。当一个列表只包含少量列表项,并且每个列表项要么是小整数值,要么是长度比较短的字符串,那么Redis就会用ziplist来做列表的实现。
当一个哈希表只包含少量的键值对,而且每个键值对的键和值要么就是小整数值、要么就是较短的字符串,Redis就会使用ziplist来实现哈希表。
前面学习了Redis中的数据结构,如SDS、双端链表、字典、压缩列表、整数集合等。
Redis并没有直接使用这些数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,这个系统包含字符串对象、列表对象、哈希对象、集合对象和有序集合对象这5种类型的对象,每种对象都用到了至少一种前面所介绍的数据结构。
通过这五种不同类型的对象,Redis可以在执行命令之前,根据对象的类型来判断一个对象是否可以执行给定的命令。使用对象的另一个好处是,我们可以针对不同的使用场景,为对象设置多种不同的数据结构实现,从而优化对象在不同场景下的使用效率。
除此之外,Redis的对象系统还实现了基于引用计数技术的内存回收机制,当程序不再使用某个对象的时候,这个对象所占用的内存就会被自动释放;另外,Redis还通过引用计数技术实现了对象共享机制,这一机制可以在适当的条件下,通过让多个数据库键共享同一个对象来节约内存。
最后,Redis的对象带有访问时间记录信息,该信息可以用于计算数据库键的空转时长,在服务器启用了maxmemory功能的情况下,空转时长较大的那些键可能会优先被服务器删除。
例如,下面的命令在数据库中创建了一个新的键值对,其中键值对的键是包含了字符串值”msg”的对象,而键值的值则是一个包含了字符串值”Hello”的对象。
127.0.0.1:6379> set msg "Hello"
OK
Redis中的每个对象由一个redisObject结构表示。
/* A redis object, that is a type able to hold a string / list / set */
/* The actual Redis Object */
#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; // 引用计数
void *ptr; // 指向底层实现数据结构的指针
} robj;
其中类型可以为:
/* Object types */
#define REDIS_STRING 0 // 字符串对象
#define REDIS_LIST 1 // 列表对象
#define REDIS_SET 2 // 集合对象
#define REDIS_ZSET 3 // 有序集合对象
#define REDIS_HASH 4 // 哈希对象
编码表示对象的底层实现是使用的哪种数据结构:
/* Objects encoding. Some kind of objects like Strings and Hashes can be * internally represented in multiple ways. The 'encoding' field of the object * is set to one of this fields for this object. */
#define REDIS_ENCODING_RAW 0 /* Raw representation */
#define REDIS_ENCODING_INT 1 /* Encoded as integer */
#define REDIS_ENCODING_HT 2 /* Encoded as hash table */
#define REDIS_ENCODING_ZIPMAP 3 /* Encoded as zipmap */
#define REDIS_ENCODING_LINKEDLIST 4 /* Encoded as regular linked list */
#define REDIS_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
#define REDIS_ENCODING_INTSET 6 /* Encoded as intset */
#define REDIS_ENCODING_SKIPLIST 7 /* Encoded as skiplist */
#define REDIS_ENCODING_EMBSTR 8 /* Embedded sds string encoding */
使用OBJECT ENCODING
命令可以查看一个数据库键对应的值对象的编码(即底层实现是用的什么数据结构)
命令TYPE
用于查看一个数据库键对应的值对象的类型是五种类型中的哪一个。
redisObject
结构体中有一个refcount
表示的就是该对象的引用计数,当引用计数为0时可以回收对象。基于此,可以实现对象的共享。例如,Redis会在服务器初始化时创建0~9999所对应的10000个字符串,达到共享的目的,从而节约内存。可以看到下面对1000的引用计数是2。
127.0.0.1:6379> SET A 1000
OK
127.0.0.1:6379> OBJECT REFCOUNT A
(integer) 2
源码见redis.c/initServer->createSharedObjects;
redisObject
结构体中有一个lru
属性,该属性记录了最后一个被访问的时间。
OBJECT IDLETIME
命令可以打印出给定键的空转时长,这是通过用当前事件减去lru得到的。
除了可以被OBJECT IDLETIME命令打印出来之外,键的空转时长还有另外一项作用:如果服务器打开了maxmemory选项,并且服务器用于回收内存的算法为volatile-lru或者allkeys-lru,那么当服务器占用的内存数超过了maxmemory选项所设置的上限值时,空转时长较高的那部分键会优先被服务器释放,从而回收内存。
配置文件的maxmemory选项和maxmemory-policy选项的说明介绍了关于这方面的更多信息。
// 当有客户端连接请求时,即监听套接字可读时,设置处理函数为acceptTcpHandler
initServer
---->aeCreateFileEvent(..., AE_READABLE, acceptTcpHandler);
// 事件处理主循环
main
---->aeMain
-------->aeProcessEvents(eventLoop, AE_ALL_EVENTS)
// 接受client的连接,并设置已连接套接字cfd上的可读事件处理函数为
acceptTcpHandler
---->cfd = anetTcpAccept(..., listenfd, ...);
---->acceptCommonHandler(cfd,0);
-------->createClient
------------>aeCreateFileEvent(...,cfd,AE_READABLE,readQueryFromClient)
// 当client有可读事件发生时,即client发送了命令给server,通过事件循环的调用过程如下
readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask)
---->processInputBuffer(redisClient *c)
-------->processCommand(redisClient *c)
------------>freeMemoryIfNeeded(void) //如果需要的话,就释放内存
具体过程应该是通过某种I/O复用技术(如epoll)来实现的事件的监听和处理(如epoll_wait)