Redis 基础数据结构(二)

接下来我们逐个介绍Redis基本数据结构

一、String(字符串)简单介绍

1、Redis的String是最简单的数据结构,他的内部表示就是一个字符数组。

2、Redis的字符串是动态的字符串,是可以修改的字符串(这点是区别于java里面的字符串String的),类似java语言里面的ArrayList实现,Redis采用预分配冗余空间的方式来减小内存的频繁分配。如下图所示,redis内部分字符串分配的实际空间capacity一般要高于字符串时间长度len。

3、扩容:当字符串长度小于1MB时,扩容是加倍现有空间,如果字符串长度超过了1MB,一次只会扩容1MB的空间。

4、需要注意的是,字符串最大的长度为512MB。

5、(扩展)字符串由很多个字节组成,每个字节由8个bit组成,如此便可以将一个字符串看成是很多个bit的组合,这便是bitmap(位图)数据结构。位图的介绍也会放在后面来介绍

二、String(字符串)内部实现

1、Redis中的字符串是可以修改的字符串,在内存中是以字节数组的形式存在的。redis使用c语言编写,我们知道在c语言里面,字符串标准形式是以null(0x\0)作为结束符,在redis里面,如果要获取以为NULL结尾的字符串长度使用的strlen标准库函数,这个函数的算法时间复杂度是O(n),他会对字节数组进行扫描,这对于单线程redis来说是无法承受的。

2、Redis的字符串叫做“SDS”,也就是Simple Dynamic String ,他的结构是一个带长度信息的字节数组。定义如下

struct SDC<T> {
    // 数组容量
	T capactity;
    // 数组长度
	T len;
    // 特殊标志符
	byte flags;
    // 数组内容
	byte[] content;
}

3、content 里面存储了真正的字符串内容,len表示字符串的实际长度。

4、redis的字符串数据结构既然是动态的数据结构,肯定支持append操作,如果数组没有冗余空间,那么追加操作必然涉及分配新数组,然后将旧内容进行拷贝,再append到新内容中,如果字符串的长度非常长,新内存和复制的开销就会很大。源码如下

/* Append the specified binary-safe string pointed by 't' of 'len' bytes to the
 * end of the specified sds string 's'.
 *
 * After the call, the passed sds string is no longer valid and all the
 * references must be substituted with the new pointer returned by the call. */
sds sdscatlen(sds s, const void *t, size_t len) {
    size_t curlen = sdslen(s);
    // 按需调整空间,如果capacity不够追加的内容,就会重新分配字节数组并复制原字符串的内容到新的数组中
    s = sdsMakeRoomFor(s,len);
    if (s == NULL) return NULL;
    memcpy(s+curlen, t, len);
    sdssetlen(s, curlen+len);
    s[curlen+len] = '\0';
    return s;
}

5、Redis对内存的使用可以说做到了极致,例如上面定义的SDS结构体使用了泛型T,因为当字符串比较短时,len和capacity可以使用byte 和 short 来表示不同长度的字符串可以使用不同的结构体来表示。Redis规定字符串的长度不能超过512MB,创建字符串时len和capacity一样长,不会分配冗余空间,这是因为绝大部分情况下,我们不会使用到Redis的append操作来修改字符串内容。

6、redis的字符串有两种存储方式,在长度特别短的时候,使用embstr形式存储,当长度超过44字节时,使用raw形式存储。那什么是embstr和raw呢?二者又有什么区别呢?以及为什么是44为分界线呢?我们做个实验看一下,如下代码,debug Object 输出中的encoding字段,一个字符的差别,存储形式却发生了变化。

com.xiaozhameng.aliyun:6379> set codehole abcdefghijklmnopqrstuvwxyz012345678912345678
OK
com.xiaozhameng.aliyun:6379> debug object codehole
Value at:0x7f62a76150c0 refcount:1 encoding:embstr serializedlength:45 lru:3942476 lru_seconds_idle:15
com.xiaozhameng.aliyun:6379> set codehole abcdefghijklmnopqrstuvwxyz0123456789123456789
OK
com.xiaozhameng.aliyun:6379> debug object codehole
Value at:0x7f62a76665e0 refcount:1 encoding:raw serializedlength:46 lru:3942521 lru_seconds_idle:9
com.xiaozhameng.aliyun:6379> 

为了解释上面的这种情况,我们先了解一下redis的对象头结构,redis中所有的对象都有如下的头结构

// 64-bit system
struct RedisObject {
	int4 type;		// 4bits
	int4 encoding;	// 4bits
	int24 lru;		// 24bits
	int32 refcount;	// 4bytes;
	void *ptr;		// 8bytes; 
}

不同的对象具有不同的类型,同一个类型(type)会有不同的存储形式(encoding),为了记录对象的LRU信息,使用了24个bit来记录LRU信息,每个对象都有一个引用计数,当引用计数为0时,对象就会被销毁。内存被回收。ptr指针指向对象内容的具体位置。这样一个RedisObject 对象头结构要占用16byte的存储空间。另外再看下SDS结构体的大小,SDS对象头大小容量是capacity+3,至少是3字节,意味着分配一个字符串的最小空间占用是19byte。我们再来看一下embstr和raw,embstr是这样一种存储形式,它将RedisObject对象头结构和SDS对象连续存在一起,使用malloc 方法一次分配,而raw则需要使用两次malloc方法,两个对象头在内存地址上一般是不连续的。而内存分配器jemalloc temalloc 等分配内存大小的单位都是2/4/8/32/64字节等,为了能容纳一个完整的embstr,jemalloc至少会分配32个字节的空间,如果字符串再长一点,那就是64个字节空间,如果字符串总体超过了64字节,而如果字符串总体超过64字节,Redis就认为它是一个大字符串,不再适合使用embstr形式存储了。而应该使用raw形式。

那么当内存分配器分配了64字节空间时,这个字符串的长度最大可以是多少呢?我们前面提到了SDS结构体中的content中的字符串是以NULL结尾的字符串,之所以多出这样一个字节,是为了便于直接使用glibc的字符串处理函数,以及为了便于字符串的调试打印。如下图所示,我们看出当内存分配器分配64字节空间时,content的长度最大只有45字节,而字符串又是以NULL 结尾,所以content最大能容纳的长度也就是44字节了。

7、扩容策略:当字符串长度小于1MB时,扩容空间采用加倍策略,保证100%冗余空间,而当字符串长度超过1MB时,为了避免加倍的冗余空间过大而导致浪费,每次扩容只会多分配1MB的空间。

三、总结一下

这节里面,主要了解了Redis中String(字符串)这种基本的数据类型,以及底层的数据结构,了解了底层数据存储结构后,相信在以后使用Redis的字符串的时候,使用会更加得心应手。需要注意的是,虽然Redis最大支持的字符串大小为512MB,但是我们在业务使用时,要尽量避免存储大大对象,一方面是因为了避免网络传输带宽的开销,另外一方面我们已经知道了Redis的字符串是使用字符数组存储,那么在操作大对象存取的时候,难免给内存操作造成负担,给系统带来不变要的隐患。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值