在开始这一部分之前,首先要明确几个概念上的区别,否则容易搞混淆:
- 面向用户的数据结构
- 底层数据结构
- 底层存储方式
面向用户数据类型
面向用户的数据类型,即我们常见的字符串、列表、哈希map等等。。。具体包括一下几类:
Binary-safe strings
: 二进制安全字符串Lists
: 数组;字符串的集合,按照元素插入先后顺序排序Sets
: 集合;字符串的集合,元素是唯一的、无排序的字符串Sorted sets
: 有序集合;在集合的基础上,每个字符串元素绑定一个浮点数score,元素根据score排序Hashes
: 字典(maps);元素由键值对组成,键值对都是字符串Bit arrays
: 比特数组;本质是字符串,可以通过特定的命令,实现位操作HyperLogLogs
:Streams
:
底层数据结构
底层数据结构和我们常说的“redis五种常用数据结构”不太一样;比如:
- 二进制安全字符串的底层数据结构是简单动态字符串SDS
- 数组的底层数据结构是压缩列表 和普通双向链表(比较新的版本已经改为快速列表)
- 哈希表的底层数据结构是支持渐进式rehash的dict结构
- 集合的底层同样是dict结构,只不过value是NULL
- 有序集合的底层数据结构是dict + 跳表
等等,详细的实现会在后面说到各种底层数据结构时提到
底层存储方式
底层存储方式又是另一个维度的概念,比如:
- 简单动态字符串SDS: 有
embstr
和raw
两种底层存储方式
目前感觉概念是比较复杂的,而且维度比较混乱,以上内容可能有误,也许后面理解更深刻后会回来修改
这一节的学习就从简单动态字符串SDS开始
简单动态字符串
数据结构
简单动态字符串的底层数据结构
struct SDS<T> {
T capacity; // 此处用T,不用int;是因为redis对不同长度的字符串使用不同的数据类型;如byte或者short
T len;
byte flags;
byte[] content;
}
capaticy
: 表示字符串容量len
: 表示字符串实际长度flags
: 固定一个字节,表示SDS header
类型;SDS
可以有五种不同header类型,以存储不同长度的字符串(redis对内存做了极致的使用)#define SDS_TYPE_5 0
#define SDS_TYPE_8 1
#define SDS_TYPE_16 2
#define SDS_TYPE_32 3
#define SDS_TYPE_64 4
content
: 表示字符串存储的字符数组
SDS的一般特性
说起SDS
的特性,一般会与C语言的字符串做对比。(因为其他高级语言可能也在某些方面做了优化,不太方面直接比较)
- O(1)时间复杂度获取字符串长度
SDS
的数据结构中len
字段记录SDS的长度,故获取SDS
长度的时间复杂度为O(1)
C语言
中获取字符串长度,需要遍历整个字符串,时间复杂度O(n)
- 避免字符串缓冲区溢出
SDS
使用预分配策略,在长度要超过预分配容量时自动扩容
- 减少内存分配次数
SDS
是可变长度的,由于capacity
的存在,可以为字符串预分配内存空间供SDS
增加长度,不用每次增加长度都分配内存
- 二进制安全
SDS
使用长度len
来定位字符串结束字符,在遍历字符串过程中,不会对数据进行假设和过滤,数据读取和数据写入完全一致C语言
中默认以\0
结尾,如果二进制数据中本来就有\0
字符,那么字符串将被截断
SDS存储方式
SDS的存储方式有两种,embstr
和raw
:
- 当字符串长度小于44字节时,使用
embstr
存储 - 当字符串长度超过44字节时,使用
raw
存储
127.0.0.1:6379> set testKeyShort abcdefghijklmnopqrstuvwxyz
OK
127.0.0.1:6379> debug object testKeyShort // Short长度为26,使用embstr存储
Value at:0x7fb39b231500 refcount:1 encoding:embstr serializedlength:27 lru:13230045 lru_seconds_idle:12
127.0.0.1:6379>
127.0.0.1:6379> set testKeyLong abcdefghijklmnopqrstuvwxyz0123456789012345678
OK
127.0.0.1:6379> debug object testKeyLong // Long长度为45,使用raw存储
Value at:0x7fb39b017d30 refcount:1 encoding:raw serializedlength:46 lru:13230051 lru_seconds_idle:9
Redis头结构
所有的redis对象都有下面这样的头结构:
type RedisObject {
int4 type; // 4bits, 对象类型;redis目前可以表示16种数据类型,如SDS、ziplist等
int4 encoding; // 4bits, 存储形式;同一个类型的不同的存储形式;如SDS可以有embstr、raw两种存储形式
int24 lru; // 3bytes, 每条数据的LRU信息
int32 refcount; // 4bytes, 4个字节记录对象的引用计数,用于内存回收
void *ptr; // 8bytes(64位操作系统),指向对象内容的指针
}
所以,每个对象的头结构一般需要0.5 + 0.5 + 3 + 4 + 8 = 16
个bytes的存储空间
SDS结构体大小
在字符串比较小时,SDS
数据结构中 T = int8
type SDS {
int8 capacity;
int8 len;
int8 flags;
bytes[] content;
}
所以,一个SDS结构体的大小为capaticy + 3
,其中capacity
为字符串的容量,3则表示capaticy/len/flags
各占一个字节空间
embstr VS raw
现在我们知道,一个SDS对象 = RedisObjct
+ SDS
,即对象头 + SDS数据结构本身
embstr
的存储形式是将**RedisObject
和SDS
**连续存储,其内存地址连续,使用malloc方法一次分配内存raw
的存储形式是将Redisobject
和SDS
分开存储,其内存地址不连续,需要使用两次malloc分配内存
一般内存分配器jemalloc
、tcmalloc
分配内存的大小单位都是2/4/8/16/32/64
,当一个SDS分配内存小于64个字节时,使用embstr
存储,否则使用raw
存储
对于一个完整的SDS
,其已有固定长度为16 + 3 = 19
,即一个16字节的对象头和3个字节的SDS固定字段,Redis
字符串还需要一个字节已NULL
结尾,所以,当一个内存分配单位为64bytes时,字符串的最大容量
capacity = 64 - 16 - 3 - 1 = 44