redis存储对象_Redis源码学习(16)Redis中字符串对象类型实现(上)

对于Redis中字符串对象的类型的代码主要分布在两个文件之中,其中在src/object.c文件中主要是实现了字符串数据类型的构造相关的操作,另外在src/t_string.c文件中则实现了字符串的相关命令。

对于Redis中的字符串对象,可以使用三种编码类型,分别是:

  1. OBJ_ENCODING_RAW

  2. OBJ_ENCODING_INT

  3. 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对于字符串型的字符串对象定义了两种编码方式:

  1. 如果使用的是OBJ_ENCODING_RAW这种编码方式,那么表示对象的robj与表示底层数据的sds在内存分布上是相互分离的,通过robj.ptr指针指向sds数据,来实现对象对数据的引用。

    595b77f64ba50ccb1a134fac074f8062.png

  2. 如果使用的是OBJ_ENCODING_EMBSTR这种编码方式,那么对象robj数据与底层数据的sds是处于一个连续的内存块中的,sds数据存储在robj数据之后。

    e881cd9cf2f354a4c33be5945345fc39.png

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);

其中createRawStringObjectcreateEmbeddedStringObject函数用于创建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 10000Redis会在启动的时候自动创建这么一组整数形式的字符串对象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则会返回系统创建的共享对象。其具体的执行步骤为:

  1. 判断value以及valueobj的数值,如果满足value >= 0 && value < OBJ_SHARED_INTEGERS && valueobj == 0,那么直接从shared.integers中返回共享对象。

  2. 如果不满足1中的条件,那么判断value >= LONG_MIN && value <= LONG_MAX,如果满足这种情况,那么使用OBJ_ENCODING_INT编码方式创建一个字符串对象,并使用o->ptr = (void *)((long)value);value存储在robj.ptr指针上。

  3. 如果上述两个条件都不满足,那么Redis会通过sdsfromlonglong创建一个sds数据,并使用这个数据创建一个字符串对象。

createStringObjectFromLongLongcreateStringObjectFromLongLongForValue则是对于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字符串对象中解码出一个整形数值,getLongLongFromObjectOrReplygetLongFromObjectOrReply在尝试解码失败的时候,会通知给对应的客户端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分别对应使用memcpystrcoll进行检查,也就是对字符串对象进行二进制比较或者根据本地环境,基于字典顺序进行比较。

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%的时候,那么会调用sdsRemoveFreeSpacesds的空闲内存进行释放。

robj *tryObjectEncoding(robj *o);

tryObjectEncoding这个函数则是应用一组规则尝试对一个字符串对象的内存进行优化:其基本的逻辑为:

  1. 只有OBJ_ENCODING_RAW编码以及OBJ_ENCODING_EMBSTR编码的字符串才有优化内存空间的必要,这一点可以通过调用sdsEncodedObject函数来进行验证。

  2. 对于共享对象的内存优化是不安全的,因为这个对象可能在整个Redis的内存空间的各个地方被引用,甚至有可能在其他地方正在处理中。对象是否被共享,可以通过robj.refcount字段来进行判断。

  3. 检查该字符串是否可以表示一个长整形数,可以通过len <= 20 && string2l(s, len, &value)这个条件进行判断:

    1. 如果robj.encodingOBJ_ENCODING_RAW,则释放底层sds数据,将整数保存在robj.ptr指针上。

    2. 如果robj.encodingOBJ_ENCODING_EMBSTR,那么这种情况则需要释放整个对象,然后调用createStringObjectFromLongLongForValue来创建一个新的OBJ_ENCODING_INT编码的字符串对象。

    1. 如果解析出来整数处于Redis的共享整数对象shared.integers范围内,那么直接返回系统共享对象。

    2. 如果解析出的整数数值不在共享整数对象:

  4. 如果没法编码成OBJ_ENCODING_INT对象,则会检查sds数据的长度,如果小于OBJ_ENCODING_EMBSTR_SIZE_LIMIT,那么通过createEmbeddedStringObject函数将对象转化为OBJ_ENCODING_EMBSTR对象。

  5. 最后通过trimStringObjectIfNeeded函数,尝试对底层sds数据所分配的内存进行进一步的优化。

字符串对象的其他操作
robj *getDecodedObject(robj *o);

getDecodeObject这个函数只对字符串对象生效,会从传入参数所对应的对象返回一个新的对象,如果原始对象是一个字符串类型对象,那么会增加其引用计数,并将这个原始对象返回;当原始对象是一个整数型对象时,则会将这个整数转化为一个字符串形式,然后为其创建一个字符串类型的新对象。

robj *dupStringObject(const robj *o);

dupStringObject这个函数会赋值一个字符串对象,并确保返回的对象与原始对象具有相同的编码类型。

size_t stringObjectLen(robj *o);

stringObjectLen这个函数则用于获取一个字符串对象底层数据的大小。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值