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的值也以这种方式存储呢,反正指针的内存省不掉 --看错了,大于10000的要创建一个新的robj对象,耗内存多了);除此之外,以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;
        }

可以看到,旧的一个桶分裂成了新的两个桶,新的hash表和旧表相比同一个桶内的元素是倒置的。

数据到达最大内存如何处理?

redis通过"zmalloc_"开头的一系列方法,将使用的内存做了统计,如果客户端发送了可能占用内存的命令,且当前统计的内存超过了设定,将会根据配置项maxmemory-policy,这个配置有如下取值:volatile-random,volatile-lru,volatile-ttl,allkeys-lru,allkeys-random,noeviction几种取值,采取一定的措施来减小内存。实现此步骤的函数是freeMemoryIfNeeded,下面一个个说明各个取值含义。

  • noeviction ,不删除已有数据,给客户端返回失败。

  • allkeys-random,从整个字典中随机选取键值对删除。

  • volatile-random 从有过期时间的键值对中选取随机值。

  • volatile-ttl 从带过期的键中选取5个比较找出最早的过期键。可以看出这个算法随机性还是比较强的,并不是从所有键值对中找出最早的过期键。

  • allkeys-lru和volatile-lru lru机制是这几种里面最复杂的了,着重说一下,可能有些认识不足,每次访问会更新键值对(实际存储在value中)的最后访问时间,db->eviction_pool存储了redis中将要淘汰的键,一共存储16个键,值按照值是该键的最后访问距今的时间,所以idle值越大,说明数据越该淘汰,淘汰实现函数是evictionPoolPopulate,首先从目标字典中选取16个key,依次计算每个键值对的最后访问时间,判断是否比eviction_pool的记录都大,是则更新,产生一个更新后的eviction_pool。这里有几个问题,因为eviction_pool存储的数据可能是好几轮之前的旧数据了,所以如果有客户端删除了某个eviction_pool中的键值对,eviction_pool是感知不了的。即eviction_pool可能存在废数据,这个问题redis在代码中又做了查询处理,存在才删除。第二,同样是旧数据导致的, 如果原来处于eviction_pool的数据被触发了查询,更新了lru值,但eviction_pool中是不会更新的,下轮evictionPoolPopulate依然可能将此"热点数据"删除。第三,和第二条类似,旧数据的idle值是不会更新的,但是随着时间的推移,这个本该要更新的。总结一下redis中lru的缺点,1.redis的lru算法仅仅是局部数据间的比较,对比的不全面;2.redis每次淘汰的数据未必是eviction_pool中最应该淘汰的数据。

 

 

 


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值