Redis进阶;底层数据结构实现

Redis进阶;底层数据结构实现

Redis K-V 底层设计原理

我们都知道redis单线程还这么快的原因是他是基于内存的;现如今内存的读取速度是极快的,ns级别的访问速度;仅次于cpu内部的多级缓存;

redis有哪些应用场景呢?

String底层数据结构

  • redis是使用C语言开发的,所以String底层肯定也是由C语言定义的;

c语言的string是一个char数组实现,但redis并没有直接采用这种实现方式,因为这种方式c在读取时是按照/0来截取数组的,因为在数组的最后c语言会加上/0来标记数组结束,但是redis面向的是各种语言的调用,所以它并不能直接使用这种结尾方式,

解决方式也不算难;那就是使用结构体存储,相当于Java中的对象之类的;无非就是解决字符截取的问题,那么直接定义一个长度表示元素在数组中的个数不就能解决了;所以

redis 3.2 以前
struct sdshdr {
    int len;//字符个数;
    int free;//剩余容量
    char buf[];//具体存放字符数据的数组,
};

但是这个做法又产生了新的问题,那就是结构体中的两个多余变量是int类型的啊,不也要占用空间;所以在3.2之后改成了如下:

redis 3.2typedef char *sds;

struct __attribute__ ((__packed__)) sdshdr5 {//容量2的5次方;
    unsigned char flags; /*一个字节做标记位,其中3bit用来标记类型, 5bit标记字符串长度 */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* used */已经使用的长度 
    uint8_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; /* used */
    uint16_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len; /* used */
    uint32_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
...

根据不同长度字符串选择不同容量的结构体,非人性化

redis底层数据库设计

我们都知道redis底层有16个数据库;那么数据库是怎么定义的呢?其实也很容易想到,那就是和上面的string一样,是redis自己设计的一个结构体用来存储数据库中元素信息;也就是定义一个结构体,里面的属性指针指向不同的字典存放具体的数据

typedef struct redisDb {//db对象
    dict *dict;                //字典  
    dict *expires;          //过期时间管理
    dict *blocking_keys;          //阻塞队列
    dict *ready_keys;           
    dict *watched_keys;         
    int id;                      //数据库序号
    long long avg_ttl;           //查看过期时间
    unsigned long expires_cursor;  
    list *defrag_later;          
} redisDb;

解释一下 dict:其实就是存放键值对的一个结构体(对象);在c++或Java中的体现是Map这种集合;

typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2];
    long rehashidx; 
    unsigned long iterators;  
    } dict;

这里面主要看dictht这个结构体;它就是存放键值对的地方;至于具体是如何存放的;可以预想的是;redis既然这么快,存放数据当然是数组这种形式;redis采用的方式也很容易想到,就是数组+ 链表;采用hash算法获取索引;存放在数组中,发生hash冲突之后就采用头插法构成链表;这和Java中hashMap不谋而合;

typedef struct dictht {
    dictEntry **table;//指向具体的键值对table
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
} dictht;


typedef struct dictEntry {
    void *key;   //键
    union {
        void *val; //值
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;//下一个
} dictEntry;

Redis的KEY:redis的可以都是string组成的,也就是开头说的sds结构体;

Redis的Value:Key统一都是string,但是redis有这么多中数据结构,他的值总当然也是由不同的数据结构构成!也就是val指针指向的是一个新的结构体;

typedef struct redisObject {
    unsigned type:4; //四个bit标记value类型 list set string hash
    unsigned encoding:4; //标记编码类型 raw int embstr 
    unsigned lru:LRU_BITS;  
    int refcount;
    void *ptr;
} robj;

List底层设计

  • List可以从两边添加元素,不难想到redis底层使用双端链表来实现的;

底层使用一种叫做ziplist来实现;

robj *createZiplistObject(void) {
    unsigned char *zl = ziplistNew();
    robj *o = createObject(OBJ_LIST,zl);
    o->encoding = OBJ_ENCODING_ZIPLIST;
    return o;
}

unsigned char *ziplistNew(void) {
    unsigned int bytes = ZIPLIST_HEADER_SIZE+ZIPLIST_END_SIZE;
    unsigned char *zl = zmalloc(bytes);
    ZIPLIST_BYTES(zl) = intrev32ifbe(bytes);
    ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE);
    ZIPLIST_LENGTH(zl) = 0;
    zl[bytes-1] = ZIP_END;
    return zl;
}


robj *createObject(int type, void *ptr) {
    robj *o = zmalloc(sizeof(*o));
    o->type = type;
    o->encoding = OBJ_ENCODING_RAW;
    o->ptr = ptr;
    o->refcount = 1;
    if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
        o->lru = (LFUGetTimeInMinutes()<<8) | LFU_INIT_VAL;  
    } else {
        o->lru = LRU_CLOCK();   // 获取 24bit 当前时间秒数
    }
    return o;
}

这种结构在产生连锁更新时会出问题所以升级为quickList

详细地址

Hash底层设计

  • Hash其实也是字典来实现的,不过在数据量少的时候其实是使用ziplist来实现的;因为ziplist是一串连续空间,也就是数组,获取元素很快;当元素变大是就会改为hashtable;

hashtable也就是字典dict;不再赘述;

Set底层设计

  • set是一个集合,并且并不重复,那么可以思考一下redis会如何选择数据结构呢?集合存放用一个数组便可以完成,但是去重该如何实现呢?

其实set底层使用了一个value恒为null的hashtable ,这样便可以达到去重的效果;

但是其中还有一个小细节:如果set存储的全是整形时会采用intset这个数据结构;

intset优点:占用空间少,查询可以用二分,速度也很快;数据量大可以升级,直到64位也表示不了就会使用hashtable;

Zset底层设计

  • zset与set的区别就是zset有序并且有两个元素,设置分数;键为元素 值为分数;根据分数排序;

既然要排序的话肯定是要用得链表;但是数据量大的时候元素的查找会变得很慢了;为了解决这种问题,redis采用了跳表这种数据结构;

  • 具体设计图:

可以看到,一条跳表由一个个节点构成,每一个节点都有对应的层级属性构成,另外还有指向前结点和后节点的指针,方便元素的查找;

渐进式rehash及扩容机制

上面我们介绍了redis数据库中都是键值对存储元素数据的,键统一都是string,而value则指向一个对象redisObject,而redisobject又指向不同的数据结构:ziplist hash set之类的;另外还有一个指针指向数据存放的内存地址(sds);

再看一下redis字典dict的定义:

typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2];
    long rehashidx; 
    unsigned long iterators;  
    } dict;

其中指向的dictht是一个数组,长度为二,那么这个是干嘛的?

redis底层采用数组+ 链表+hash散列来存储元素,那就会出现一个问题;链表过长怎么办,元素过多怎么办?

所以中间需要有一个扩容机制,也就设计到了数据的转移,所以才会出现两个dictht,就是用来转移数据用得,第一次加入数据会加入ht[0],扩容后rehash到ht[1];

可是问题又出现了,redis是单线程的;转移那麽多数据肯定耗时耗力;难道在此期间就不管用户请求了嘛?当然不是的,为了处理这个问题,redis采用了一种渐进式rehash的做法,也就是说rehash并不是一次性完成的,二是分批次完成;隔一段时间就rehash;

  • 那再扩容期间有数据插入或查询怎么办?

    • 插入:之间插入到扩容的新数组,
    • 查询:两个数组都会查一遍;
  • 如何扩容以及扩容判断条件

源码如下

/*
     * 如果哈希表ht[0]中保存的key个数与哈希表大小的比例已经达到1:1,即保存的节点数已经大于哈希表大小
     * 且redis服务当前允许执行rehash,或者保存的节点数与哈希表大小的比例超过了安全阈值(默认值为5)
     * 则将哈希表大小扩容为原来的两倍
     */
    if (d->ht[0].used >= d->ht[0].size &&
        (dict_can_resize ||
         d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))
    {
        return dictExpand(d, d->ht[0].used*2);
    }

既然有扩容当然也有缩容:

条件: 当哈希表保存的key数量与哈希表的大小的比例小于10%时需要缩容。最小容量为4

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值