Redis字符串(key-string)的底层实现:Redis虽然是用C语言写的,但却没有直接使用C语言的字符串,而是自己实现了一套字符串。目的就是为了提升速度,提升性能。Redis构建了一个叫做简单动态字符串(simple dynamic string),简称SDS。结构可如下图表示:
struct sdshdr{
int len; //记录已使用长度
int free; //记录未使用长度
char[] buf; //字符数组 个人理解为指向一个字符数组的指针char* buf
};
Redis的字符串也会遵守C语言的字符串的实现规则,即最后一个字符为空字符。然而这个空字符不会被计算在len里头。SDS的最厉害最奇妙之处在于它的Dynamic。动态变化长度。在进行扩容时会进行如下步骤:
- 计算出所需的大小是否足够
- 开辟空间至满足所需大小
- 开辟已使用大小len相同长度的空闲free空间(如果len < 1m),开启1m长度的空闲free空间(如果len>=1m)。
Redis 字符串的性能优势:
- 快速获取字符串长度
- 避免缓冲区溢出
- 降低空间分配次数提升内存使用效率
- 空间预分配:对于追加操作来说,Redis不仅会开辟空间至够用而且还会预分配未使用的空间(free)来用于下一次操作。至于未使用的空间(free)的大小则由修改后的字符串长度决定。当修改后的字符串长度len < 1M,则会分配与len相同长度的未使用的空间(free) 当修改后的字符串长度len >= 1M,则会分配1M长度的未使用的空间(free)有了这个预分配策略之后会减少内存分配次数,因为分配之前会检查已有的free空间是否够,如果够则不开辟了。
- 惰性空间回收:与上面情况相反,惰性空间回收适用于字符串缩减操作。比如有个字符串s1=“hello world”,对s1进行sdstrim(s1," world")操作,执行完该操作之后Redis不会立即回收减少的部分,而是会分配给下一个需要内存的程序。当然,Redis也提供了回收内存的api,可以自己手动调用来回收缩减部分的内存。
hash是一个键值对(key =>value)集合。
- Redis hash是一个string(key)类型的field和val(value)值的映射表 ,hash结构特别适合用于存储对象。Redis hash中的value的内部实现为一个hashmap,并提供了直接存取这个map成员的接口。在hashmap中的key我们统一为field,值统一为val。在对数据的修改和存取时都可以直接通过其内部map的field, 也就是通过 key(存储的数据集的key) + field(属性标签) 就可以操作对应属性数据了,既不需要重复存储数据,也不会带来序列化和并发修改控制的问题,很好的解决了问题。
//哈希表结构定义
typedef struct dictht{
dictEntry **table;//哈希表数组
unsigned long size;//哈希表大小
unsigned long sizemask;//哈希表大小掩码,用于计算索引值,总是等于 size-1
unsigned long used;//该哈希表已有节点的数量
}dictht
//哈希表是由数组 table 组成,table 中每个元素都是指向 dict.h/dictEntry 结构,dictEntry 结构定义如下:
typedef struct dictEntry{
void *key; //键
union{ //值
void *val;
uint64_tu64;
int64_ts64;
}v;
struct dictEntry *next;//指向下一个哈希表节点,形成链表
}dictEntry
//key用来保存键,val用来保存值,这个值可以是一个指针也可以是uint64_t整数,也可以是int64_t整数
- 总结Redis hash的结构是一个数组,数组元素指向一个链表,链表每一个节点是一个hash表,结构如下图:
- Redis hash对应value内部实际就是一个hashmap(也可称之为字典),实际这里会有2种不同实现,这个hash的成员比较少时Redis为了节省内存会采用类似一维数组的方式来紧凑存储(ziplist),而不会采用真正的hashmap结构,对应的value redisObject的encoding为zipmap,当成员数量增大时会自动转成真正的hashmap,此时encoding为ht。即在数据量比较少的时候Redis hash的内部实现为一个hash数组,当数据量较大的时候才采用hashmap的方式进行实现。
- 压缩列表(ziplist)是Redis为了节省内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型数据结构,一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值。
- 注意这里还有一个指向下一个哈希表节点的指针,我们知道哈希表最大的问题是存在哈希冲突,如何解决哈希冲突,有开放地址法和链地址法。Redis hash底层采用的是链地址法,通过next这个指针可以将多个哈希值相同的键值对连接在一起,用来解决哈希冲突
- 扩容和收缩:当哈希表保存的键值对太多或者太少时,就要通过 rerehash(重新散列)来对哈希表进行相应的扩展或者收缩。具体步骤:
- 如果执行扩容操作,会基于原hash表已使用的空间扩大一倍创建另一个hash表;同理,如果是收缩则会根据已使用的空间缩小一倍创建一个hash表。
- 利用hash算法重新计算索引值,然后将键值对重新放到对应的位置上。
- 所有键值对都同步完之后,释放原hash表的内存空间。
- 触发扩容的条件:
- 服务器目前没有执行 BGSAVE 命令或者 BGREWRITEAOF 命令,并且负载因子大于等于1。
- 服务器目前正在执行 BGSAVE 命令或者 BGREWRITEAOF 命令,并且负载因子大于等于5。
- ps:负载因子 = 哈希表已保存节点数量 / 哈希表大小。
- 渐近式 rehash:什么叫渐进式 rehash?也就是说扩容和收缩操作不是一次性、集中式完成的,而是分多次、渐进式完成的。如果保存在Redis中的键值对只有几个几十个,那么 rehash 操作可以瞬间完成,但是如果键值对有几百万,几千万甚至几亿,那么要一次性的进行 rehash,势必会造成Redis一段时间内不能进行别的操作。所以Redis采用渐进式 rehash,这样在进行渐进式rehash期间,字典的删除查找更新等操作可能会在两个哈希表上进行,第一个哈希表没有找到,就会去第二个哈希表上进行查找。但是进行 增加操作,一定是在新的哈希表上进行的。
Redis set是string类型的无序集合。底层也是采用hashtable(与上例中的hashmap结构类似)来进行实现的。集合中的数据都是无序的,所以进行插入、删除、查询操作的时间复杂度都是O(1)。
- Redis set对外提供的功能与list类似,是一个列表功能,特殊之处在于set是可以进行自动排重的,当你需要存储一个列表数据,又不希望出现重复时,set是一个很好的选择,并且set还提供了一个检验成员是否存在于集合内的接口。
list是简单的字符串列表,按插入的顺序进行排序。Redis list的数据结构如下:
//节点结构
typedef struct listNode{
struct listNode *prev;//前置节点
struct listNode *next;//后置节点
void *value; //节点的值
}listNode
//redis list结构
typedef struct list{
listNode *head;//表头节点
listNode *tail;//表尾节点
unsigned long len;//链表所包含的节点数量
void *(*dup)(void *ptr);//节点值复制函数
void (*free) (void *ptr);//节点值释放函数
int (*match) (void *ptr,void *key);//节点值对比函数
}list;
从结构来看,Redis list的底层实现是通过一个双向链表(或一个ziplist),如图:
Redis list的特性:
- 双端:链表具有指向前置节点和后置节点的指针,获取这两个节点的时间复杂度都为O(1)
- 无环:表头节点的前驱指针和尾节点的后续指针都指向null,遍历链表是也是以null作为结束标记
- 带链表长度计数器:可以再O(1)的时间复杂度通过len属性获取链表的长度
- 支持多种数据类型存储:链表节点使用 void* 指针来保存节点值,可以保存各种不同类型的值
Redis zset和Redis set一样也是一个string类型元素的集合,且不允许重复的成员,但zset可以通过用户提供的一个优先级参数来为集合内的成员进行排序。zset的内部实现为一个跳跃表(skiplist)和map(上例中的hashmap),跳跃表结构如下图:
- 由图可以看出skiplist是一种有序的数据结构,他通过每个节点中维持多个指向下一个节点的指针从而达到快速访问节点的目的具有如下性质:
- 由多层结构组成,每一层都是一个有序链表,排列顺序由高层到低层,每层至少包含两个链表节点,分别是前面的head与后面的nil节点。
- 最底层的链表包含了所有元素。
- 如果一个节点出现在该层,那么该层之下的所有层都包含有该节点
- Redis zset的结构定义如下:
//跳跃表
typedef struct zskiplistNode {
struct zskiplistLevel{ //层
struct zskiplistNode *forward;//前进指针
unsigned int span; //跨度
}level[];
struct zskiplistNode *backward; //后退指针
double score; //分值
robj *obj; //成员对象
} zskiplistNode;
//跳跃表节点
typedef struct zskiplist{
structz skiplistNode *header, *tail;//表头节点和表尾节点
unsigned long length; //表中节点的数量
int level; //表中层数最大的节点的层数
}zskiplist;
- 可以抽象为如下结构:
- 搜索:从最高层的链表节点开始,如果比当前节点要大和比当前层的下一个节点要小,那么则往下找,也就是和当前层的下一层的节点的下一个节点进行比较,以此类推,一直找到最底层的最后一个节点,如果找到则返回,反之则返回空。
- 插入:首先确定插入的层数,有一种方法是假设抛一枚硬币,如果是正面就累加,直到遇见反面为止,最后记录正面的次数作为插入的层数。当确定插入的层数k后,则需要将新元素插入到从底层到k层。
- 删除:在各个层中找到包含指定值的节点,然后将节点从链表中删除即可,如果删除以后只剩下头尾两个节点,则删除这一层。