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.2 后
typedef char *sds;
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags;
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; 已经使用的长度
uint8_t alloc;
unsigned char flags;
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
uint16_t len;
uint16_t alloc;
unsigned char flags;
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
uint32_t len;
uint32_t alloc;
unsigned char flags;
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
...
根据不同长度字符串选择不同容量的结构体,非人性化
redis底层数据库设计
我们都知道redis底层有16个数据库;那么数据库是怎么定义的呢?其实也很容易想到,那就是和上面的string一样,是redis自己设计的一个结构体用来存储数据库中元素信息;也就是定义一个结构体,里面的属性指针指向不同的字典存放具体的数据
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;
解释一下 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;
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;
unsigned encoding:4;
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();
}
return o;
}
这种结构在产生连锁更新时会出问题所以升级为quickList
详细地址
Hash底层设计
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;
-
那再扩容期间有数据插入或查询怎么办?
- 插入:之间插入到扩容的新数组,
- 查询:两个数组都会查一遍;
-
如何扩容以及扩容判断条件
源码如下
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
|