目录
一、字符串类型
1.1 内部编码
1.2 SDS(简单动态字符串)
1.3 空间预分配机制
1.4 RedisObject
1.5 embstr & raw
二、哈希类型
2.1 内部编码
2.2 ziplist(压缩列表)
2.3 hashtable(哈希表)
三、列表类型
3.1 内部编码
3.2 quicklist(快速列表)
四、集合类型
五、有序集合类型
5.1 内部编码
5.2 skiplist(跳跃表)
Redis 是一种基于键值对(key-value)的 NoSQL 的数据库。
Redis 有丰富的数据类型,不仅便于多应用场景开发,同时也能提高开发效率。
Redis 主要提供了 5 种基本数据结构:字符串、哈希、列表、集合、有序集合。
同时在字符串的基础上演变出了位图(Bitmaps)和 HyperLogLog,以及 GEO(地理信息定位)。
每种数据结构都有对应的适用场景,合理的选择数据结构以及优化 Redis 相关配置以达到更优的性能。
一、字符串类型
Redis 有着丰富的数据结构,首当其冲的就是字符串类型,Redis 的最基本的数据结构。
首先键是字符串类型,其他数据类型都是在字符串类型上构建的。
字符串类型的值可以是字符串、数字或二进制(图片、音频、视频),但值不能超过 512MB。
1.1 内部编码
字符串类型的内部编码有三种:
- int:8 字节的长整型
- embstr:长度小于等于 44 字节的字符串
- raw:长度大于 44 字节的字符串
字符串类型的底层实现是简单动态字符串。
1.2 SDS(简单动态字符串)
Redis 是用 C 语言实现的,C 语言的字符串标准形式是以 NULL 作为结束符(占用 1 字节的内存空间),并且获取字符串长度是以的是 strlen 标准库函数,函数的时间复杂度是 O(n),需要对整个字符串遍历扫描,而单线程的 Redis 来说是难以承受的。
于是,Redis 在此基础上实现了自己的字符串:简单动态字符串(SDS),带有长度的字节数组。
Redis 在 3.2 版本对 SDS 做了内存优化,极大地提高了内存空间的使用。
3.2 版本之前 SDS 的结构:
struct sdshdr {
unsigned int len; //数组长度 4bytes
unsigned int free;//数组容量 4bytes
char buf[];
};
无论字符串大小多少,即使是空字符串,也会有 8 个字节来存储字符串信息。
3.2 版本之后 SDS 结构由 sdshdr1 种增至 sdshdr5、sdshdr8、sdshdr16、sdshdr32、sdshdr64 一共 5 种类型
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags; /* 低3位存储类型, 高5位预留 */
char buf[];/*柔性数组,存放实际内容*/
};
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* 已使用长度,用1字节存储 */
uint8_t alloc; /* 总长度,用1字节存储 */
unsigned char flags; /* 低3位存储类型, 高5位预留 */
char buf[];/*柔性数组,存放实际内容*/
};
struct __attribute__ ((__packed__)) sdshdr16 {
uint16_t len; /* 已使用长度,用2字节存储 */
uint16_t alloc; /* 总长度,用2字节存储 */
unsigned char flags; /* 低3位存储类型, 高5位预留 */
char buf[];/*柔性数组,存放实际内容*/
};
struct __attribute__ ((__packed__)) sdshdr32 {
uint32_t len; /* 已使用长度,用4字节存储 */
uint32_t alloc; /* 总长度,用4字节存储 */
unsigned char flags; /* 低3位存储类型, 高5位预留 */
char buf[];/*柔性数组,存放实际内容*/
};
struct __attribute__ ((__packed__)) sdshdr64 {
uint64_t len; /* 已使用长度,用8字节存储 */
uint64_t alloc; /* 总长度,用8字节存储 */
unsigned char flags; /* 低3位存储类型, 高5位预留 */
char buf[];/*柔性数组,存放实际内容*/
};
五种类型的 SDS 至少要用 3 位来存储类型,一个字节 8 位,剩余 5 位用来存储长度,最多满足长度小于 32 的字符串的存储。即 sdshdr5。
其他类型的使用,Redis 根据字符串实际长度动态选择合适的类型存储。
需要注意的是:
当 Redis 创建空字符串时,sdshdr5 会被 sdshdr8 代替,避免创建空字符串后,其内容可能会频繁更新而引发扩容。
1.3 空间预分配机制
- 若 sds 中剩余空闲长度 avail 大于新增内容的长度 addlen,直接在柔性数组 buf 末尾追加即可,无须扩容。
- 若 sds 中剩余空闲长度 avail 小于或等于新增内容的长度 addlen,则分情况讨论:新增后总长度 len+addlen<1MB 的,按新长度的 2 倍扩容;新增后总长度 len+addlen>1MB 的,按新长度加上 1MB 扩容。
- 最后根据新长度重新选取存储类型,并分配空间。此处若无须更改类型,通过 realloc 扩大柔性数组即可;否则需要重新开辟内存,并将原字符串的 buf 内容移动到新位置。
提示: 也因 Redis 字符串的预分配机制,尽量减少字符串频繁修改操作如 append、setrange,改为直接使用 set 修改字符串,降低预分配带来的内存浪费和内存碎片化
1.4 RedisObject
所有的 Redis 对象都有以下头结构:
struct redisObject {
unsigned type:4; //类型
unsigned encoding:4; //存储形式
unsigned lru:LRU_BITS; //LRU信息
int refcount; //引用计数
void *ptr; //对象内容存储位置
};
- 不同类型的对象的类型 type 不同(4bits)
- 同一类型 type 也会有不同的存储形式 encoding(4bits)
- 为了记录对象 LRU 信息,使用了 24bits
- 每个对象都有个引用计数(4Bytes),当引用计数为零时,对象就会被销毁,内存被回收。
- ptr 指针将指向对象内容(body)的具体存储位置(8bytes)
这样算下来,一个 RedisObject 对象头结构需要占据 16 字节的存储空间。
1.5 embstr & raw
字符串有两种存储方式:embstr 和 raw。
两者的区别:
在长度特别短时,使用 embstr 形式存储,当超过最小长度 44 字节时,使用 raw 存储
1.4 RedisObject 中我们知道对象头占据 16 字节的存储空间,字符串比较小时,SDS 使用 sdshdr8,SDS 头至少占据 3 个字节,就意味着分别一个字符串分配的最小空间为 19 个字节。
C 语言字符串是以 NULL 结尾的,占据 1 字节。
而内存分配器 jemalloc/tcmalloc 等分配内存大小的单位都是 2、 4、 8、 16、 32、 64 等等,为了能容纳一个完整的 embstr 对象, jemalloc 最少会分配 32 字节的空间,如果字符串再稍微长一点,那就是 64 字节的空间。如果总体超出了 64 字节, Redis 认为它是一个 大字符串,不再使用 emdstr 形式存储,而使用 raw 形式。
留给字符串内容的大小最多:64-19-1=44 个字节
二、哈希类型
几乎所有类型的语言都提供了哈希类型,比如 java 的 hashmap。
在 Redis 中,哈希类型是指键值本身又是一个键值对结构, 形如 value={{field1, value1}, ...{fieldN, valueN}} 。
2.1 内部编码
- ziplist(压缩列表):当哈希类型元素个数小于 hash-max-ziplist-entries 配置(默认 512 个)、同时所有值都小于 hash-max-ziplist-value 配置(默认 64 字节)时,Redis 会使用 ziplist 作为哈希的内部实现,ziplist 使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面比 hashtable 更加优秀。
- hashtable(哈希表):当哈希类型无法满足 ziplist 的条件时, Redis 会使用 hashtable 作为哈希的内部实现, 因为此时 ziplist 的读写效率会下降,而 hashtable 的读写时间复杂度为 O(1) 。
2.2 ziplist(压缩列表)
压缩表类似于数组,不同的是压缩列表在表头有三个字段 zlbytes、zltail 和 zllen,分别表示列表长度、列表尾的偏移量和列表中的 entry 个数;压缩列表在表尾还有一个 zlend,表示列表结束。
在压缩列表中,如果我们要查找定位第一个元素和最后一个元素,可以通过表头三个字段的长度直接定位,复杂度是 O(1)。而查找其他元素时,就没有这么高效了,只能逐个查找,此时的复杂度就是 O(N) 了。
2.3 hashtable(哈希表)
Redis 中除了哈希结构的数据会用到哈希表之外,同时为了实现从键到值的快速访问,Redis 使用了一个哈希表来保存所有键值对。
因为这个哈希表保存了所有的键值对,所以可以称之为全局哈希表。哈希表的最大好处是可以通过 O(1)的时间复杂度来查找键值对。
Redis 的哈希表的内部实现结构是数组 + 链表二维结构 。
当写入大量数据时,不可避免的会出现哈希冲突,Redis 解决哈希冲突采用方式是链式哈希,即同一个哈希桶中的多个元素用一个链表来保存,它们之间依次用指针连接。
写入的数据越来越多,链表也越来越长,元素查询的效率也会变低(链表的特点:增删快,查询慢)。不可避免的需要进行 rehash 操作,增加数组长度,分散数据的保存,避免链表数据过长。
Redis 为了rehash操作更高效,Redis 默认使用了两个(全局)哈希表,通常只有一个哈希表有值。
当数据量越来越多,Redis 进行 rehash 操作,分为三步:
- 给表 2 分配空间,比如当前表大小的 2 倍
- 数据迁移到表 2
- 释放当前表的内存空间。
在 java 中步骤二的数据迁移过程是一次性完成的,当数据量比较大时,这个过程的耗时会较长,会阻塞线程,对单线程的 Redis 来说是承受不起的,为了避免这个问题 Redis 提出了渐进式 rehash。
简单的说,就是在执行第二步数据迁移时,Redis 仍然正常处理请求,每处理一个请求时,从哈希表 1 的第一个索引位置开始,将这个索引位置下面的所有数据迁移到哈希表 2 中;当处理下一个请求时,再将下一个索引位置的数据进行迁移。
当服务空闲时,redis 也维护有一个定时任务,会周期性(例如 100ms)的进行 rehash 操作,缩短整个 rehash 的过程。
这样就巧妙地把一次性大量拷贝的开销,分摊到了多次处理请求的过程中,避免了耗时操作,保证了数据的快速访问。
三、列表类型
Redis 的列表类型相当于 java 中的 linkList,特点不言而喻,增删快,时间复杂度为 O(1),查找慢,时间复杂度为 O(n)。
当列表弹出了最后一个元素之后,该数据结构自动被删除,内存被回收。
常用来做异步队列使用。 将需要延后处理的任务结构体序列化成字符串塞进 Redis 的列表,另一个线程从这个列表中轮询数据进行处理。
特点:
- 有序
- 可重复
3.1 内部编码
Redis3.2 版之前:
- ziplist(压缩列表) : 当列表的元素个数小于 list-max-ziplist-entries 配置(默认 512 个) , 同时列表中每个元素的值都小于 list-max-ziplist-value 配置时(默认 64 字节) , Redis 会选用 ziplist 来作为列表的内部实现来减少内存的使用。
- linkedlist(链表) : 当列表类型无法满足 ziplist 的条件时, Redis 会使用 linkedlist 作为列表的内部实现。
Redis 的 List 底层使用压缩列表本质上是将所有元素紧挨着存储,所以分配的是一块连续的内存空间,虽然数据结构本身没有时间复杂度的优势,但是这样节省空间而且也能避免一些内存碎片;
Redis3.2 版之后:
- quicklist(快速列表)
3.2 quicklist(快速列表)
考虑到链表的附加空间相对太高, prev 和 next 指针就要占去 16 个字节 (64bit 系统的指针是 8 个字节),另外每个节点的内存都是单独分配,会加剧内存的碎片化,影响内存管理效率。后续版本对列表数据结构进行了改造,使用 quicklist 代替了 ziplist 和 linkedlist。
//列表节点
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;
quicklist 是一个双向链表,链表中的每个节点是一个 ziplist 结构。quicklist 可以看成是用双向链表将若干小型的 ziplist 连接到一起组成的一种数据结构。当 ziplist 节点个数过多,quicklist 退化为双向链表,一个极端的情况就是每个 ziplist 节点只包含一个 entry,即只有一个元素。当 ziplist 元素个数过少时,quicklist 可退化为 ziplist,一种极端的情况就是 quicklist 中只有一个 ziplist 节点。
quicklist 内部默认单个 ziplist 长度为 8k 字节,超出了这个字节数,就会新起一个 ziplist。 ziplist 的长度由配置参数 list-max-ziplist-size 决定。
四、集合类型
集合(set)类型也是用来保存多个的字符串元素,但和列表类型不一样的是,集合中不允许有重复元素,并且集合中的元素是无序的,不能通过索引下标获取元素。
一个集合最多可以存储 个元素。Redis 除了支持集合内的增删改查,同时还支持多个集合取交集、并集、差集,合理地使用好集合类型,能在实际开发中解决很多实际问题。
内部编码
- intset(整数集合) : 当集合中的元素都是整数且元素个数小于 set-maxintset-entries 配置(默认 512 个) 时, Redis 会选用 intset 来作为集合的内部实现, 从而减少内存的使用。
- hashtable(哈希表) : 当集合类型无法满足 intset 的条件时, Redis 会使用 hashtable 作为集合的内部实现。
五、有序集合类型
有序集合保留了集合不能有重复成员的特性,但不同的是, 有序集合中的元素可以排序。
但是它和列表使用索引下标作为排序依据不同的是, 它给每个元素设置一个分数(score) 作为排序的依据。
有序集合中的元素不能重复, 但是 score 可以重复, 就和一个班里的同学学号不能重复, 但是考试成绩可以相同。
5.1 内部编码
- ziplist(压缩列表) :当有序集合的元素个数小于 zset-max-ziplistentries 配置(默认 128 个) ,同时每个元素的值都小于 zset-max-ziplist-value 配置(默认 64 字节) 时,Redis 会用 ziplist 来作为有序集合的内部实现,ziplist 可以有效减少内存的使用。
- skiplist(跳跃表) :当 ziplist 条件不满足时,有序集合会使用 skiplist 作为内部实现,因为此时 ziplist 的读写效率会下降。
5.2 skiplist(跳跃表)
跳表在链表的基础上,增加了多级索引,通过索引位置的几个跳转,实现数据的快速定位。