手记

redis源码分析(四):数据读写相关操作

数据存储是一个数据库的核心功能,对于redis来说,最重要的任务是缓存,redis默认有16个数据库,首次连接使用的是db0,可以用select语句来选择其他编号的数据库和客户端绑定。

存储格式

redisDb的数据结构以及重要的成员变量结构如下:

typedef struct redisDb {

    // 数据库键空间,保存着数据库中的所有键值对
    dict *dict;                 /* The keyspace for this DB */

    // 键的过期时间,字典的键为键,字典的值为过期事件 UNIX 时间戳
    dict *expires;              /* Timeout of keys with a timeout set */

    // 正处于阻塞状态的键
    dict *blocking_keys;        /* Keys with clients waiting for data (BLPOP) */

    // 可以解除阻塞的键
    dict *ready_keys;           /* Blocked keys that received a PUSH */

    // 正在被 WATCH 命令监视的键
    dict *watched_keys;         /* WATCHED keys for MULTI/EXEC CAS */

    struct evictionPoolEntry *eviction_pool;    /* Eviction pool of keys */

    // 数据库号码
    int id;                     /* Database ID */

    // 数据库的键的平均 TTL ,统计信息
    long long avg_ttl;          /* Average TTL, just for stats */} redisDb;typedef struct dict {

    // 类型特定函数
    dictType *type;    // 私有数据
    void *privdata;    // 哈希表
    dictht ht[2];    // rehash 索引
    // 当 rehash 不在进行时,值为 -1
    int rehashidx; /* rehashing not in progress if rehashidx == -1 */

    // 目前正在运行的安全迭代器的数量
    int iterators; /* number of iterators currently running */} dict;typedef struct dictht {
    
    // 哈希表数组
    dictEntry **table;    // 哈希表大小
    unsigned long size;    
    // 哈希表大小掩码,用于计算索引值
    // 总是等于 size - 1
    unsigned long sizemask;    // 该哈希表已有节点的数量
    unsigned long used;

} dictht;typedef struct dictEntry {
    
    // 键
    void *key;    // 值
    union {        void *val;        uint64_t u64;        int64_t s64;
    } v;    // 指向下个哈希表节点,形成链表
    struct dictEntry *next;} dictEntry;typedef struct redisObject {

    // 类型
    unsigned type:4;//有5种取值 //#define REDIS_STRING 0//#define REDIS_LIST 1//#define REDIS_SET 2//#define REDIS_ZSET 3//#define REDIS_HASH 4

    // 编码
    unsigned encoding:4;    // 对象最后一次被访问的时间
    unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */

    // 引用计数
    int refcount;    // 指向实际值的指针
    void *ptr;

} robj;

其中最关键的结构是dictEntry,dictEntry是哈希表的节点,val指针存储着实际的数据,redis的key都是字符串格式的robj,value也是一个robj对象,encoding有8种编码取值,分别对应着几种redis的内部结构:

#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的数据类型只有5种,他们的关系如下:

  • string类型的数据:如果value是一个数字的话:小于10000的值存储在共享空间(相同的value对象的地址都是一样的,参考java装箱中-128~127的整数),大于10000小于21个字节的部分以REDIS_ENCODING_INT方式编码,这样robj的ptr指针可以直接存储这个整数(这里有疑问,为什么不把小于10000的值也以这种方式存储呢,反正指针的内存省不掉);除此之外,以REDIS_ENCODING_EMBSTR的方式编码,redis可存储的最大字符串是512M 。

  • list类型的数据:如果list的长度小于配置,内部以ziplist存储,否则以linkedlist存储。配置项list-max-ziplist-entries,默认512。

  • set类型的数据:如果插入的都是整形数据,那么存储在intset中,只要set中存入一个非整型数据,立刻转换成hash table。

  • zset类型的数据:相比set多了一个排序的功能,zset数据默认以ziplist的结构存储,如果满足以下条件其一,转为skiplist。(1).zset节点数大于配置的zset-max-ziplist-entries,默认128;(2)zset插入的新节点长度大于配置项zset-max-ziplist-value,默认64字节。

  • hash类型的数据: 默认是ziplist ,当满足以下条件之一,转化为dict方式存储。(1).hash的数量大于hash-max-ziplist-entries,默认512;(2)hash表有一个值的长度超过hash-max-ziplist-value,默认64。

blocking_keys参数主要用于brpoplpush(弹出a队列队尾,并插入b队列队首),brpop(弹出队尾),blpop(弹出队首)命令,当alpha队列为空时,这些命令会阻塞请求的客户端一段时间,直到期间有其他客户端对这个key进行push操作或者超时,这些命令的语法为:(去掉前面的b即为对应的非阻塞语法)。

brpoplpush alpha reciver timeout

brpop alpha
blpop alpha

一个命令从发起到返回的过程

管中窥豹,我们以基本的set为例。1.分析指令参数;2.调用redisCommandTable中对应的函数进行处理;3.查询dict结构中是否有该键,有则更新,没有则创建一个新的,设置value;4.组好返回给客户端的报文。

如何处理过期的键?

所有带过期事件的键,均有一份备份位于字典expires中。redis内部有两种机制来清除过期数据,1.在时间事件周期内,主动调用activeExpireCycle来清除过期键,每个db最多随机取20个键来判断超时时间,超时了则执行删除逻辑,同时清除redisDb中的expires和dict两个字典。2.当有客户端主动来执行这个键相关操作的时候(get,expire ,incr,等等),主动检查此键在expires中是否过期,过期则执行删除。 可见redis不能保证数据一过期就立马删除的。

数据库字典的resize逻辑

当数据量少于hash表桶数量的10%,或者数据量大于桶的数量的时候,发生字典的resize()。每个字典有两个hash表,即dictht ht[2];ht[0]是实际使用的hash表,而ht[1]是在resize()的时候启用,resize()时启用rehashidx字段,标记着rehash进展到哪个桶了,数据将分次从ht[0]迁移到ht[1],迁移和处理过期的键类似,也是有一个时间事件的周期每次最大处理100个桶,普通的查询修改等操作,每次处理一个桶。迁移完成时令ht[0] = ht[1],然后置空ht[1],恢复了常态,如此resize()调整完成。核心代码如下:

        // 略过数组中为空的索引,找到下一个非空索引
        while(d->ht[0].table[d->rehashidx] == NULL) d->rehashidx++;        // 指向该索引的链表表头节点
        de = d->ht[0].table[d->rehashidx];        /* Move all the keys in this bucket from the old to the new hash HT */
        // 将链表中的所有节点迁移到新哈希表
        // T = O(1)
        while(de) {
            unsigned int h;            // 保存下个节点的指针
            nextde = de->next;            /* Get the index in the new hash table */
            // 计算新哈希表的哈希值,以及节点插入的索引位置
            h = dictHashKey(d, de->key) & d->ht[1].sizemask;            // 插入节点到新哈希表
            de->next = d->ht[1].table[h];
            d->ht[1].table[h] = de;            // 更新计数器
            d->ht[0].used--;
            d->ht[1].used++;            // 继续处理下个节点
            de = nextde;
        }



作者:msrpp
链接:https://www.jianshu.com/p/c0c4d521f3bb


0人推荐
随时随地看视频
慕课网APP