redis 有5种数据类型。
- 1.字符串 String
- 2.列表 list
- 3.集合 set
- 4.有序集合 zset
- 5.哈希字典 hash
那么他们是如何实现的呢?那就要先对其底层的数据结构进行下手了。
一 动态字符串
字符串的底层是一个名叫做简单动态字符串的东西,其实是相当于对C中对一个字符串数组做了封装。
struct sds {
char * data;
unsigned len;
unsigned free;
};
这种设计对好处是,
- 可以O(1)获取字符串长度。
- 可以使用C中的字符串处理函数,并且由于有了free字段,可以进行越界检查,避免写越界。
- 可以减少内存的分配,进行重复利用。这一点体现在预先分配较大内存,和len实现的。
- 二进制安全,因为即使存在着空字符比如\0也无妨,因为它是按照len字段来获取内容的。而不是C中的函数,按照\0来结尾。
二 链表
在C++中有stl的链表可以使用,但是C中是没有的。所以redis自己实现了链表,具体的说是双向链表。
简单的示例:
struct ListNode{
void * pdata;
struct ListNode * preNode;
struct ListNode * nextNode;
};
struct List{
struct ListNode * head;
struct ListNode * tail;
unsigned len;
// 节点值复制函数
void *(*dup)(void *ptr);
// 节点值释放函数
void (*free)(void *ptr);
// 节点值对比函数
int (*match)(void *ptr, void *key);
};
上面就是一个链表结构,这样的实现,有一些好处:
- 双端链表,获取前后节点的复杂度O(1),长度也是O(1)
- 多态,可以兼容任何类型。实现不同的函数就行。
三 哈希字典
redis的字典是使用hash表来实现的。采用链表的方式解决hash冲突。MurmurHash2算法计算hash值。相对上面的数据结构这个稍微有点复杂。
hash的节点结构如下:
struct hashNode{
struct hashNode * next;
void * key;
union data{
void * data;
uint64 u64;
int64 s64;
} v;
};
上面是一个hash的节点,下面是一个hash表的结构
struct hashList
{
hashNode **table;
int used;//已经使用的节点
int size;//哈希表的大小
int marksize;//用来计算hash值的。通过位与的结果选择hash节点。
};
那么字典的结构就呼之欲出了。
struct dict{
struct hashList ht[2];//使用两个hash表,来避免rehash时的问题。
struct dictType * dicttype;//数据类型,这个是使字典支持多态的原因
int rehashindex;//当不在进行rehash的时候为-1
void * privateData;//私有数据
};
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;
四 跳跃表
跳跃表是一种能和平衡树相媲美的数据结构,但是相对于树,来说,实现起来更加简单一点,涉及的都是链表相关的操作。
五 整数集合
typedef struct intset {
// 编码方式
uint32_t encoding;
// 集合包含的元素数量
uint32_t length;
// 保存元素的数组,这个是一个有序的不重复的整数集合。
int8_t contents[];
} intset;
这个整数集合,是可以进行升级的,而不只是存储int8的整数,因为有encoding字段,所以可以存储不同类型的整数。
整数集合的升级策略。
1.当新添加的数据的数据类型的长度比现有集合的长度要大的话,才进行升级。
2.升级时,先按照新数据类型的长度重新分配内存,并将所有现存的数据转换成新数据类型大小。
3.然后将新数据插入到数组中,保持数据有序。
六 压缩列表
redis 为了节省内存还有一种数据结构叫做压缩列表,类似结构如下:
struct zipList{
uint32_t bytes;//总字节数,内存大小
uint32_t tail;//末尾元素的到起始位置的偏移量。
uint32_t size;//元素的个数
void * data;//元素中的数据
int8_t zend;//用来标示,压缩列表的结尾。是一个特殊值。
};
元素的结构:
struct {
previous_entry_length ;//记录了上一个节点的长度
encoding;//记录当前节点内容的格式。
content;//存储内容。
};
redis 的对象,个人感觉是一种通用的类型。是在上面的数据结构的更抽象的一层。
结构类似:
typedef struct redisObject {
// 类型
unsigned type:4;
// 编码
unsigned encoding:4;
// 指向底层实现数据结构的指针
void *ptr;
// ...
} robj;
类型常量 对象的名称
REDIS_STRING 字符串对象
REDIS_LIST 列表对象
REDIS_HASH 哈希对象
REDIS_SET 集合对象
REDIS_ZSET 有序集合对象
编码常量 编码所对应的底层数据结构
REDIS_ENCODING_INT long 类型的整数
REDIS_ENCODING_EMBSTR embstr 编码的简单动态字符串
REDIS_ENCODING_RAW 简单动态字符串
REDIS_ENCODING_HT 字典
REDIS_ENCODING_LINKEDLIST 双端链表
REDIS_ENCODING_ZIPLIST 压缩列表
REDIS_ENCODING_INTSET 整数集合
REDIS_ENCODING_SKIPLIST 跳跃表和字典
可以说是,每个对象都至少两个数据类型可以实现
类型 编码 对象
REDIS_STRING REDIS_ENCODING_INT 使用整数值实现的字符串对象。
REDIS_STRING REDIS_ENCODING_EMBSTR 使用 embstr 编码的简单动态字符串实现的字符串对象。
REDIS_STRING REDIS_ENCODING_RAW 使用简单动态字符串实现的字符串对象。
REDIS_LIST REDIS_ENCODING_ZIPLIST 使用压缩列表实现的列表对象。
REDIS_LIST REDIS_ENCODING_LINKEDLIST 使用双端链表实现的列表对象。
REDIS_HASH REDIS_ENCODING_ZIPLIST 使用压缩列表实现的哈希对象。
REDIS_HASH REDIS_ENCODING_HT 使用字典实现的哈希对象。
REDIS_SET REDIS_ENCODING_INTSET 使用整数集合实现的集合对象。
REDIS_SET REDIS_ENCODING_HT 使用字典实现的集合对象。
REDIS_ZSET REDIS_ENCODING_ZIPLIST 使用压缩列表实现的有序集合对象。
REDIS_ZSET REDIS_ENCODING_SKIPLIST 使用跳跃表和字典实现的有序集合对象。
参考,redis设计与实现