1、基于内存读写
内存的读写速度很快
2、采用的多路复用
Epoll
模型
3、高效率的数据结构
常用的五大Redis的数据结构,及他们各自的底层实现结构
string
hash
list
set
sortset(zset)
string
的底层实现是简单动态字符串(SDS -simple dynamic string)
hash
的底层实现是hash表
或则压缩列表(ziplist)
list
的底层实现是双向列表(quicklist)
或者压缩列表
set
的底层实现是hash表(hashtable)
或者整数数组
sortset(zset)
的底层实现是压缩列表
或者跳表
各个数据结构的底层实现概览
底层K-V结构图
底层K-V图
1、String的底层实现(SDS结构)
value是string
类型的时候分为三种情况
(1)、当设置的值是整数类型的时候,redis底层会将string
类型转化为int
来存储
(2)、设置的值小于等于44个字节的时候,使用的编码是embstr
127.0.0.1:6379> set a test
OK
127.0.0.1:6379> object encoding a
"embstr"
(3)、设置的值大于44个字节的时候,使用的编码是raw
127.0.0.1:6379> set a 012345678901234567890123456789012345678901234
OK
127.0.0.1:6379> object encoding a
"raw"
redis是用C语言编写的,在C语言中string
类型是用字符数组char[]
来实现的。redis实现字符串的底层并没有直接使用C语言中的字符数组的形式,而是进行了改造,构造出了一种SDS的数据结构
redis3.2以前的版本SDS数据结构
struct sdshdr{
int free; // buf[]数组未使用字节的数量
int len; // buf[]数组所保存的字符串的长度
char buf[]; // 保存字符串的数组
}
redis3.2及以后的版本SDS数据结构
(unit8_t 长度为1字节)
(unit16_t 长度为2字节)
(unit32_t 长度为3字节)
(unit64_t 长度为4字节)
存储2^5-1的范围(0-31)
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
char buf[];
};
存储2^8-1的范围(0-255)
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* used */
uint8_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
uint16_t len; /* used */
uint16_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
uint32_t len; /* used */
uint32_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
uint64_t len; /* used */
uint64_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
使用新的SDS结构而不使用C原生字符数组的原因是:
- 1、提升效率
原来的字节数组计算长度
和扩容方面
效率都比较低
计算长度需要从头遍历到结束标记\0
,才可以求得长度
扩容方面,每次计算扩容字符串长度的时候都需要做内存的重新分配,如果字符串不常修改还可以接受,但是redis中value会经常的修改,所以需要重新设置扩容算法
(1)、空间预分配:修改前检查free
的空间是否够用(如果修改后len
的值小于1M
,则分配free
的大小于len相等,如果len
的值大于1M
,则此时分配的free
的大小为1M
)
(2)、惰性空间释放:当缩短SDS字符串是,不会立即回收多余的空间,而是增加free
的值为以后再次增长操作提供空间- 2、保证二进制安全
C语言中的字符数组要求以\0
作为结尾,所以字符串中只能保存文本信息,不能存储二进制数据。改造后的SDS使用len来判断字符串的长度,而非\0
。所以保证了二进制的安全性,可以存储二进制文件
Redis3.2将SDS结构进一步优化为SDShdr5,SDShdr8,SDShdr16,SDShdr32的目的:
- 节省存储空间
原来的SDS结构中int
类型的len
和int
类型的free
各自分别占4个字节,也就是最大长度可以表示为2^32-1的长度,平常使用的长度很少达到这个长度,所以对长度进行一下优化
SDShdr5
结构除去buff数组外只有一个字节长度的flag
SDShdr8
结构除去buff数组外只有一个字节长度的flag
,各一个字节长度的len
和alloc
SDShdr16
结构除去buff数组外只有一个字节长度的flag
,各两个字节长度的len
和alloc
SDShdr32
结构除去buff数组外只有一个字节长度的flag
,各三个字节长度的len
和alloc
其中flag
将8个bit位分为高3位和低5位,高3位表示type
,低5位表示长度
type
为000的时候表示SDShdr5
type
为001的时候表示SDShdr8
type
为010的时候表示SDShdr16
type
为011的时候表示SDShdr32
字节长度超多44的用
raw
和小于44的用embstr
编码
redis在创建一个字符串对象的时候会创建两个对象,一个redisObejct一个是sds
redisObject的结构如下typedef struct redisObject { unsigned type:4;//对象类型(4位=0.5字节) unsigned encoding:4;//编码(4位=0.5字节) unsigned lru:LRU_BITS;//记录对象最后一次被应用程序访问的时间(24位=3字节) int refcount;//引用计数。等于0时表示可以被垃圾回收(32位=4字节) void *ptr;//指向底层实际的数据存储结构,如:SDS等(8字节) } robj;
embstr
使用jemalloc
内存分配器,在创建的时候会将redisObejct
和sds
结构创建为一个连续的内存空间
jemalloc
最大分配的内存为64字节(CPU的缓存行一次性读取的最大字节也是64,这样只需要一次读取即可,效率最高),除去redisObject
所用的(4+3+0.5+0.5+8)的16个字节和sds
中len
和free
所占的8个字节,再加上\0
所占用的1个字节,留给buff字符数组最大的长度为(64-8-16-1)=39
所以3.2以前的redis超过39
个字节就用raw
,39
个字节以内使用embstr
但是在redis3.2之后的版本,对SDS
结构进行了优化,使用flag``uint8 len
和uint8 alloc
将8个字节缩短为3个字节,所以buff数组的大小由原来的39
个字节增长为44
个字节
embstr
和raw
的区别
embstr
在创建的时候创建的是连续的内存空间,所以在回收的时候效率更快,只需要回收一次,而raw
在创建的时候不一定创建的是两个连续的空间,需要回收两次
embstr
在查找的时候效率更高
2、list的底层实现
list的底层使用快速双向链表quicklist
或者压缩链表ziplist
来实现的。
list的底层并没有使用传统的双向链表的结构是因为
(1)、双向链表需要有一个前指针
和后指针
,每个指针占用的空间分别都是8byte,占用内存
比较多
(2)、双向链表所通用的一个问题是会形成很多的内存碎片
压缩链表ziplist
结构是
压缩链表
ziplist
特点
(1)、是一个连续的内存空间,极大节省内存空间
(2)、在扩容
的时候需要重新寻找一大块内存空间
(3)、从头开始遍历
的时候,比较麻烦
所以当ziplist
很长的时候,就需要使用quicklist
来实现list
,quicklist
会将ziplist
分成很多个段
快速双向链表quicklist
结构
通过设置,提高效率
1、list-max-ziplist-size = -2
-2
为默认值,表示单个ziplist
节点最大能存储8kb
,超过这个限制就开始分裂为多个ziplist
,其他的设置还有-5: max size: 64 Kb <-- not recommended for normal workloads -4: max size: 32 Kb <-- not recommended -3: max size: 16 Kb <-- probably not recommended -2: max size: 8 Kb <-- good -1: max size: 4 Kb <-- good
2、
list-compress-depth = 0
默认设置为0,表示都不进行压缩,如果为1,则表示从头节点往后1个,尾节点往后一个不用压缩,其他的节点全部压缩(压缩是为了减少内存碎片,quicklist
中的多个ziplist
也会形成内存碎片)
3、hash的底层实现
hash的底层实现为hashtable
或者ziplist
。
hashtable的底层实现
hashtable的扩容
扩容过程
hashtable扩容注意
1、正常扩容和收缩
扩展:ht[1]的大小为第一个大于等于ht[0].used*2。
收缩:ht[1]的大小为第一个大于等于ht[0].used/2 。
2、渐进式扩容
如果key值太多的情况下,redis会采用渐进式扩容。首选需要扩容的话,会先维护一个索引计数器变量rehashidx
,设置为0表示开始扩容,然后再每次对字典的增删改操作的同时进行扩容,多次操作之后才能扩容完成,扩容完成之后索引计数器
变量就设置为-1
当数据量比较小或者单个元素的时候,底层使用的是ziplist存储,具体可以通过配置来制定
-
hash-max-ziplist-entries = 512
当ziplist
的元素超过512个,将改为hashtable
(默认值) -
hash-max-ziplist-entries = 64
当单个元素的大小超过64byte时,将改为hashtable
(默认值)
127.0.0.1:6379> hset hash-test k1 123456789-123456789-123456789-123456789-123456789-123456789-12345
(integer) 1
127.0.0.1:6379> object encoding hash-test
"hashtable"
127.0.0.1:6379> hset hash-test1 k1 123456789-123456789-123456789-123456789-123456789-123456789-123
(integer) 1
127.0.0.1:6379> object encoding hash-test1
"ziplist"
注意!
1、hashtable
是无序的 ziplist
是有序的
2、在能使用hash
的情况下优先使用hash
,不要使用String
,因为使用太多的String
,则会创建出过多的key
,当key
大量的时候,就会容易发生hash碰撞
,所以就需要频繁的rehash
,每次rehash
就会创建2倍的内存,造成内存浪费
4、set的底层实现
hash的底层实现为整数数组intset
或者hashtable
。
当set都为整数的时候,set的底层实现都是使用intset
结构实现
如果set中存在字符串的值,则使用hashtable
来实现
intset
是有序的,hashtable
是无序的
127.0.0.1:6379> sadd test-set1 1 2 3 4
127.0.0.1:6379> smembers test-set1
1) "1"
2) "2"
3) "3"
4) "4"
127.0.0.1:6379> object encoding test-set1
"intset"
127.0.0.1:6379> sadd test-set1 a b c
127.0.0.1:6379> smembers test-set1
1) "3"
2) "2"
3) "1"
4) "c"
5) "4"
6) "b"
7) "a"
127.0.0.1:6379> object encoding test-set1
"hashtable"
# 再次删除字符串之后,还是使用hasttable结构存储
127.0.0.1:6379> srem test-set1 a b c
(integer) 3
127.0.0.1:6379> smembers test-set1
1) "1"
2) "2"
3) "4"
4) "3"
127.0.0.1:6379> object encoding test-set1
"hashtable"
5、sortset的底层实现
sortset
底层使用压缩列表ziplist
或跳表skiplist
的结构实现
当数据量小的情况下,使用ziplist
实现,当数据量大的情况下使用ziplist
实现,具体可以通过配置设置
zset-max-ziplist-entris 128 #元素个数超过128个,将使用skiplist
zset-max-ziplist-value 64 #单个元素大小超多64byte,将使用skiplist
默认设置下的底层结构
127.0.0.1:6379> zadd test-zset 1 a
(integer) 1
127.0.0.1:6379> zrange test-zset 0 -1
1) "a"
127.0.0.1:6379> object encoding test-zset
"ziplist"
127.0.0.1:6379> zadd test-zset 2 123456789-123456789-123456789-123456789-123456789-123456789-12345
(integer) 1
127.0.0.1:6379> zrange test-zset 0 -1
1) "a"
2) "123456789-123456789-123456789-123456789-123456789-123456789-12345"
127.0.0.1:6379> object encoding test-zset
"skiplist"
skiplist
的底层实现
查找对应元素的时候,先从最高的索引层找,例如找c 150,则先从L1找,L1的指针指向b,查看b120小于150,则继续往后找,b的指针指向null,则向下一层找,向下一层b的指针指向c,查看c的score为150,所以找到对应的元素c
skiplist结构 skiplist的底层实现
参考资料
网友评论