4、Redis高性能的根本原理

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类型的lenint类型的free各自分别占4个字节,也就是最大长度可以表示为2^32-1的长度,平常使用的长度很少达到这个长度,所以对长度进行一下优化
    SDShdr5结构除去buff数组外只有一个字节长度的flag
    SDShdr8结构除去buff数组外只有一个字节长度的flag,各一个字节长度的lenalloc
    SDShdr16结构除去buff数组外只有一个字节长度的flag,各两个字节长度的lenalloc
    SDShdr32结构除去buff数组外只有一个字节长度的flag,各三个字节长度的lenalloc
    其中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内存分配器,在创建的时候会将redisObejctsds结构创建为一个连续的内存空间
jemalloc最大分配的内存为64字节(CPU的缓存行一次性读取的最大字节也是64,这样只需要一次读取即可,效率最高),除去redisObject所用的(4+3+0.5+0.5+8)的16个字节和sdslenfree所占的8个字节,再加上\0所占用的1个字节,留给buff字符数组最大的长度为(64-8-16-1)=39
所以3.2以前的redis超过39个字节就用raw,39个字节以内使用embstr
但是在redis3.2之后的版本,对SDS结构进行了优化,使用flag``uint8 lenuint8 alloc将8个字节缩短为3个字节,所以buff数组的大小由原来的39个字节增长为44个字节

embstrraw的区别
embstr在创建的时候创建的是连续的内存空间,所以在回收的时候效率更快,只需要回收一次,而raw在创建的时候不一定创建的是两个连续的空间,需要回收两次
embstr在查找的时候效率更高

2、list的底层实现

list的底层使用快速双向链表quicklist或者压缩链表ziplist来实现的。
list的底层并没有使用传统的双向链表的结构是因为
(1)、双向链表需要有一个前指针后指针,每个指针占用的空间分别都是8byte,占用内存比较多
(2)、双向链表所通用的一个问题是会形成很多的内存碎片

压缩链表ziplist结构是

ziplist

压缩链表ziplist特点
(1)、是一个连续的内存空间,极大节省内存空间
(2)、在扩容的时候需要重新寻找一大块内存空间
(3)、从头开始遍历的时候,比较麻烦
所以当ziplist很长的时候,就需要使用quicklist来实现listquicklist会将ziplist分成很多个段

快速双向链表quicklist结构

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的扩容
扩容过程

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 = 512ziplist的元素超过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的底层实现

参考资料

1、https://blog.csdn.net/u010710458/article/details/80604740

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容