redis是什么?
REmote DIctionary Server(redis)远程字典服务器,是一个key-value的存储系统。redis是一个开源的、使用ANSI① C语言编写、遵守BSD协议②、支持网络、可基于内存亦可持久化③的日志型、key-value数据库,并提供多种语言的API。
数据结构
redis数据结构,也就是Value的值的类型。包括(String,hash,list,set,zset)。下面让我们了解下每种类型在底层如何存储,以及其应用场景。
String(字符串)
在redis内部,string类型有两种底层存储结构。redis会根据存储的数据以及用户的操作指令自动选择合适的结构
- int:存放整数类型
- SDS:存放浮点、字符串、字节类型
SDS:简单动态字符串 simple dynamic string
typedef struct sdshdr {
// buf中已经占用的字符长度
unsigned int len;
// buf中剩余可用的字符长度
unsigned int free;
// 数据空间
char buf[];
}
根据上述底层数据结构可见,其底层是一个char数组。buf最大容量512M,里面可以放字符串、浮点数和字节。所以你甚至可以放一张序列化后的图片。以及项目中常用的序列化对象等等
思考?为什么它没有直接使用数组?而是对数组进行了包装?
假定底层结构为数组:当value为hello的时候,底层结构如下,但是当存储的值改为hello word时呢?需要重新开辟一块内存空间,效率很低
包装数组的意义
buf的扩容方式分为以下两种情况,修改后的大小小于1M以及修改后的大小大于1M
- 修改后的len小于1M,此时分配给free的大小和len一样,比如说修改过后为10字节,那么给free也是10字节,buf 的实际长度变成了 10 + 10 + 1 = 21b (不要问我为什么加1)
- 修改后的len大于1M,此时分配给的free长度为1M,比如修改过后为10M,那么free是1M,buf的实际长度变成了10M + 1M + 1b
buff空间释放:惰性空间释放,当字符串缩短时,并没有真正的缩容,而是移动free指针。这样将来字符串长度增加时,就不用重新分配内存了。但是这样会导致内存浪费,SDS提供了方法来真正释放内存
sdsfree 函数释放内存
Hash(散列)
hash底层有两种实现:压缩列表和字典(dict)。
压缩列表
压缩列表有点类似于数组,通过一片连续的内存空间来存储数据。不过,它跟数组不同的一点是,它允许存储的数据大小不同。每个节点上增加一个length属性来记录这个节点的长度,这样比较方便地得到下一个节点的位置。
struct ziplist<T> {
int32 zlbytes; //整个压缩列表占用字节数
int32 zltail_offset; //最后一个元素距离压缩列表起始位置的偏移量,用来快速定位到最后一个节点
int16 zllength; //元素的个数
T[] entries; //元素内容列表,依次紧凑存储
int8 zlend; //标志元素列表的结束,值恒为0xFF
}
快速列表
redis3.2版本之后,对列表数据结构进行了改造,使用quicklist代替了ziplist和linkedlist。简单来说就是链表+压缩列表,每个链表的节点都是一个压缩列表。
struct quicklistNode {
quicklistNode* prev;
quicklistNode* next;
ziplist* zl; //指向压缩列表
int32 size; //ziplist的 字节总数
int16 count; //ziplist中的元素个数
int2 encoding; //存储形式
……
}
struct quicklist {
quicklistNode* prev;
quicklistNode* next;
long count; //元素总个数
int nodes; //ziplist 的节点个数
int compressDepth; //LZF 算法压缩深度
}
字典
说是字典,其实就是我们熟悉的hashmap(散列表)。只不过redis的字典值只能是字符串,而且rehash的方式不一样。
rehash
我们都知道,hashmap的几个参数 DEFAULT_INITIAL_CAPACITY(数组容量)、DEFAULT_LOAD_FACTOR(影响因子)两个参数相乘得到一个阈值threshold,当数组容量达到此值时,hashmap会进行扩容,扩容后会进行一次性rehash(这里我就不深入分析了,大家感兴趣可以自己了解下)。
渐进式rehash
与上述不同的是,我们redis中存储的数据量可能较大,采用rehash一次性扩容的方式可能耗时过多(简直对不起我们redis快的称号)。所以redis才用了渐进式rehash,当负载因子达到阈值之后,只申请空间,但不将老的数据迁移到新的散列表中。当有数据插入时,将新数据插入到新散列表中,并且从老的散列表中拿出一个数据放入新的散列表。每次插入数据都重复上述过程,久而久之,老的散列表中的数据就全部搬到新的散列表中了。这样一个一个的迁移,rehash的过程就变得无感了。这个过程就叫做渐进式rehash。
List(列表)
list底层也有两种数据结构:链表linkedlist以及压缩列表ziplist。当list元素个数少切元素内容长度不大时使用ziplist,反之使用linkedList。
链表
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;
压缩列表
压缩列表同hash的压缩列表
Set(集合)
set是没有重复数据的集合。set也有两种数据结构。intset和字典
intset
intset,一种特殊的set数据结构,由多个整形元素组成。intset也是一个有序的整型集合,数据结构如下
typedef struct intset {
//字符编码 三种类型 int16_t(默认) int32_t nt64_t 分别占 2 4 8字节
uint32_t encoding;
//元素大小
uint32_t length;
//元素数组
int8_t contents[];
} intset;
intset是一个有序的集合,所以插入时间复杂度On,查找采用二分查找,速度还是挺快的。感兴趣的可以看下其中的方法。这里我不做过多介绍了
字典
字典同hash的字典
Zset(有序集合)
zset是可排序的set。采用跳表的数据结构。与hash的实现方式类似,如果元素个数不多且不大,就是用压缩列表ziplist来存储。不过由于zset包含了score的排序信息,所以在ziplist的内部,是按照score排序递增来存储的。所以每次插入都要移动之后的数据。
跳表
redis的zset是一个复合结构,一方面他需要一个hash结构来存储value和score的关系,另一方面需要按照score排序,并且能够指定score的来获取列表。这就需要一个跳跃列表结构。
struct zslnode{
string value;
double score;
zslnode*[] forwards; //多层连接指针
zslnode* backward; //回溯指针
}
struct zsl {
zslnode* header; //跳跃列表头指针
int maxLevel; //跳跃列表当前最高层
map<string, zslnode*> ht; //hash结构的所有键值对
}
如果所示,跳表的作用就是为了减少查询次数,就类似与二分查找,只不过这里是分层记录中间值。比如我们要查找8,先在最上层L2查找,发现在1和9之间;然后去L1层查找,发现在5和9之间;然后去L0查找,发现在7和9之间,然后找到8。
总结
redis基本数据类型讲到这里就结束了,我们了解了redis底层真正存储数据的5中结构,分别是string、hash、list、set以及zset,那么redis的常用命令以及使用场景是怎么样的呢?期待下次分享。
关注不迷路