1.SDS
简介:Redis没有采用C语言的以空字符串结尾的字符数组,而是构建一种简单动态字符串(Simple dynamic string,SDS),并将它作为string的表示。
struct sdshdr {
// buf 中已占用空间的长度
int len;
// buf 中剩余可用空间的长度
int free;
// 数据空间
char buf[]; // 依然以’\0’结尾
};
SDS与C语言的字符串的区别
- 常数复杂度获取字符串长度。
- 通过free杜绝缓存区溢出
- 空间预分配(如果len小于1MB,那么分配的free将等于len,如果len大于1MB那么分配free1MB)和惰性空间释放(缩短SDS时不立即回收内存而是记录到free中)
二进制安全,所以能保存图片,视频,音频文件
常用操作:- get:sdsrange—O(n)
- set:sdscpy—O(n)
- create:sdsnew—O(1)
- len:sdslen—O(1)
2.链表
typedef struct listNode {
// 前置节点
struct listNode *prev;
// 后置节点
struct listNode *next;
// 节点的值
void *value;
} listNode;
typedef struct list {
// 表头节点
listNode *head;
// 表尾节点
listNode *tail;
// 节点值复制函数
void *(*dup)(void *ptr);
// 节点值释放函数
void (*free)(void *ptr);
// 节点值对比函数
int (*match)(void *ptr, void *key);
// 链表所包含的节点数量
unsigned long len;
} list;
常用操作:
- rpush: listAddNodeHead —O(1)
- lpush: listAddNodeTail —O(1)
- push:listInsertNode —O(1)
- index : listIndex —O(N)
- pop:ListFirst/listLast —O(1)
- llen:listLength —O(N)
3.字典
字典在底层是由哈希表实现的,所以我们从哈希表开始入手。
(1)哈希表
typedef struct dictht {
// 哈希表数组
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小掩码,用于计算索引值
// 总是等于 size - 1
unsigned long sizemask;
// 该哈希表已有节点的数量
unsigned long used;
} dictht;
(2)哈希节点
typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
// 指向下个哈希表节点,形成链表
struct dictEntry *next;
(3)字典
typedef struct dict {
// 类型特定函数
dictType *type;
// 私有数据
void *privdata;
// 哈希表
dictht ht[2];
// rehash 索引
// 当 rehash 不在进行时,值为 -1
int rehashidx; /* rehashing not in progress if rehashidx == -1 */
// 目前正在运行的安全迭代器的数量
int iterators; /* number of iterators currently running */
} dict;
(4)字典特定函数
/*
* 字典类型特定函数
*/
typedef struct dictType {
// 计算哈希值的函数
unsigned int (*hashFunction)(const void *key);
// 复制键的函数
void *(*keyDup)(void *privdata, const void *key);
// 复制值的函数
void *(*valDup)(void *privdata, const void *obj);
// 对比键的函数
int (*keyCompare)(void *privdata, const void *key1, const void *key2);
// 销毁键的函数
void (*keyDestructor)(void *privdata, void *key);
// 销毁值的函数
void (*valDestructor)(void *privdata, void *obj);
} dictType;
总结:字典这种数据结构看起来十分复杂,其实十分简单,只要看过Java的HashMap的源代码都应该很容易懂。
注意:
1. 哈希表有一个数据结构,unsigned long sizemask;
有什么用呢,就是让本来的 “哈希节点的key % size” 计算出索引变成 “哈希节点的key & sizemask”,位运算更加高效,当然前提是哈希节点是2的x次幂。
2. rehash也是跟Java相似,为什么dict的ht[2]有两个哈希表呢,原来dict[0]是原来的哈希表,dict[1]是扩容后的哈希表。
常用操作:
- Hset: dictAdd/dictReplace —-O(1)
- Hget: dictFetchValue—O(1)
- Randomkey:dictGetRandomKey—O(1)
- Hdel/del: dictDelete—O(1)
4.整数集合
整数集合是集合键的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis就会使用整数集合作为集合键的底层实现。
typedef struct intset {
// 编码方式
uint32_t encoding;
// 集合包含的元素数量
uint32_t length;
// 保存元素的数组
int8_t contents[];
} intset;
常用操作:
- sadd:intsetAdd—O(1)
- smembers:intsetGetO(1)—O(N)
- srem:intsetRemove—O(N)
- slen:intsetlen —O(1)
5.压缩列表
压缩列表是列表键和hash键的底层实现之一。当一个列表键只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么redis就会使用压缩列表来做列表键的底层实现。
zlbytes代表整个压缩列表占用的内存字节数。
zltail代表压缩列表起始地址到尾节点的偏移量。
zllen节点数量
entry 节点
zlend 0xFF标记压缩列表末端。
常用操作:
- Push/hset:ziplistPush/ziplistInsert 平均O(N), 最坏O(N2)
- Llen/hlen:ziplislen平均O(N), 最坏O(N2)
- Hget:ziplistGet —-O(1)
- Index:ziplistIndex—-O(N)
- Hdel/pop:ZiplistDelete—平均O(N), 最坏O(N2)
- 遍历:ziplistPrev/ziplistNext—-O(1)
6. 对象
字符串对象
字符串对象的编码可以是int,raw,embstr
如果是整数值,则编码是int,如果是小于32字节的字符串,则编码是embstr,如果是大于32字节的字符串,则编码是raw。
下面是他们的对象结构:
embstr和raw的区别是 raw编码调用了两次内存分配函数来分配redisObject结构和sdshdr结构,而embstr只需要调用一次分配一段连续的空间。
列表对象
列表对象的编码可以是ziplist或者linkedlist。
当列表对象可以同时满足一下两个条件时,列表对象使用ziplist编码:
- 列表保存的所有字符串元素的长度都小于64字节;
- 列表对象保存的元素数量小于512个。
不能满足这两个条件的都是用LinkedList编码。
下面就是这个对象的两种编码的结构:
每个StringObject实际上就是一个sdshdr。
哈希对象
哈希对象的编码可以是ziplist或者是hashtable。
当哈希对象同时满足以下两个条件时,哈希对象使用ziplist对象:
- 哈希对象保存的所有键和值的字符串长度都小于64个字节;
- 哈希对象保存的键值对数量小于512个
不能满足这两个条件的哈希对象需要使用hashtable编码。
下面就是这个对象的两种编码的结构:
集合对象
集合对象的编码可以是intset和hashtable
当集合对象同时满足以下条件时,对象使用intset编码:
- 集合对象保存的所有元素都是整数值
- 集合对象保存的元素数量不超过512个
不能满足这两个条件的集合对象需要使用hashtable编码。
下面就是这个对象的两种编码的结构:
有序集合对象
有序集合对象的编码可以是ziplist和skiplist
当集合对象同时满足以下条件时,对象使用intset编码:
- 有序集合保存的元素数量小于128个
- 有序集合保存的所有元素成员的长度都小于64字节
不能满足这两个条件的集合对象需要使用skiplist编码。
下面就是这个对象的两种编码的结构:
ziplist编码的有序集合对象其实跟集合对象的差不多,区别是集合元素按分值从小到大进行排序。
7.对象的引用计数
因为C语言并不具备自动内存回收功能,所以redis对象系统构建了一个引用计数refcount技术来实现内存回收机制,通过这一机制,程序可以通过跟踪对象的引用计数信息,在适当的时候自动释放对象进行内存回收。
对象的引用计数信息会随着对象的使用状态而不断变化:
- 在创建一个新对象时,引用计数的值会被初始化为1;
- 当对象被一个新程序使用时,它的引用计数值会被增1
- 当对象不再被一个程序使用时,它的引用计数会减1
- 当对象的引用计数为0时,对象所占用的内存会被释放。