redis存储编码
1. 4、Redis高性能的根本原理
内存的读写速度很快
Epoll 模型
常用的五大Redis的数据结构,及他们各自的底层实现结构
string hash list set sortset(zset)
string 的底层实现是 简单动态字符串(SDS -simple dynamic string)
hash 的底层实现是 hash表 或则 压缩列表(ziplist)
list 的底层实现是 双向列表(quicklist) 或者 压缩列表
set 的底层实现是 hash表(hashtable) 或者 整数数组
sortset(zset) 的底层实现是 压缩列表 或者 跳表
各个数据结构的底层实现概览
value是 string 类型的时候分为三种情况
(1)、当设置的值是整数类型的时候,redis底层会将 string 类型转化为 int 来存储
(2)、设置的值小于等于44个字节的时候,使用的编码是 embstr
(3)、设置的值大于44个字节的时候,使用的编码是 raw
redis是用C语言编写的,在C语言中 string 类型是锋顷用字符数组 char[] 来实现的。redis实现字符串的底层并没有直接使用C语言中的字符困基磨数组的形式,而是进行了改造,构造出了一种SDS的数据结构
list的底层使用 快速双向链表quicklist 或者 压缩链表ziplist 来实现的。
list的底层并没有使用传统的双向链表的结构是因为
(1)、双向链表需要有一个 前指针 和 后指针 ,每个指针占用的空间分别都是8byte, 占用内存 比较多
(2)、双向链表所通用的一个问题是会形成很多的 内存碎片
压缩链表 ziplist 结构是
快速双向链表 quicklist 结构
hash的底层实现为 hashtable 或者 ziplist 。
hashtable的底层实现
当数据量比较小或者单个元素的时候,底层使用的是ziplist存储,具体可以通过配置来制定
1、 hashtable 是无序的 ziplist 是有序的
2、在能使用 hash 的情况下优先使用 hash ,不要使用 String ,因为使用太多的 String ,则会创建出过多的 key ,当 key 大量的时候,就会容易发生 hash碰撞 ,所以就需要频繁的 rehash ,每次 rehash 就会创建2倍的内存,造成内存浪费
hash的底层实现为 整数数组intset 或者 hashtable 。
当set都为整数的时候,set的底层实现都是使用 intset 结构实现
如果set中存在字符串的值,则使用 hashtable 来实现
intset 是汪斗有序的, hashtable 是无序的
sortset 底层使用 压缩列表ziplist 或 跳表skiplist 的结构实现
当数据量小的情况下,使用 ziplist 实现,当数据量大的情况下使用 ziplist 实现,具体可以通过配置设置
默认设置下的底层结构
skiplist 的底层实现
查找对应元素的时候,先从最高的索引层找,例如找c 150,则先从L1找,L1的指针指向b,查看b120小于150,则继续往后找,b的指针指向null,则向下一层找,向下一层b的指针指向c,查看c的score为150,所以找到对应的元素c
1、 https://blog.csdn.net/u010710458/article/details/80604740
2. Redis底层数据结构之string
我们都知道, Redis 是由 C 语言编写的。在 C 语言中,字符串标准形式是以空字符 作为结束符的,但是 Redis 里面的字符串却没有直接沿用 C 语言的字符串。主要是因为 C 语言中获取字符串长度可以调用 strlen 这个标准函数,这个函数的时间复杂度是 O(N) ,由于 Redis 是单线程的,承受不了这个时间复杂度。
在上一篇文亮亏章中,我们介绍了 Redis 的 RedisObject 的数据结构,如下所示:
对于不同的对象, Redis 会使用不同的类型来存储。对于同一种类型 type 会有不同的存储形式 encoding 。对于 string 类型的字符串,其底层编码方式共有三种,分别为 int 、 embstr 和 raw 。
使用 object encoding key 可以查看 key 对应的 encoding 类型,如下所示:
对于 embstr 和 raw 这两种 encoding 类型,其存储方式还不太一样。对于 embstr 类型,它将 RedisObject 对象头和 SDS 对象在内存中地址是连在一起的,但对于派亩 raw 类型,二者在内存地址不是连续的。
在介绍 string 类型的存储类型时,我们说到,对于 embstr 和 raw 两种类型其存储方式不一样,但 ptr 指针最后都指向一个 SDS 的结构。那什么是 SDS 呢? Redis 中的字符串称之为 Simple Dynamic String ,简称为 SDS 。与普通 C 语言的原始字符串结构相比, sds 多了一个 sdshdr 的头部信息, sdshdr 基本数据结构如下所示:
可以看出, SDS 的结构有点类似于 Java 中的 ArrayList 。 buf[] 表示真正存储的字符串内容, alloc 表示所分配的数组的长度, len 表示字符串的实际长度,并且由于 len 这个属性的存在, Redis 可以在 O(1) 的时间复杂度内获取数组长度。
为了追求对于内存的极致优化,对于不同长度的字符串, Redis 底层会采用不同的结构体来表示。在 Redis 中的 sds.h 源码中存在着五种 sdshdr ,分别如下:
上面说了, Redis 底层会根据字符串的长度来决定具体使用哪种类型的 sdshdr 。可以看出, sdshdr5 明显区别于其他四种结构,它一般只用于存储长度不会变化,且长度小于32个字符的字符串。但现在一般都不再使用该结构, 因为其结构没有 len 和 alloc 这两个属性,不具备动态扩容操作 ,一旦预分配的内存空间使用完,就需要重新分配内存并完成数据的复制和迁移,类似于 ArrayList 的扩容操作,这种操作对性能的影响很大。
上面介绍 sdshdr 属性的时候说过, flag 这个属性用于标识使用哪种 sdshdr 类型, flag 的低三位标识当前 sds 的类型,分别如下所示:
同时,注意到在每个 sdshdr 的头定义上都有一个 attribute((packed)) ,这个是为了告诉 gcc 取消优化对齐 ,这样,每个字段分配的内存地址就是 紧紧排列在一起的 , Redis 中字符串参数的传递直接使用 char* 指针,其实现原理在于,由于 sdshdr 内存分配禁止了优化对齐,所以 sds[-1] 指向的就是 flags 属性的内存地址,而通过 flags 属性又可以确定 sdshdr 的属性,进而可以读取头部字段确定 sds 的相关属性。
sds的逻辑图如下所示:
相比较于 C 语言原始的字符串,尘键森 sdshdr 的具备一些优势。
由于 sdshdr 中存在 len 这个属性,所以可以在 O(1) 的时间复杂度下获得长度;而传统的 C 语言得使用 strlen 这个标准函数获取,时间复杂度为 O(N) 。
原始的 C 语言一直使用与长度匹配的内存,这样在追加字符串导致字符串长度发生变化时,就必须进行内存的重新分配。内存重新分配涉及到复杂算法和系统调用,耗费性能和时间。对于 Redis 来说,它是单线程的,如果使用原始的字符串结构,势必会引发频繁的内存重分配,这个显然是不合理的。
因而, sds 每次进行内存分配时,都会通过内存的预分配来减少因为修改字符串而引发的内存重分配次数。这个原理可以参数 Java 中的 ArrayList ,一般在使用 ArrayList 时都会建议使用带有容量的构造方式,这样可以避免频繁 resize 。
对于 SDS 来说,当其使用 append 进行字符串追加时,程序会用 alloc-len 比较下剩下的空余内存是否足够分配追加的内容 ,如果不够自然触发内存重分配,而如果剩余未使用内存空间足够放下,那么将直接进行分配,无需内存重分配。其扩容策略为, 当字符串占用大小小于1M时,每次分配为 len * 2,也就是保留100%的冗余;大于1M后,为了避免浪费,只多分配1M的空间。
通过这种预分配策略, SDS 将连续增长 N 次字符串所需的内存重分配次数 从必定 N 次降低为最多 N 次。
缓冲区溢出是指当某个数据超过了处理程序限制的范围时,程序出现的异常操作。 原始的 C 语言中,是由编码者自己来分配字符串的内存,当出现内存分配不足时就会发生 缓存区溢出 。而 sds 的修改函数在修改前会判断内存,动态的分配内存,杜绝了 缓冲区溢出 的可能性。
对于原始的 C 语言字符串来说,它会通过判断当前字符串中是否存在空字符 来确定是否已经是字符串的结尾。因而在某些情况下,如使用空格进行分割一段字符串时,或者是图片或者视频等二进制文件中存在 等,就会出问题。而 sds 不是通过空字符串来判断字符串是否已经到结尾,而是通过 len 这个字段的值。所以说, sds 还具备 二进制安全 这个特性,即可以安全的存储具有特殊格式的二进制数据。
https://www.cnblogs.com/reecelin/p/13358432.html
3. Redis涓璖tring鍜孒ash鍝涓缁撴瀯镟村姞鐪佸唴瀛
string缁撴瀯锛
鍙浠ョ亩鍗曚换锷Redis镄凷tring缁撴瀯鏄鐢⊿DS锛堢亩鍗曞姩镐佸瓧绗︿覆锛夋暟鎹缁撴瀯𨱒ュ疄鐜扮殑銆
hash缁撴瀯浣跨敤ziplist缁撴瀯镞讹细
ziplist链澶х殑鐗圭偣灏辨槸锛屼粬涓嶆槸hashtable缁撴瀯锛岃屾槸涓涓姣旇缉闀跨殑瀛楃︿覆锛屽皢key-value閮芥寜镦ч‘搴忎緷娆℃憜鏀惧埌涓涓闀块暱镄勫瓧绗︿覆閲屾潵瀛桦偍銆傚傛灉瑕佹垒镆愪釜key锛屽氨闇瑕侀亶铡嗘暣涓闀垮瓧绗︿覆銆
铡熷洜锛 浣跨敤string瀛桦偍鏁版嵁镞讹纴姣忎竴涓璁板綍閮芥槸涓涓猄DS閮介渶瑕佸瓨鍦╨en銆乫ree𨱒ユ爣璇嗐备絾鏄浣跨敤hash镄剒iplist镞讹纴鍙闇瑕佹爣澶寸殑鍑犱釜镙囱瘑浣嶅栵纴鎺ョ潃閮芥槸绱у噾镄勬暟鎹銆傝繖灏辨槸涓轰粈涔坔ash(ziplist)姣攕tring镟磋妭鐪佸唴瀛樼殑铡熷洜銆
hash鏁版嵁缁撴瀯锛屽湪缂栫爜鏂瑰纺涓婃湁涓ょ嶏纴1鏄痟ashTable锛2鏄疴ipList銆
鎹㈠彞璇濊达纴zipList姣旇捣hashTable鍗犵敤镄勭┖闂村皯锛屼絾鏄浼氲楄垂镟村歝pu𨱒ヨ繘琛屾煡璇銆
缁忚繃瀹炴祴锛寁alue鏁伴噺鍦512镞讹纴镐ц兘鍜屽崟绾镄删ashTable鍑犱箮镞犲樊鍒锛屽湪value鏁伴噺涓嶈秴杩1024镞讹纴镐ц兘浠呮湁鏋佸皬镄勯檷浣庯纴鍙浠ュ拷鐣ャ
钥屽唴瀛桦崰鐢锛寊ipList姣攈ashTable闄崭绠鏋佸氥
redis涓涓轰粈涔坔ash姣攕tring锅氱紦瀛樻洿鑺傜渷鍐呭瓨涓庢晥鐜囨洿楂桡纻 - 镀忕伃闇镐竴镙风湅镞ュ嚭 - 绋嫔簭锻业TS500
阃夋嫨钖堥俣edis鏁版嵁缁撴瀯锛屽噺灏80%镄勫唴瀛桦崰鐢
redis string搴曞眰鏁版嵁缁撴瀯
4. Redis底层数据结构解密
一:摘要概述
很多 redis 的使用者都可以清晰明白的道出Redis中常用的对象如string、list、hash、set、zset,一些场景比较丰富的使用者可能会说布隆过滤器、geo、Hash等。但是对于这些对象底层实现的数据结构却是知之甚少,将会详细阐述redis中的底层数据结构。为了弥补大家的创伤,今天分享Redis底层数据结构内容。
二:SDS
string作为redis中常用对象之一,普遍用于用户信息缓存等场景。当string对象中encoding编码为embstr或raw时都是采用sds作为其底层实现
2.1 SDS结构
源码文件位于redis安装目录src下的sds.h,sds声明了五种头部类型,分别为sdshdr5、sdshdr8、sdshdr16、sdshdr32、sdshdr64。根据字符串长度创建不同头部的sds实例
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len;
uint8_t alloc;
unsigned char flags;
char buf[];
};
属性名称作用含义
len字符串长度
alloc预分配空间大小
flags低三位用于表示sds类型,可以查看sds.h文件76-82行定义
buf[]存储字符串用数组
2.2 SDS与C字符串区别
区别描述
长度计算 c中的字符串长度计算需要数组遍历,但是redis中的sds自身维护了len属性。所以O(1)时间复杂度即可
缓冲区溢出c中字符串更改如果未提前做好内存分配则会内存溢出,但是sds则会根据alloc与len计算预留内存是否足够分配重新申请内存
动态扩展 缓冲区溢出已经阐述这个概念,sds的内存空间会在字符串内容变更时自动扩展计算。策略为当字符换小于1M时*2翻倍,大于1M时每次扩容1M
惰性释放 与空间预分配相似操作的还有内存惰性释放,即字符串删除某些内容后所占用的内存空间并不会立即释放,后续字符串变更扩展就无需再申请内存
二:ZipList
ziplist可以说把redis对于内存的极致操作体现的淋漓尽致,链表除了节点值之外还需要维护前后节点两个指针,并且还会造成内存碎片。压缩列表紧凑的内存布局,所有节点都维护在整块内存中处理
2.1 ZipList结构
属性名称作用含义
zlbytes列表健占用内存的总字节数,在对列表健内存重分配或者是计算zlend的时候使用
zltail 指向压缩列表起始地址的指针
zllen 压缩列表的节点数量
entry压缩列表保存的节点数据
zlend压缩列表的尾节点
2.2 Entry节点结构
属性名称作用含义
previous_entry_length 字节为单位记录上一个节点的长度,如果上一个字节长度小于254占用1字节。大于254占用5字节,第一个字节设置为OxFE(十进制254),后面四个字节储存长度
encoding 记录content记录的数据类型以及长度。长度一、二、五字节,值的最高位为00、01、10表示类型为字节数组,长度使用除去最高位的其它位记录。11开头表示储存整数,除去最高位其他位置表示content数据长度
content 记录压缩列表记录的数据