这里写目录标题
SDS:简单动态字符串(enbster)
- 数据结构:
- 杜绝缓存溢出:
当需要对SDS进行修改时,会先检查SDS的空间是否满足要求,如果不能,就会自动将SDS的空间扩展至所需大小,在执行修改工作。 - 内存分配策略
3.1 空间预分配(用于优化SDS的增长操作)
如果SDS空间需要扩展,redis不仅会给SDS分配所需的空间,还会额外分配未使用的空间。未使用空间的大小由SDS的长度决定。如果SDS长度小于1MB,则未使用空间free = 使用空间len;如果大于1MB,则每次分配1MB的未使用空间。
3.2 惰性空间释放(用于优化SDS的缩短操作)
在字符串修改之后缩短时,SDS会有多余的空间,这个空间并不会被立即释放,而是保留下来用于SDS增长时的需求。
链表(linkedlist)
- 链表结构实现
字典(HT)
字典,又称为符号表,关联数据或者映射,是用于保存键值对的key - value 的抽象数据结构。它的每个键值都是独一无二的
- 数据结构
- 哈希算法
Redis会通过计算键值得哈希值来确定在数组中的索引位置,在将键值对的哈希表节点以链表的形式放在指定索引上。计算哈希值得算法是MurmurHash2算法。 - 哈希冲突
当两个或两个以上的键被分配到同一个索引上面时,就会产生哈希冲突。redis使用链地址法来解决键冲突。每一个哈希表节点都维护一个next指针,新节点会被添加到链表的头结点位置,它的next指针指向已经存在的节点。 - rehash
当哈希表的键值对数量太多或者太少时,程序会对哈希表的大小进行相应的扩展或者收缩,这是通过rehash来实现的。
进行rehash的条件:
负载因子 = 哈希表已保存节点数量 / 哈希表大小
如果服务器没有执行BGSAVE或BGREWRITEAOF命令,且负载因子大于等于1时,需要对哈希表进行扩展;
如果服务器正在执行BGSAVE或者BGREWIRTEAOF命令,且负载因子大于等于5时,也要执行扩展;
如果负载因子小于0.1时,需要对哈希表执行收缩操作。
rehash实现过程:
为字典的ht[1]哈希表分配空间(如果是扩展,则ht[1]的大小为第一个ht[0].used*2的2^n;如果是收缩,ht[1]的大小为第一个ht[0].used的2 ^n)
将保存在ht[0]中的所有键值对rehash到ht[1]中:rehash指重新计算哈希值和索引值,然后将键值对放置到ht[1]哈希表的指定位置上。
当ht[0]所有的键值对都迁移到ht[1]之后,释放ht[0],将ht[1]设置为ht[0],并在ht[1]新创建一个空白哈希表,为下一次rehash做准备。
渐进式rehash:
rehash过程并不是一次性完成的,而是渐进的、分多次的完成。
因为如果键值对数量比较大的时候,要一次性将ht[0]的数据全部rehash到ht[1]中会导致服务器在一段时间内停止服务。
1)为ht[1]分为空间。让字典同时持有ht[0] 和 ht[1]两个哈希表
2)在字典中维持一个索引计数器变量rehashidx,并将它的值设置为0,表示rehash正式开始。
3)在rehash进行期间,每次对字典进行增删改查,除了执行指定操作外,还会顺带将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1],当rehash工作完成之后,程序将rehashidx属性+1;
4)直到ht[0]上所有的键值对都被rehash到ht[1],这是程序将rehash属性的值设置为-1,表示操作已经完成。
跳跃表(skiplist)
跳跃表是一种有序的数据结构与,它在每个节点都维持多个指向其他节点的指针,从而能实现快速访问。
查找复杂度:平均:O(logn);最坏:O(N)
作用:是有序集合键的底层实现之一,如果有序集合包含的元素数量比较多或者成员是比较长的字符串时,Redis就会使用跳表来作为底层实现。
redis用到链表的地方:有序集合键、集群节点中用作内部数据结构。
整数集合(intset)
整数集合是集合键的底层实现之一。当一个集合只包含整数值元素,且这个集合元素数量不多时,就会使用整数结合作为集合键的底层实现。
- 数据结构
- 升级
当编码方式为int16时,新添加的元素是一个int64类型的整数,这时候集合所有的元素都会被转换成int64类型。这就整数集合的升级。
升级步骤:
1)根据新元素的类型,扩展整数集合底层数组的空间大小,并为新元分配空间。
2)将底层数据所有的元素都转换成和新元素相同的类型,并将转换后的元素放到正确的位置,同时需要维护底层数组的有序性不变。
3)将新元素添加到底层数组中。
升级的优势:提升灵活性;节约内存。 - 降级
整数集合不支持降级操作,一旦数组升级之后,编码状态就会一致保存保存在升级之后的状态。
压缩列表(ziplist)
压缩列表是列表键和哈希建的底层实现之一,如果列表建只包含少量列表项,且每个列表项是小整数值或短字符串,redis就会使用压缩列表来实现底层。
- 数据结构
- 连锁更新
因为enrty中有个属性是privious_entry_length,这个属性的长度是由前一个节点长度决定的,如果前一个节点长度小于254字节,则privious_entry_length只需要1个字节长,但如果大于等254字节,则需要5个字节长。
假设当前有若干个entry,每个节点长度都在250-253字节之间。这个时候,因为每个节点的长度都没有超过254字节,所以所有节点的privious_entry_length属性长度都是1字节。但是当我们新插入一个节点A,它的长度达到了254字节,则它的后一个节点B为了能表示新节点A的长度,主要将原本1字节长的privious_entry_length扩展成5字节,这时节点B本身的长度也到达了254(或以上),同理,节点B的后一个节点C为了能表示节点B的长度,也需要将privious_entry_length属性的长度扩展。。。也就是说,节点A后面的所有节点都需要进行扩展,这就会造成连锁更新。
对象
redis使用对象来表示数据库的键和值。每次创建一个新的键值对时,就会至少创建两个对象,一个表示键,一个表示值。
typrof struct redisObject{
//类型
unsigned type:4;
//编码
unsigned encoding:4;
//指向底层实现数据结构的指针
void *ptr;
...
}
- 类型
常用类型 | 对象的名称 | 添加操作 |
---|---|---|
REDIS_STRING | 字符串对象 | SET |
REDIS_LIST | 列表对象 | RPUSH |
REDIS_HASH | 哈希对象 | HMSET |
REDIS_SET | 集合对象 | SADD |
REDIS_ZSET | 有序集合对象 | ZADD |
- 编码
字符串对象
字符串对象的编码可以是 int、raw 或者embstr
编码转换:对于int编码的字符串对象来说,如果执行的操作是的这个对象保存的不再是整数值而是字符串值,那么字符串编码会从int变成raw;对于embstr编码的字符串对象实际上是只读的,如果要修改,会先将对象的编码从embstr转换成raw。再执行修改命令。
列表对象
列表对象的编码有两种:ziplist 或者 liknedlist
当列表对象保存的所有字符串元素长度都小于64字节,且列表保存的元素数量小于512个,则使用ziplist编码保存数据。
RPUSH numbers 1 “three” 5
而不过上述两个条件中的任意一个不满足,就会使用linkedlist编码保存数据
哈希对象
哈希对象的编码有两种:ziplist 或者 hashtable(ht)
HSET profile name “Tom”
HSET profile age 25
HSET profile career “Programmer”
哈希对象保存的所有键值对的长度都小于64,且键值对数量小于512个,使用ziplist编码保存数据
ziplist编码格式
不能满足上述任一条件,都是使用hashtable来保存数据。
hashtable(ht)字典编码
集合对象
集合对象的编码有两种:intset 和 hashtable
当集合对象保存的所有元素都是整数,且保存的元素数量不超过512个使用intset编码
如果上述条件有一个不满足就使用hashtable编码
有序集合对象
有序集合编码有两种: ziplist 和 sliplist
有序集合对象保存的元素小于128个且所有保存的元素长度小于64字节,有序集合对象底层编码是ziplist;
压缩列表的集合元素按分值从小到大进行排序,表头分值小,表尾分值大。
如果不满足上述两个条件的任意一个,就只能使用skiplist编码。
skiplist编码使用zset结构作为底层,一个zset维护一个字典和一个跳表。跳表按分值从小到大保存,可用于实现ZRANK、ZRANGE等命令。字典维护了一个成员到分值的映射。可以用o(1)复杂度查找给定成员的分值。
同时使用跳表和字典是因为:跳表适用于范围查询,但查找成员分值这个操作的复杂度为o(logN),而字典可以降低到o(1)。
内存回收:计数
为一个对象都维护一个refcount(引用计数),在创建一个新对象时,引用计数的值会被初始化为1,而对象被一个新程序使用时,它的应用计数值会增1。当新程序不使用时,会减1。当引用计数值降为0时,所占用的内存就会被释放。