Redis

主要参考:
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

源码下载, 我下载的是3.0分支:

git clone -b 3.0 https://github.com/antirez/redis.git

Redis数据库中每个键值对儿的键总是一个字符串,而值可以为:字符串、列表、哈希、集合、有序集合。

sds 简单动态字符串 Simple Dynamic String

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;
}
  • len属性,可以以O(1)返回字符串长度。
  • free属性保存可用空间大小。内存分派策略为预分派和惰性释放,提高效率。杜绝缓冲区溢出,对sds进行修改时会检测是否有足够的空间。
  • 二进制安全(binary-safe),C字符串只能保存文本数据,遇到’\0’则认为字符串结束。sds 的API以二进制方式处理放在buf的数据。可以看到源码中比较操作用的是memcmp,而不是strcmp
// 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;
}

链表 adlist

// 双端链表的节点定义
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)

rehash

随着操作不断进行,哈希表中保存的键值对逐渐地增大或减少,为了让哈希表的负载因子(load factor)维持在一个合理的范围之内,当哈希表保存的键值对数量太多或太少时,程序需要对哈希表进行扩展或收缩(通过rehash来完成)。
load factor = ht[0].used / ht[0].size
rehash是渐进式的,这是为了避免对服务器性能造成影响,所以分多次、渐进的将ht[0]里面的键值对慢慢地rehash到ht[1]。

渐进式rehash过程:
  • 为ht[1]分配空间
  • rehashidx = 0,表示rehash从index=0的地方开始
  • 程序每次对字典执行添加、删除、查找或者更新操作时,程序都会顺带将 ht[0][rehashidx]—rehash–> ht[1] 中,并且rehashidx++
    基本思想是将rehash分散到对字典的每个添加、删除、查找和更新操作上,从而避免了集中式rehash带来的庞大计算量。
    其中,在进行渐进式rehash的过程中,字典的删除、查找、更新等操作会在两个哈希表上进行。例如,查找一个键时,会先在ht[0]里面查找,如果没有找到,会继续到ht[1]里进行查找。
    而rehash期间的添加操作一律被保存在ht[1]中,ht[0]中只减不增,随着rehash的进行,最终变为空表。

跳跃表 skiplist

http://blog.csdn.net/haidao2009/article/details/8206856
Skip List是一种随机化的数据结构,基于并联的链表,其效率可比拟于二叉查找树(对于大多数操作需要O(log n)平均时间)。基本上,跳跃列表是对有序的链表增加上附加的前进链接,增加是以随机化的方式进行的,所以在列表中的查找可以快速的跳过部分列表(因此得名)。所有操作都以对数随机化的时间进行。Skip List可以很好解决有序链表查找特定值的困难。

它是一种有序数据结构,通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。支持评价O(logN), 最坏O(N)复杂度的节点查找,大部分情况下效率可以与平衡树媲美,并且因为实现更为简单,所以有不少程序使用跳跃表来代替平衡树。

Redis只在两个地方用到了跳跃表

  • 有序集合
  • 集群节点中用作内部数据结构

整数集合 intset

整数集合是集合的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素数量不多时,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

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 */

Redis_第1张图片
使用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)

你可能感兴趣的:(redis)