对于Redis中字符串对象的类型的代码主要分布在两个文件之中,其中在src/object.c文件中主要是实现了字符串数据类型的构造相关的操作,另外在src/t_string.c文件中则实现了字符串的相关命令。
对于Redis中的字符串对象,可以使用三种编码类型,分别是:
OBJ_ENCODING_RAW
OBJ_ENCODING_INT
OBJ_ENCODING_EMBSTR
其中当字符串的长度较短的时候,Redis会采用OBJ_ENCODING_EMBSTR
的编码方式,这个长度阈值的定义为#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44
,当超过这个长度的字符串则用OBJ_ENCODING_RAW
的编码方式,而当这个字符串实际上是一个整形数的时候,Redis则会采用OBJ_ENCODING_INT
对其进行编码。
字符串对象类型是Redis中最基本的数据类型,虽然名为字符串数据对象,但是其可以存储的数据除了数字,文本数据之外,还可以存储例如一张图片或者其他的序列化数据,其本质原因是,字符串对象类型底层采用的是sds
数据类型,而这个类型是二进制安全的,因此可以存储任何数据,每一个字符串对象最大可以存储512MB的数据,这个限制是通过src/t_string.c文件中的静态函数checkStringLength
来定义的。
Redis字符串对象的基础操作
Redis字符串对象的构造
字符串型对象的构造
Redis对于字符串型的字符串对象定义了两种编码方式:
如果使用的是
OBJ_ENCODING_RAW
这种编码方式,那么表示对象的robj
与表示底层数据的sds
在内存分布上是相互分离的,通过robj.ptr
指针指向sds
数据,来实现对象对数据的引用。如果使用的是
OBJ_ENCODING_EMBSTR
这种编码方式,那么对象robj
数据与底层数据的sds
是处于一个连续的内存块中的,sds
数据存储在robj
数据之后。
Redis为创建字符串型对象提供了三个接口函数:
robj *createRawStringObject(const char *ptr, size_t len);
robj *createEmbeddedStringObject(const char *ptr, size_t len);
robj *createStringObject(const char *ptr, size_t len);
其中createRawStringObject
与createEmbeddedStringObject
函数用于创建OBJ_ENCODING_RAW
编码或者OBJ_ENCODING_EMBSTR
编码的字符串对象,而createStringObject
则是对上述两个函数更高级的封装,会判断len
是否大于#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44
,如果大于这个阈值,那么会按照OBJ_ENCODING_RAW
编码,调用createRawStringObject
接口进行创建,否则的话则会调用createEmbeddedStringObject
来创建字符串对象。
另外,我们可以通过createEmbeddedStringObject
的实现来了解OBJ_ENCODING_EMBSTR
编码对象的内存分布:
robj *createEmbeddedStringObject(const char *ptr, size_t len)
{
robj *o = zmalloc(sizeof(robj) + sizeof(struct sdshdr8) + len + 1);struct sdshdr8 *sh = (void *)(o + 1);
o->type = OBJ_STRING;
o->encoding = OBJ_ENCODING_EMBSTR;
o->ptr = sh + 1;
o->refcount = 1;
...
}
数值型对象的构造
整形数值
处于节约内存的考虑,一部分整形数值是直接存储在robj.ptr
这个指针中,对于这种形式的对象,Redis将其设定为OBJ_ENCODING_INT
的编码类型;而为了进一步的节约内存,对于比较小的整数#define OBJ_SHARED_INTEGERS 10000
,Redis会在启动的时候自动创建这么一组整数形式的字符串对象shared.integers
供后续共享。如果用户需要一个数值小于OBJ_SHARED_INTEGERS
的整数型字符串对象,那么他可以直接从shared.integers
中进行引用,而不需要重复创建。
Redis为创建整形数值字符串对象提供了三个接口函数:
robj *createStringObjectFromLongLongWithOptions(long long value, int valueobj);
robj *createStringObjectFromLongLong(long long value);
robj *createStringObjectFromLongLongForValue(long long value);
其中createStringObjectFromLongLongWithOptions
是这个三个函数中的基础,这个函数会给定一个整形数value
,返回对应的字符串对象,如果valueobj
参数为1,那么Redis会强制为value
创建一个独立的字符串对象,否则如果value
小于OBJ_SHARED_INTEGERS
的话,Redis则会为value
则会返回系统创建的共享对象。其具体的执行步骤为:
判断
value
以及valueobj
的数值,如果满足value >= 0 && value < OBJ_SHARED_INTEGERS && valueobj == 0
,那么直接从shared.integers
中返回共享对象。如果不满足1中的条件,那么判断
value >= LONG_MIN && value <= LONG_MAX
,如果满足这种情况,那么使用OBJ_ENCODING_INT
编码方式创建一个字符串对象,并使用o->ptr = (void *)((long)value);
将value
存储在robj.ptr
指针上。如果上述两个条件都不满足,那么Redis会通过
sdsfromlonglong
创建一个sds
数据,并使用这个数据创建一个字符串对象。
而createStringObjectFromLongLong
和createStringObjectFromLongLongForValue
则是对于createStringObjectFromLongLongWithOptions
函数的一个封装,分别相当于valueobj == 0
以及valueobj == 1
的情况。
浮点数数值
robj *createStringObjectFromLongDouble(long double value, int humanfriendly);
上面这个函数用于从一个长浮点数value
中创建一个字符串对象。
Redis数值型字符串对象的解码
整形数值的解码
int isSdsRepresentableAsLongLong(sds s, long long *llval);int isObjectRepresentableAsLongLong(robj *o, long long *llval);
上面两个函数分别用于从一个sds
数据和robj
数据中尝试解码出整形数据,如果解码失败函数会返回C_ERR
,否则返回C_OK
,并解码出的数值存储在llval
之中。而这个isObjectRepresentableAsLongLong
现在在Redis并未被使用,取代它的是使用更加广泛的getLongLongFromObject
的函数。
int getLongLongFromObject(robj *o, long long *target);int getLongLongFromObjectOrReply(client *c, robj *o, long long *target, const char *msg);int getLongFromObjectOrReply(client *c, robj *o, long *target, const char *msg);
上述三个函数都是用于尝试从一个给定的robj
字符串对象中解码出一个整形数值,getLongLongFromObjectOrReply
和getLongFromObjectOrReply
在尝试解码失败的时候,会通知给对应的客户端client
。
浮点数值的解码
int getDoubleFromObject(const robj *o, double *target);int getDoubleFromObjectOrReply(client *c, robj *o, double *target, const char *msg);int getLongDoubleFromObject(robj *o, long double *target);int getLongDoubleFromObjectOrReply(client *c, robj *o, long double *target, const char *msg);
上述四个函数用于尝试从一个robj
对象中尝试解码浮点数数据。
Redis字符串对象的比较
int compareStringObjectsWithFlags(robj *a, robj *b, int flags);int compareStringObjects(robj *a, robj *b);int collateStringObjects(robj *a, robj *b);int equalStringObjects(robj *a, robj *b);
上面四个函数用于实现对两个字符串对象的比较操作,compareStringObjectsWithFlags
是比较操作的的基础实现,后面的三个函数都是基于compareStringObjectsWithFlags
这个函数进行的封装。
compareStringObjectsWithFlags
函数接口用来根据传入的flags
参数来调用memcpy
或者strcoll
来对底层数据进行比较。如果待比较的字符串对象是整数型的字符串,那么调用这个接口进行比较的话,会将数字转化为字符串,然后在通过memcpy
或者strcoll
进行比较。
Redis定义了两个比较的flags:
#define REDIS_COMPARE_BINARY (1 << 0)
#define REDIS_COMPARE_COLL (1 << 1)
上述这两个flags分别对应使用memcpy
和strcoll
进行检查,也就是对字符串对象进行二进制比较或者根据本地环境,基于字典顺序进行比较。
Redis字符串对象的其他操作
字符串对象的裁剪
void trimStringObjectIfNeeded(robj *o)
{if (o->encoding == OBJ_ENCODING_RAW && sdsavail(o->ptr) > sdslen(o->ptr) / 10) {
o->ptr = sdsRemoveFreeSpace(o->ptr);
}
}
从前面对于sds
介绍,我们知道,为了防止频繁的分配内存,sds
通常会多分配出一段内存以备后用。同时sds
中还提供sdsRemoveFreeSpace
接口用于释放空闲的内存。基于上述这两个特性,Redis定义了一个对于字符串对象的剪裁操作函数,用于优化对象的内存占用,当底层sds
中空闲内存的大小超过已被使用内存的10%的时候,那么会调用sdsRemoveFreeSpace
对sds
的空闲内存进行释放。
robj *tryObjectEncoding(robj *o);
而tryObjectEncoding
这个函数则是应用一组规则尝试对一个字符串对象的内存进行优化:其基本的逻辑为:
只有
OBJ_ENCODING_RAW
编码以及OBJ_ENCODING_EMBSTR
编码的字符串才有优化内存空间的必要,这一点可以通过调用sdsEncodedObject
函数来进行验证。对于共享对象的内存优化是不安全的,因为这个对象可能在整个Redis的内存空间的各个地方被引用,甚至有可能在其他地方正在处理中。对象是否被共享,可以通过
robj.refcount
字段来进行判断。检查该字符串是否可以表示一个长整形数,可以通过
len <= 20 && string2l(s, len, &value)
这个条件进行判断:如果
robj.encoding
为OBJ_ENCODING_RAW
,则释放底层sds
数据,将整数保存在robj.ptr
指针上。如果
robj.encoding
为OBJ_ENCODING_EMBSTR
,那么这种情况则需要释放整个对象,然后调用createStringObjectFromLongLongForValue
来创建一个新的OBJ_ENCODING_INT
编码的字符串对象。
如果解析出来整数处于Redis的共享整数对象
shared.integers
范围内,那么直接返回系统共享对象。如果解析出的整数数值不在共享整数对象:
如果没法编码成
OBJ_ENCODING_INT
对象,则会检查sds
数据的长度,如果小于OBJ_ENCODING_EMBSTR_SIZE_LIMIT
,那么通过createEmbeddedStringObject
函数将对象转化为OBJ_ENCODING_EMBSTR
对象。最后通过
trimStringObjectIfNeeded
函数,尝试对底层sds
数据所分配的内存进行进一步的优化。
字符串对象的其他操作
robj *getDecodedObject(robj *o);
getDecodeObject
这个函数只对字符串对象生效,会从传入参数所对应的对象返回一个新的对象,如果原始对象是一个字符串类型对象,那么会增加其引用计数,并将这个原始对象返回;当原始对象是一个整数型对象时,则会将这个整数转化为一个字符串形式,然后为其创建一个字符串类型的新对象。
robj *dupStringObject(const robj *o);
dupStringObject
这个函数会赋值一个字符串对象,并确保返回的对象与原始对象具有相同的编码类型。
size_t stringObjectLen(robj *o);
stringObjectLen
这个函数则用于获取一个字符串对象底层数据的大小。