同样的,假如要缩小字符串的长度,那么也需要重新申请释放内存。否则,字符串一直占据着未使用的空间,会造成内存泄露。
C 语言避免缓存区溢出和内存泄露完全依赖于人为,很难把控,但是使用 sds 就不会出现这两个问题,因为当我们操作 sds时,其内部会自动执行空间分配策略,从而避免了上述两种情况的出现。
空间预分配空间预分配指的是当我们通过 api 对 sds 进行扩展空间的时候,假如未使用空间不够用,那么程序不仅会为 sds 分配必须要的空间,还会额外分配未使用空间,未使用空间分配大小主要有两种情况:
1、假如扩大长度之后的 len 属性小于等于 1MB (即 1024 * 1024),那么就会同时分配和 len 属性一样大小的未使用空间(此时 buf 数组已使用空间 = 未使用空间)。
2、假如扩大长度之后的 len 属性大于 1MB,那么就会分配 1MB 未使用空间大小。
执行空间预分配策略的好处是提前分配了未使用空间备用后,就不需要每次增大字符串都需要分配空间,减少了内存重分配的次数。
惰性空间释放惰性空间释放指的是当我们需要通过 api 减小 sds 长度的时候,程序并不会立即释放未使用的空间,而只是更新 free 属性的值,这样空间就可以留给下一次使用。而为了防止出现内存溢出的情况,sds 单独提供给了 api 让我们在有需要的时候去真正的释放内存。
sds 和 C 语言字符串区别下面表格中列举了 Redis 中的 sds 和 C 语言中实现的字符串的区别:
C 字符串 SDS只能保存文本类不含空字符串 \0 数据 可以保存文本或者二进制数据,允许包含空字符串 \0
获取字符串长度的复杂度为 O(n) 获取字符串长度的复杂度为 O(1)
操作字符串可能会造成缓冲区溢出 不会出现缓冲区溢出情况
修改字符串长度 N 次,必然需要 N次内存重分配 修改字符串长度 N 次,最多需要 N 次内存重分配
可以使用 C 字符串相关的所有函数 可以使用 C 字符串相关的部分函数
sds 是如何被存储的
在 Redis 中所有的数据类型都是将对应的数据结构再进行了再一次包装,创建了一个字典对象来存储的,sds也不例外。每次创建一个 key-value 键值对,Redis 都会创建两个对象,一个是键对象,一个是值对象。而且需要注意的是在 Redis 中,值对象并不是直接存储,而是被包装成 redisObject 对象,并同时将键对象和值对象通过 dictEntry 对象进行封装,如下就是一个 dictEntry 对象:
typedef struct dictEntry { void *key;//指向key,即sds union { void *val;//指向value uint64_t u64; int64_t s64; double d; } v; struct dictEntry *next;//指向下一个key-value键值对(哈希值相同的键值对会形成一个链表,从而解决哈希冲突问题) } dictEntry;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;当我们在 Redis 客户端中执行命令 set name lonely_wolf ,就会得到下图所示的一个结构(省略了部分属性):
看到这个图想必大家会有疑问,这里面的 type 和 encoding 到底是什么呢?其实这两个属性非常关键,Redis 就是通过这两个属性来识别当前的 value 到底属于哪一种基本数据类型,以及当前数据类型的底层采用了何种数据结构进行存储。
type 属性type 属性表示对象类型,其对应了 Redis 当中的 5 种基本数据类型:
类型属性 描述 type 命令返回值REDIS_STRING 字符串对象 string
REDIS_LIST 列表对象 list
REDIS_HASH 哈希对象 hash
REDIS_SET 集合对象 set
REDIS_ZSET 有序集合对象 zset
可以看到,这就是对应了我们 5 种常用的基本数据类型。
encoding 属性