Redis为什么快
-
Redis完全基于内存
大部分都是简单的存取操作,大量的时间花费在IO上。Redis绝大部分操作时间复杂度为O(1),所以速度十分快。
-
高效的网络io模型
Redis采用多路IO复用模型,在内部采用epoll代理。多路是指多个网络连接,IO复用是指复用同一个线程。
-
单线程
不存在上下文切换问题,也不用考虑锁的问题,不存在加锁释放锁的操作
Redis数据库设计
用key进行hash,对哈希值取模
链地址法
hash冲突:链表法,一个entry有next指针,(尾插法/头插法)
为什么用头插法
因为redis用在缓存,放了之后有希望马上被访问到
redis用的头插法会退化到O(n)
怎么解决退化到O(n)
当链表比当前hashtable长之后,redis进行hashmap扩容
rehash渐进式哈希
扩容之后进行重新取模,进行数据迁移
数据的迁移不是一次性完成的,而是可以通过dictRehash()这个函数分步规划的,并且调用方可以及时知道是否需要继续进行渐进式哈希操作。
rehash是以bucket(桶)为基本单位进行渐进式的数据迁移的
一个bucket对应哈希表数组中的一条entry链表
redis是单线程模型,渐进式哈希则将这种代价可控地分摊了
利用了两个哈希表进行的 , 有点类似数据库的迁移 , 读的时候先读旧库 , 读不到读新库 , 写的时候只写新库 ; 其他旧数据一点点的往新库上搬
Redis key数据类型
可以是任意类型
将任意key转换为string对象
Redis value数据类型
Redis String类型
没用直接使用c语言的string
自创了SDS(simple dynamic string)
SDS可以在O(1)的时间复杂度获取字符串长度
避免了字符串中出现\0
struct sdshdr {
//记录buf数组中已使用字节的数量
//等于SDS所保存字符串的长度
unsigned int len;
//记录buf数组中未使用字节的数量
unsigned int free;
//char数组,用于保存字符串
char buf[];
};
在后续版本,分了sdshdr5,sdshdr8,sdshdr16等等
sdshdr5:只有一个flag 类型是char(一个byte,8个bit,前3bit是type,后5是长度)和buf
sdshdr8/16:有alloc(对应之前的free),成倍扩容,后续变成alloc
Redis数据库源码设计
/* Redis数据库结构体 */
typedef struct redisDb {
// 数据库键空间,存放着所有的键值对(键为key,值为相应的类型对象)
dict *dict;
// 键的过期时间
dict *expires;
// 处于阻塞状态的键和相应的client(主要用于List类型的阻塞操作)
dict *blocking_keys;
// 准备好数据可以解除阻塞状态的键和相应的client
dict *ready_keys;
// 被watch命令监控的key和相应client
dict *watched_keys;
// 数据库ID标识
int id;
// 数据库内所有键的平均TTL(生存时间)
long long avg_ttl;
} redisDb;
当redis 服务器初始化时,会预先分配 16 个数据库,该数字配置在redis.conf配置文件中
所有数据库保存到结构 redisServer 的一个成员 redisServer.db 数组中,而redisClient中存在一个名叫db的指针指向当前使用的数据库(默认为0号数据库)。
typedef struct dict {
dictType *type;//一个指向dictType结构的指针。它使得dict的key和value能够存储任何类型的数据。
void *privdata;
dictht ht[2];//个字典中存放两个hash表,平常使用的是ht[0]上边的,只有扩容缩容的时候才会使用到另一个
long rehashidx; //正如注释所说,当值为-1的时候,表示没有进行rehash,否则,该值用来表示Hash表ht[0]执行rehash到了哪个元素,并记录该元素的数组下标值。
unsigned long iterators; //用来记录当前运行的安全迭代器数,当不为0的时候表示有安全迭代器正在执行,这时候就会暂停rehash操作。
} dict;
typedef struct dictType {
// hash方法,根据关键字计算哈希值
unsigned int (*hashFunction)(const void *key);
// 复制key
void *(*keyDup)(void *privdata, const void *key);
// 复制value
void *(*valDup)(void *privdata, const void *obj);
// 关键字比较方法,确认是原key相同还是hash冲突
int (*keyCompare)(void *privdata, const void *key1, const void *key2);
// 销毁key
void (*keyDestructor)(void *privdata, void *key);
// 销毁value
void (*valDestructor)(void *privdata, void *obj);
} dictType;
typedef struct dictht {
dictEntry **table;
unsigned long size;
unsigned long sizemask;
unsigned long used;
} dictht;
typedef struct dictEntry {
void *key;
union {
void *val; //value -->redisObject(类型)
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next;
} dictEntry;
typedef struct redisObject {
// 类型
unsigned type:4;
// 对齐位
unsigned notused:2;
// 编码方式 根据数据不同的值,使用不同方式存储
unsigned encoding:4;
// LRU 时间(相对于 server.lruclock)
unsigned lru:22;
// 引用计数
int refcount;
// 指向对象的值
void *ptr;
} robj;
Redis String int编码
因为 void *ptr;占8Byte,而longint也是8Byte,如果用8Byte的地址去存8Byte的longint就太蠢了,所以有做判断,
if len<=20 ,调用一个string2l函数,尝试将这个值转换为整形值,存到*ptr
Redis String embstr编码
embstr-->raw
cpu进行一次内存io,cache line读取64个byte
redisObject本身16byte,4byte格外支出(sdshdr8:len alloc flag \0),还剩44个字节
所以,如过小于44个byte,则称为embstr(嵌入式string)
Redis List
L/RPUSH
L/RPOP(POP之后会删除)
同向类似栈,反向类似队列
BRPOPLPUSH,pop后push到另一个list做备份
Redis List底层实现
采用quicklist和ziplist编码
没有用双端链表(next和pre指针加起来占了16byte)
而是使用了ziplist压缩链表
ziplist是一块连续的内存空间,元素之间紧挨着存储,没有任何冗余空隙。它的设计目标就是为了提高存储效率。ziplist可以用于存储字符串或整数,其中整数是按真正的二进制表示进行编码的,而不是编码成字符串序列。它能以O(1)的时间复杂度在表的两端提供push和pop操作。
struct ziplist<T> {
int32 zlbytes; //整个压缩列表占用字节数,包含本身
int32 zltail_offset; //最后一个元素距离压缩列表起始位置的偏移量,用于快速定位到最后一个节点,从而可以在ziplist尾部快速的执行push,pop操作
int16 zllength; //元素个数,该字段只有16bit所以可以表达的最大值为2^16-1,如果ziplist元素超了该值呢?这里规定,如果zllength小于等于 2^16-2,该字段表示为ziplist中元素的个数,否则想知道ziplist长度需要遍历整个ziplist
T[] entries;
int8 zlend; //ziplist最后一个字节,标志压缩列表的结束,值恒为 0xFF(255)
}
倒着遍历ziplist怎么能找到上个一个节点的开始位置呢?
由于压缩列表中的数据以一种不规则的方式进行紧邻,无法通过后退指针来找到上一个元素,而通过保存上一个节点的长度,用当前的地址减去这个长度,就可以很容易的获取到了上一个节点的位置,通过一个一个节点向前回溯,来达到从表尾往表头遍历的操作
struct entry {
int<var> prevlen; # 前一个 entry 的字节长度
int<var> encoding; # 元素类型编码
optional byte[] content; # 元素内容
}
它的 prevlen 字段表示前一个 entry 的字节长度,当压缩列表倒着遍历时,需要通过这个字段来快速定位到下一个元素的位置。它是一个变长的整数,当字符串长度小于 254(0xFE) 时,使用一个字节表示;如果达到或超出 254(0xFE) 那就使用 5 个字节来表示。
可
Redis Rehash实现
/* Performs N steps of incremental rehashing. Returns 1 if there are still
* keys to move from the old to the new hash table, otherwise 0 is returned.
*
* Note that a rehashing step consists in moving a bucket (that may have more
* than one key as we use chaining) from the old to the new hash table, however
* since part of the hash table may be composed of empty spaces, it is not
* guaranteed that this function will rehash even a single bucket, since it
* will visit at max N*10 empty buckets in total, otherwise the amount of
* work it does would be unbound and the function may block for a long time. */
int dictRehash(dict *d, int n) {
int empty_visits = n*10; /* Max number of empty buckets to visit. */
if (!dictIsRehashing(d)) return 0;
while(n-- && d->ht[0].used != 0) {
dictEntry *de, *nextde;
/* Note that rehashidx can't overflow as we are sure there are more
* elements because ht[0].used != 0 */
assert(d->ht[0].size > (unsigned long)d->rehashidx);
//如果正在遍历的hash槽索引是null
while(d->ht[0].table[d->rehashidx] == NULL) {
d->rehashidx++;
if (--empty_visits == 0) return 1;
}
//如果从非空的哈希槽头节点遍历
de = d->ht[0].table[d->rehashidx];
/* Move all the keys in this bucket from the old to the new hash HT */
//把ht[0]的数据迁移到ht[1]上
while(de) {
uint64_t 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;
//老的used--,新的++
d->ht[0].used--;
d->ht[1].used++;
de = nextde;
}
//搬完之后置零
d->ht[0].table[d->rehashidx] = NULL;
d->rehashidx++;
}
/* Check if we already rehashed the whole table... */
if (d->ht[0].used == 0) {
zfree(d->ht[0].table);
d->ht[0] = d->ht[1];
_dictReset(&d->ht[1]);
d->rehashidx = -1;
return 0;
}
/* More to rehash... */
return 1;
}
- 判断dict是否正在rehashing,只有是,才能继续往下进行,否则已经结束哈希过程,直接返回。
- 接着是分n步进行的渐进式哈希主体部分(n由函数参数传入),在while的条件里面加入对.used旧表中剩余元素数目的观察,增加安全性。
- 一个runtime的断言保证一下渐进式哈希的索引没有越界。
- 接下来一个小while是为了跳过空桶,同时更新剩余可以访问的空桶数,empty_visits这个变量的作用之前已经说过了。
- 现在我们来到了当前的bucket,在下一个while(de)中把其中的所有元素都迁移到ht[1]中,索引值是辅助了哈希表的大小掩码计算出来的,可以保证不会越界。同时更新了两张表的当前元素数目。
- 每一步rehash结束,都要增加索引值,并且把旧表中已经迁移完毕的bucket置为空指针。
- 最后判断一下旧表是否全部迁移完毕,若是,则回收空间,重置旧表,重置渐进式哈希的索引,否则用返回值告诉调用方,dict内仍然有数据未迁移。
渐进式哈希的精髓在于:数据的迁移不是一次性完成的,而是可以通过dictRehash()这个函数分步规划的,并且调用方可以及时知道是否需要继续进行渐进式哈希操作。
Redis Set实现
顺序排列,去除重复
intset hashtable
Redis 的 Set 是 String 类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。
集合对象的编码可以是 intset 或者 hashtable(过长,或者不是int)。
Redis 中集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。
typedef struct intset {
// 编码方式
uint32_t encoding;
// 集合包含的元素数量
uint32_t length;
// 保存元素的数组
int8_t contents[];
} intset;
- 检查set是否存在不存在则创建一个set结合。
- 根据传入的set集合一个个进行添加,添加的时候需要进行内存压缩
void saddCommand(redisClient *c) {
robj *set;
int j, added = 0;
// 取出集合对象
set = lookupKeyWrite(c->db,c->argv[1]);
// 对象不存在,创建一个新的,并将它关联到数据库
if (set == NULL) {
set = setTypeCreate(c->argv[2]);
dbAdd(c->db,c->argv[1],set);
// 对象存在,检查类型
} else {
if (set->type != REDIS_SET) {
addReply(c,shared.wrongtypeerr);
return;
}
}
// 将所有输入元素添加到集合中
for (j = 2; j < c->argc; j++) {
c->argv[j] = tryObjectEncoding(c->argv[j]);
// 只有元素未存在于集合时,才算一次成功添加
if (setTypeAdd(set,c->argv[j])) added++;
}
// 如果有至少一个元素被成功添加,那么执行以下程序
if (added) {
// 发送键修改信号
signalModifiedKey(c->db,c->argv[1]);
// 发送事件通知
notifyKeyspaceEvent(REDIS_NOTIFY_SET,"sadd",c->argv[1],c->db->id);
}
// 将数据库设为脏
server.dirty += added;
// 返回添加元素的数量
addReplyLongLong(c,added);
}
Redis Hash实现
数据量或者元素大小比较小,用ziplist,大就用hashtable
元素个数超过512或者单个元素大小超过64个byte
当为ziplist时,每个entry分别存k和v
Redis Zset实现
有score,数据量小用ziplist,大用skiplist
元素个数超过128或者单个元素大小超过64byte
跳表skiplist原理
skiplisttypedf struct zskiplist{
//头节点
struct zskiplistNode *header;
//尾节点
struct zskiplistNode *tail;
// 跳表中的元素个数
unsigned long length;
//表内节点的最大层数
int level;
}zskiplist;
有点像B+树(空间换时间)