Redis的字符串数据类型底层实现原理

        在redis中定义了5种数据类型对象,分别为字符串类型对象,列表类型对象,哈希类型对象,集合类型对象和有序集合类型对象。字符串类型对象就是其中的一种,今天就来讲解一下redis中对字符串类型对象的实现。在redis的字符串类型对象中,底层都是采用的是简单动态字符串(SDS, Simple Dynamic String)数据结构来实现。

什么是SDS?

SDS(simple dynamic string),也叫简单动态字符串。众所周知,redis的底层是C语言编写的,结构定义为:

struct sdshdr{
	//buf数组中已使用的数量,统计字符串长度
	int len;
	//buf数组中未使用的数量
	int free;
	//buf字节数组,用于保存字符串
	char buf[];
}

个人仿造,便于java开发人员方便理解。在java中大致可以描述为:

class SDS{//根据上面进行抽象
	private int len;
	private int free;
	private char[] buf;
	public SDS(String str){
		this.buf = str.toCharArray();
		this.len = str.length();
		this.free = 0;
	}
}
class Test{
	public static void main(String[] args){
		//定义一个简单动态字符串
		SDS sds = new SDS("redis");
	}
}

举个例子,如果客户端执行命令:

redis> set msg "hello world";
OK

那么Redis将在数据库中创建一个新的键值对,其中键值对的键是一个字符串对象,对象的底层实现是一个保存着字符串“msg”的SDS。键值对的值也是一个字符串对象,对象的底层实现是一个保存着字符串“hello world”的SDS。

看到这里,可能会有这样的疑问?就是为什么不直接采用C语言提供的字符串(已空字符结尾的字符数组),而是才采用自己构建的简单动态字符串(SDS),并将他作为redis的默认字符串表示。这样会不会很麻烦呢?

C字符串和SDS字符串比较

1、C字符串中他本身不记录自身的长度信息,所有为了获取一个C字符串的长度,程序必须遍历整个字符串,对每个字符进行计数,直到遇到空字符为止,时间复杂度O(N)。

和C字符串相比SDS在len属性中记录了字符串本身的长度信息,因此获取SDS长度的时间复杂度O(1),这极大程度上提高了性能。

sdshdr->len //c,获取字符串长度
sds.getLen();//java,获取字符串长度

2、C字符串还会容易产生缓冲区溢出:

char *strcat(char *dest, const char *src);

因为C字符串不记录自身的长度,所以strcat假定用户在执行这个函数时,已经为dest分配了足够多的内存,可以容纳src字符串中的所有内容,而一旦这个假定不成立时,就会产生缓冲区溢出。

举个例子,假设程序里有两个在内存中紧邻着的C字符串s1和s2,其中s1保存了字符串"Redis",而s2则保存了字符串"MongoDB",如下所示:
在这里插入图片描述
如果执行以下操作:

strcat(s1, " Cluster");

将s1的内容修改为"Redis Cluster",但粗心的他却忘了在执行strcat之前为s1分配足够的空间,那么在strcat函数执行之后,s1的数据将溢出到s2所在的空间中,导致s2保存的内容被意外地修改,如下所示:
在这里插入图片描述
与C字符串不同的是,SDS的空间分配完全杜绝了发生缓冲区溢出的可能性,当需要对SDS进行修改时,程序首先会检查SDS的空间是否满足修改的需求 ( 例如,在添加时候,即free是否大于或等于添加的字符串长度 ) ,如果不符合会自动的对SDS空间进行修改,如何在执行实际的修改操作。所以使用SDS既不需要手动修改SDS的空间大小,也不会出在这里插入代码片现前面所说的缓冲区溢出问题。

举个例子,SDS的API里面也有一个用于执行拼接操作的sdscat函数,它可以将一个C字符串拼接到给定SDS所保存的字符串的后面,但是在执行拼接操作之前,sdscat会先检查给定SDS的空间是否足够,如果不够的话,sdscat就会先扩展SDS的空间,然后才执行拼接操作。
例如,如果我们执行:

redis> set msg "Redis" //执行前
OK
sdscat(s, " Cluster"); 

那么sdscat将在执行拼接操作之前检查s的长度是否足够,在发现s目前的空间不足以拼接"Cluster"之后,sdscat就会先扩展s的空间,然后才执行拼接"Cluster"的操作,拼接操作完成之后的SDS如下:
执行前:
在这里插入图片描述
执行后:
执行前
sdscat不仅对这个SDS进行了拼接操作,它还为SDS分配了13字节的未使用空间,并且拼接之后的字符串也正好是13字节长,这种现象既不是bug也不是巧合,它和SDS的空间分配策略有关,接下来的将对这一策略进行说明。

分配策略

1.空间预分配
空间预分配用于优化SDS的字符串增长操作:当对SDS进行拼接时,程序不仅会为SDS分配修改所必须要的空间,还会为SDS分配额外的未使用空间。

额外分配的未使用空间数量由以下公式决定:

如果对SDS进行修改之后,SDS的长度(也即是len属性的值)将
小于1MB,那么程序分配和len属性同样大小的未使用空间,这时SDS
len属性的值将和free属性的值相同。举个例子,如果进行修改之后,
SDS的len将变成13字节,那么程序也会分配13字节的未使用空间,SDS
的buf数组的实际长度将变成13+13+1=27字节(额外的一字节用于保存
空字符)。

如果对SDS进行修改之后,SDS的长度将大于等于1MB,那么程序会分配1MB的未使用空间。举个例子,如果进行修改之后,SDS的len将变为30MB,那么程序会分配1MB的未使用空间,SDS的buf数组的实际长度将为30MB+1MB+1byte。

通过空间预分配策略,Redis可以减少连续执行字符串增长操作所需的内存重分配次数。

2.惰性空间释放
惰性空间释放用于优化SDS的字符串缩短操作:当SDS的API需要
缩短SDS保存的字符串时,程序并不立即使用内存重分配来回收缩短后
多出来的字节,而是使用free属性将这些字节的数量记录起来,并等待
将来使用。

SDS和C字符的区别

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值