String
字符串是redis
最基础的数据结构,其它几种数据结构,都是在此基础之上构建的。字符串类型的值是字符串(简单的字符串、复杂的字符串(例如JSON、XML))、数字(整数、浮点数),甚至是二进制(图片、音频、视频),最大不能超过512MB。
string
字符串对象的内部编码有三种:int、raw 、embstr,Redis
会根据当前值的类型和长度来决定使用哪种编码来实现- int:如果一个字符串对象保存的是整数值,并且这个整数值可以用
long
类型来表示 - raw:如果字符串对象保存的是一个字符串值,并且这个字符串值的长度大于32字节
- embstr:如果字符串对象保存的是一个字符串值,并且这个字符申值的长度小于等于32字节
- int:如果一个字符串对象保存的是整数值,并且这个整数值可以用
Redis
使用对象来表示数据库中的键和值每个对象都由一个redisObject
结构表示,该结构中和保存数据有关的三个属性分别是type
属性、encoding
属性和ptr
属性typedef struct redisObiect{
//类型
unsigned type:4;
//编码
unsigned encoding:4;
//指向底层数据结构的指针
void *ptr;
}
一、字符串存储实现
Redis字符串对象底层的数据结构实现主要是int和简单动态字符串SDS(SDS是Redis自定义的底层数据结构),其通过不同的编码方式映射到不同的数据结构。
SDS动态字符串
我们在正常操作redis客户端时,redis提供了一个逻辑上的对象系统构建的键值对数据库,以方便客户端操作,这些对象包括redis常用的数据类型,但是在redis更深处,操作内存并不是直接使用这些对象,而是使用了 简单动态字符串,链表,字典(散列表),跳跃表,整数集合,压缩列表这些数据结构来操作内存
redis默认并没有直接使用C字符串【C字符串仅仅作为字符串字面量,不易改变,常用在一些无需对字符串进行修改的地方,如打印日志等】。而是以struct
的形式构造了一个SDS的抽象类型。当Redis需要一个可以被修改的字符串时,就会使用SDS来表示。在Redis数据库里,包含字符串值的键值对都是由SDS实现的(Redis中所有的键都是由字符串对象实现的即底层是由SDS实现,Redis中所有的值对象中包含的字符串对象底层也是由SDS实现)。
SDS存储结构:
struct sdshdr{
//int 记录buf数组中未使用字节的数量 如上图free为0代表未使用字节的数量为0
int free;
//int 记录buf数组中已使用字节的数量即sds的长度 如上图len为5代表未使用字节的数量为5
int len;
//字节数组用于保存字符串 sds遵循了c字符串以空字符结尾的惯例目的是为了重用c字符串函数库里的函数
char buf[];
}
SDS存储结构图:
为什么使用SDS
- 缓冲区溢出问题
C字符串,如果程序员在字符串修改的时候如果忘记给字符串重新分配足够的空间,那么就会发生内存溢出,如上图所示,忘记给s1分配足够的内存空间, s1的数据就会溢出到s2的空间, 导致s2的内容被修改.。而Redis提供的SDS其内置的空间分配策略则可以完全杜绝这种事情的发生。当API需要对SDS进行修改时, API会首先会检查SDS的空间是否满足条件, 如果不满足, API会自动对它动态扩展, 然后再进行修改。
内存重新分配
- C字符串内存重分配
在C字符串中,如果对字符串进行修改,那么我们就不得不面临内存重分配。因为C字符串是由一个N+1长度的数组组成,如果字符串的长度变长,我们就必须对数组进行扩容,否则会产生内存溢出。而如果字符串长度变短,我们就必须释放掉不再使用的空间,否则会发生内存泄漏。
- SDS内存空间分配策略
对于redis这种高性能的内存为主的数据库,如果每次都操作内存重新进行分配,那么无疑是巨大的损失,而redis新构建的底层SDS结构就是为了,解决空间分配问题
SDS分配空间会分为两大部分:
- 预分配【冗余分配】
- 惰性空间的释放
1. 预分配
我们知道在数组进行扩容的时候,往往会申请一个更大的数组,然后把数组复制过去。为了提升性能,我们在分配空间的时候并不是分配一个刚刚好的空间,而是分配一个更大的空间。Redis同样基于这种策略提供了空间预分配。当执行字符串增长操作并且需要扩展内存时,程序不仅仅会给SDS分配必需的空间还会分配额外的未使用空间,其长度存到free属性中。其分配策略如下:
- 如果修改后len长度将小于1M,这时分配给free的大小和len一样,例如修改过后为10字节, 那么给free也是10字节,buf实际长度变成了10+10+1 = 21byte
- 如果修改后len长度将大于等于1M,这时分配给free的长度为1M,例如修改过后为30M,那么给free是1M.buf实际长度变成了30M+1M+1byte
1. 惰性空间的释放
惰性空间释放用于字符串缩短的操作。当字符串缩短时,程序并不是立即使用内存重分配来回收缩短出来的字节,而是使用free属性记录起来,并等待将来使用【部分并非真正的释放】
Redis通过空间预分配和惰性空间释放策略在字符串操作中一定程度上减少了内存重分配的次数。但这种策略同样会造成一定的内存浪费,因此Redis SDS API提供相应的API让我们在有需要的时候真正的释放SDS的未使用空间。
二进制安全
C字符串中的字符必须符合某种编码(比如ASCII),并且除了字符串的末尾之外,字符串里面不能包含空字符,否则最先被程序读入的空字符将被误认为是字符串结尾,这些限制使得C字符串只能保存文本数据,而不能保存像图片、音频、视频、压缩文件这样的二进制数据。如果有一种使用空字符来分割多个单词的特殊数据格式,就不能用C字符串来表示,如”Redis\0String”,C字符串的函数会把’\0’当做结束符来处理,而忽略到后面的”String”。而SDS的buf字节数组不是在保存字符,而是一系列二进制数组,SDS API都会以二进制的方式来处理buf数组里的数据,使用len属性的值而不是空字符来判断字符串是否结束。
时间复杂程度
我们来看几个Redis常见操作的时间复杂度。
- 获取SDS长度: 由于SDS中提供了len属性,因此我们可以直接获取时间复杂度为
O(1)
,C字符串为O(n)
- 获取SDS未使用空间长度: 时间复杂度为
0(1)
,原因同1 - 清除SDS保存的内容:由于惰性空间分配策略,复杂度为
O(1)
- 创建一个长度为N的字符串:时间复杂度为
O(n)
- 拼接一个长度为N的C字符串:时间复杂度为
O(n)
- 拼接一个长度为N的SDS字符串:时间复杂度为
O(n)
Redis在获取字符串长度上的时间复杂度为常数级O(1)
SDS与C字符串比较
通过以上分析,我们可以得到,SDS这种数据结构相对于C字符串有以下优点:
- 杜绝缓冲区溢出
- 减少字符串操作中的内存重分配次数
- 二进制安全
- 由于SDS遵循以空字符结尾的惯例,因此兼容部门C字符串函数
Redis定位于一个高性能的内存数据库,其面向的就是大数据量,大并发,频繁读写,高响应速度的业务。因此在保证安全稳定的情况下,性能的提升非常重要。而SDS这种数据结构屏蔽了C字符串的一些缺点,可以提供安全高性能的字符串操作。
字符串的存储结构
上面,我们已经知道了,Redis的底层字符串SDS的结构,那么我现在看一看,redis的Strng数据类型的存储结构
字符串对象的内部编码有3种 :【Redis会根据当前值的类型和长度来决定使用哪种编码来实现】
int
:如果一个字符串对象保存的是整数值,并且这个整数值可以用long
类型来表示raw
:如果字符串对象保存的是一个字符串值,并且这个字符串值的长度大于32字节embstr
:如果字符串对象保存的是一个字符串值,并且这个字符申值的长度小于等于32字节
如果一个字符串对象保存的是整数值,并且这个整数值可以用
**long**
类型来表示,那么字符串对象会将整数值保存在字符串对象结构的**ptr**
属性里面(将**void***
转换成**1ong**
),并将字符串对象的编码设置为**int**
如果字符串对象保存的是一个字符串值,并且这个字符串值的长度大于32字节,那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串值,并将对象的编码设置为
**raw**
如果字符串对象保存的是一个字符串值,并且这个字符申值的长度小于等于32字节,那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串值,并将对象的编码设置为
**embstr**
embstr
编码是专门用于保存短字符串的一种优化编码方式,我们可以看到**embstr**
和**raw**
编码都会使用**SDS**
来保存值,但不同之处在于**embstr**
会通过一次内存分配函数来分配一块连续的内存空间来保存**redisObject**
和**SDS**
。而**raw**
编码会通过调用两次内存分配函数来分别分配两块空间来保存**redisObject**
和**SDS**
。Redis这样做会有很多好处。
**embstr**
编码将创建字符串对象所需的内存分配次数从raw编码的两次降低为一次- 释放
**embstr**
编码的字符串对象同样只需要调用一次内存释放函数 - 因为
**embstr**
编码的字符串对象的所有数据都保存在一块连续的内存里面可以更好的利用CPU缓存提升性能
原文大佬:Redis对象-字符串