redis核心

Redis底层K-V设计原理

Redis SDS Simple Dynamic String

1.二进制安全的数据分配(不会丢失数据)

2.提供了内存预分配机制,避免了频繁的内存分配

3.兼容c语言函数库

在c语言中字符串的表现形式为
char data[] = "abc\0";
 以0表示字符串的结尾
 如果出现本来就是\0字符做为字符串的话,会产生歧义

char data[] = "a\0bc\0";
 所以redis改成了新的数据结构sds
 保留了len,有多少读多少,不管中间有没有\0
 当使用append,setbit会动态扩容
socket网络中读取字节 
sds:
    free:0
    len:6
    char data[] = "abcdef\0";
这时追加3个字符,发现free字节为0,开始扩容
len = (len + 3) * 2 = 18
sds:
    free:9
    len:9
    char data[] = "abcdef123\0";
当len到达1MB(1024 * 1024 * 1 Byte)时,不会以上面的方式扩容

string 的数据结构在3.2 之前len是用int,由于buf的长度不可能一次就用到这么大的数字2^32-1,所以后面的版本做了优化,动态改变len类型

数据结构

redis 3.2 以前
struct sdshdr {
    int len;
    int free;
    char buf[];
};
redis 3.2 后

typedef char *sds;

struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
    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 {
........
#define SDS_TYPE_5  0
#define SDS_TYPE_8  1
#define SDS_TYPE_16 2
#define SDS_TYPE_32 3
#define SDS_TYPE_64 4
static inline char sdsReqType(size_t string_size) {
    if (string_size < 32)  
        return SDS_TYPE_5;
    if (string_size < 0xff) //2^8 -1  
        return SDS_TYPE_8;
    if (string_size < 0xffff) // 2^16 -1  
        return SDS_TYPE_16;
    if (string_size < 0xffffffff)  // 2^32 -1 
        return SDS_TYPE_32;
    return SDS_TYPE_64;
}

下面分别是sdshdr5sdshdr8的结构:sdshdr5的flags(1byte)前3位存储类型,后5位存储长度,sdshdr8的flags后5位是闲置的,len和alloc另外用了1byte存储

 

Redis渐进式rehash和动态扩容机制

redis为什么能存储海量数据?是怎么存储的?

redis默认是16个库,索引0-15,库的数据结构是redisDb,里面有一个dict属性(java里的HashTable和它类似,继承自Dictionary)

hash的两个特性:1.相同的输入一定是相同的输出。2.不同的输入,可能有相同的输出(hash碰撞)。当碰撞多的时候,链表就越长,时间复杂度逐渐由O(1)变成O(n)

arr[4]
hash(key) -> 自然数 % 4 = [0, arr.size-1]
hash(k1) % 4 = 0
hash(k2) % 4 = 1
hash(k3) % 4 = 1 (头插)
arr[0]->(k1,v1,next->null)
arr[1]->(k3,v3,next->k2)(k2,v2,next->null)

所以到一定数量时会扩容数组,当数组长度很大时,不会一次性迁移完成,而是渐进式rehash(扩容也是在主线程中执行的,也是单线程,是同步的,每一次都会迁移一部分,假设每次100hash槽)

Redis核心编码

String

redis6.0中的c源码
typedef struct redisDb {
    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;
redisDb中有dict
typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2];
    long rehashidx; 
    unsigned long iterators;  
    } dict;
dict中有两个dictht,就是hash表了,一个新数组,一个老数组用来复制旧数据,渐进式扩容,事件轮询渐进式rehash。
sizemask存放的是2^n - 1,方便做位运算,求hash槽是用hash(key)&(2^n-1)代替求模,这样速度更快
used是使用的槽个数,used/size = 1:1开始扩容
typedef struct dictht {
    dictEntry **table;
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
} dictht;
dictht中有dictEntry,存放的就是k-v,里面的next指向下一个entry,key指向sds,val指向redisObject
typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;
} dictEntry;
type有string,list,hash,set,zset等,type用来约束api的使用
encoding有raw,embstr,int,ziplist等
ptr指向最终数据的存储,当type为string,encoding为raw(原始类型),ptr就指向sds类型
typedef struct redisObject {
    unsigned type:4; //4bit
    unsigned encoding:4; //4bit
    unsigned lru:LRU_BITS; //24bit =3byte  
    int refcount; // 4byte
    void *ptr; //8byte,指针在内存中占8byte,长整型最大也是8byte,所以当是整型(0-2^64不超过20位,len<=20)时,直接用指针存储了值(字符串转成了整型),节省cpuIO时间和内存
} robj; //一共16byte

缓存行cache line:64byte
redisObject 16byte,48byte
如果要让数据存在缓存行,应该使用哪种数据结构?sdshdr5最大2^5-1=31byte,在48byte以内可以使用sdshdr8 ,sdshdr8 本身又占用4byte(len,alloc,flags都是1byte,buf[]数组结束符\0也是1byte),那最大可以存44byte,在44byte以内会存在cache line中达到优化效果
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[];
};

查看字符编码 3.2和6.2的版本有一定区别 ,右边是44byte的字符,编码是embstr,以前是raw

亿级用户日活统计bitmap(底层string类型),使用setbit给字符串offset设置0或1,返回旧值,当所有值置为0 时,这个字符串还是会存在于内存中,最大用到offset是100位,100/8=12.5就是13字节

 使用bitcount统计总数,bitcount key [start end],这里start end是字节索引,不是offset位。统计n天连续登陆用两个key按位与bitop and destKey [key ...],统计周活,用7天的key按位或or

bitmap就是string,最大也就是512MB=512 * 1024 * 1024 * 8bit= 2^(9+10+10+3),2^32-1个bit位,底层就是通过offset右移3位相当于/8算出byte数,根据byte,从字符数组根据byte索引找到那个字节,再用一些位运算得到bit值。

List

 

 以前的list存储空间不连续,pre,next指针都是一个类型8byte属于胖指针,redis会根据不同类型动态变化

zlbytes:32bit,表示ziplist占用的字节总数。
zltail:    32bit,表示ziplist表中最后一项(entry)在ziplist中的偏移字节数。通过zltail我们可以很方便地找到最后一项,从而可以在ziplist尾端快速地执行push或pop操作
zlen:     16bit, 表示ziplist中数据项(entry)的个数。
entry:表示真正存放数据的数据项,长度不定
zlend: ziplist最后1个字节,是一个结束标记,值固定等于255。
prerawlen: 前一个entry的数据长度。
len: entry中数据的长度
data: 真实数据存储



robj *createQuicklistObject(void) {
    quicklist *l = quicklistCreate();
    robj *o = createObject(OBJ_LIST,l);
    o->encoding = OBJ_ENCODING_QUICKLIST;
    return o;
}

quicklist *quicklistCreate(void) {
    struct quicklist *quicklist;
    quicklist = zmalloc(sizeof(*quicklist));
    quicklist->head = quicklist->tail = NULL;
    quicklist->len = 0;
    quicklist->count = 0;
    quicklist->compress = 0;
    quicklist->fill = -2;
    quicklist->bookmark_count = 0;
    return quicklist;
}
可以看到list里的头结点和尾结点都执行quicklistNode
typedef struct quicklist {
    quicklistNode *head;
    quicklistNode *tail;
    unsigned long count;       
    unsigned long len;           
    int fill : QL_FILL_BITS;                
    unsigned int compress : QL_COMP_BITS;  
    unsigned int bookmark_count: QL_BM_BITS;
    quicklistBookmark bookmarks[];
} quicklist;
quicklistNode里的zl就是ziplist
typedef struct quicklistNode {
    struct quicklistNode *prev;
    struct quicklistNode *next;
    unsigned char *zl;
    unsigned int sz;            
    unsigned int count : 16;    
    unsigned int encoding : 2;    
    unsigned int container : 2;  
    unsigned int recompress : 1; 
    unsigned int attempted_compress : 1; 
    unsigned int extra : 10;  
} quicklistNode;

List是一个有序(按加入的时序排序)的数据结构,Redis采用quicklist(双端链表) 和 ziplist 作为List的底层实现。 可以通过设置每个ziplist的最大容量,quicklist的数据压缩范围,提升数据存取效率

redis6版本的高级配置,文件redis.conf,redis3没有此配置

单个ziplist节点最大能存储8kb(-2代表8k),超过则进行分裂,将数据存储在新quicklistNode的ziplist节点中 
list-max-ziplist-size  -2 

0 代表所有节点,都不进行压缩,1, 代表从头节点往后走一个,尾节点往前走一个不用压缩,其他的全部压缩,2,3,4 ... 以此类推。
一般热点数据都存储在头结点或尾节点附近,所以不开启压缩
list-compress-depth  1   

Hash

Hash 数据结构底层实现为一个字典( dict ),也是RedisBb用来存储K-V的数据结构,当数据量比较小,或者单个元素比较小时,底层用ziplist存储,数据大小和元素数量阈值可以通过如下参数设置。

ziplist 元素个数超过 512 ,将ziplist改为hashtable编码 
hash-max-ziplist-entries  512    

单个元素大小超过 64 byte时,将ziplist改为hashtable编码
hash-max-ziplist-value    64      

 

string和hash的比较:string存储用set key value,如果一个用户的属性id:100,name:abc用string来存的话,那么属性越多,用户量大时就会频繁rehash。hash是hset key field1 value1,hash存用户就只会存在外面一个key中,避免了频繁rehash,但是不能针对某个key的feild设置过期时间,只能针对外面的key设置,string就可以。

Set

Set 为无序的,自动去重的集合数据类型,Set 数据结构底层实现为一个value 为 null 的 字典( dict ),当数据可以用整形表示时,Set集合将被编码为intset数据结构。两个条件任意满足时 Set将用hashtable存储数据。1.元素个数大于 set-max-intset-entries 512 ; 2.元素无法用整形表示(比如一开始全是整型的集合中出现了字符元素)

 如果集合全是整型,那么redis底层会自动去重并排序,这样方便查找(二分法),编码是intset,如果加入字符,就不会再排序,编码也变成hashtable类型

 
set-max-intset-entries 512       // intset 能存储的最大元素个数,超过则用hashtable编码

整数集合是一个有序的,存储整型数据的结构。整型集合在Redis
中可以保存int16_t,int32_t,int64_t类型的整型数据,并且可以保证
集合中不会出现重复数据。

encoding: 编码类型
length: 元素个数
contents[]: 元素存储

typedef struct intset {
    uint32_t  encoding;
    uint32_t  length;
    int8_t      contents[];
} intset; 
 
#define INTSET_ENC_INT16 (sizeof(int16_t))
#define INTSET_ENC_INT32 (sizeof(int32_t))
#define INTSET_ENC_INT64 (sizeof(int64_t))

ZSet

ZSet  为有序的,自动去重的集合数据类型,ZSet 数据结构底层实现为 字典(dict) + 跳表(skiplist) ,当数据比较少时,用ziplist编码结构存储。

 

 

 单个元素超过64byte原来的ziplist会改用skiplist编码(redis底层跳表没有严格的按照标准跳表设计),以前的跳表时间复杂度计算

数据项:N,索引层index 1(也就是level2)数据个数:N/2,index 2:N/2^2,index k:N/2^k。假设最高处数据为2=N/2^k,2^k=N/2,k=log2(N/2)相当于logN。以空间换时间

元素个数超过128 ,将用skiplist编码
zset-max-ziplist-entries 128   
单个元素大小超过 64 byte, 将用 skiplist编码
zset-max-ziplist-value 64     

Zset 数据结构
 // 创建zset 数据结构: 字典 + 跳表
robj *createZsetObject(void) {
    zset *zs = zmalloc(sizeof(*zs));
    robj *o;
    // dict用来查询数据到分数的对应关系, 如 zscore 就可以直接根据 元素拿到分值 
    zs->dict = dictCreate(&zsetDictType,NULL);
    
    // skiplist用来根据分数查询数据(可能是范围查找)
    zs->zsl = zslCreate();
   // 设置对象类型 
    o = createObject(OBJ_ZSET,zs);
    // 设置编码类型 
    o->encoding = OBJ_ENCODING_SKIPLIST;
    return o;
}
typedef struct zskiplistNode {
    sds ele;
    double score;
    struct zskiplistNode *backward;
    struct zskiplistLevel {
        struct zskiplistNode *forward;
        unsigned long span;
    } level[];
} zskiplistNode;
 typedef struct zskiplist {
    struct zskiplistNode *header, *tail;
    unsigned long length;
    int level;
} zskiplist;
 typedef struct zset {
    dict *dict;
    zskiplist *zsl;
} zset;

 backword是往后的指针, zrevrange myzset 0 -1 withscores这条指令就会用到。

reids3.2之后的新特性geohash

help @geo
 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值