Redis(六)redis对象介绍及部分源码解析

本文深入解析Redis中五种核心对象类型:字符串、列表、哈希、集合和有序集合,探讨其底层数据结构的选择机制,如压缩列表、双向链表、字典等,以及编码方式如何影响性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一.概述

      Redis中包含各种数据结构,但为了提高灵活性,针对不同的情况下选择合适的数据结构,Redis在数据结构之上增加了一层对象系统,其中包括五种对象类型:字符串对象,列表对象,哈希对象,集合对象和有序集合对象。每种对象可以根据情况自动选择和更改底层数据结构,以提高效率及空间利用率,比如:列表对象可以使用压缩列表实现,亦可使用双向链表实现。Redis中使用结构体redisObject来描述使用的对象类型及底层数据结构,如下所示。

typedef struct redisObject {
    // redis对象类型,占4位(字符串对象REDIS_STRING,列表对象REDIS_LIST或...)
    unsigned type:4;

    // 该对象采用何种类型的数据结构实现,或称为编码方式
    unsigned encoding:4;

    // 最后修改时间
    unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */

    // 引用计数
    int refcount;

    // 指向底层数据结构的指针
    void *ptr;
} robj;

   【注】:对于Redis数据库保存的键值对,键总是一个字符串对象,而值可以是五种对象中一种。例如:redis> RPUSH numbers 1 3 5 键为字符串对象,值为列表对象。redis> HMEST profile name Tom age 25  键为字符串对象(存储的是"profile"),值为哈希对象(存储的是[name Tome], [age 25]对)。

 

二.字符串对象(REDIS_STRING)

       字符串对象是唯一一种可以被嵌套在其它对象中的对象类型,跟据存储数据的类型及长度不同,它有3种数据结构(编码方式)存储数据,分别为整型编码,raw编码和embstr编码,下面具体说明使用场景。

1.整型编码(REDIS_ENCODING_INT)

      若存储的整型在Redis共享对象范围内,则增加共享对象引用计数并返回; 如果字符串对象中保存的是可以用long存储的整数,那么直接将void* 转换成long,这是因为指针和long型大小都占4字节其转化方式如下:

int main()
{
	void *ptr;
	*(long *)&ptr = 999; // 及先将保存指针的这块内存解读为long,再向其中写入数据
	std::cout << (long)ptr; // 输出为999
	return 0;
}

但一旦向该字符串对象中添加非整型数据或不可用long存储,则将转化为其它两种编码方式。

 

2.简单动态字符串raw编码(REDIS_ENCODING_RAW)

     当保存的是一个长度大于39字节的字符串,则使用SDS来保存这个字符串值,此时redisObject的ptr指向一个SDS结构。raw编码会调用两次内存分配函数来创建redisObject和SDS,即这两个结构的内存并不一定相连(【注】:后面要介绍的embstr编码会开辟一个足够大的内存来容纳redisObject和SDS,且包含字符串值,但这不便于字符串的更改与扩展)。

3.embstr编码的简单动态字符串(REDIS_ENCODING_EMBSTR)

     当保存的是一个长度小于等于39字节的字符串时,则使用embstr编码的SDS进行保存,如上一小节的注释所述,所谓embstr编码即将redisObject,SDS及字符串放在一个连续的内存中。但embstr编码保存的数据是只读的,一旦需要修改便会转化为raw编码。

4.embstr编码和raw编码的优缺点

  • 使用embstr编码创建字符串只需一次内存分配(释放),而raw编码则需要进行两次内存分配(释放)。
  • 由于embstr编码中的所有数据都在同一块内存中,因此可以更好的利用高速缓存带来的优势(【注】:这些数据更有可能在同一缓存行中,在访问redisObject时,SDS的数据恒很可能被同时读入缓存行,且与其它不相干数据处于同一缓存行的几率降低了,否则不相干数据可能会导致缓存行时效。)
  • raw编码下更适合对字符串数据进行修改。

【注】:对于浮点数及大整数(超过long)则亦使用embstr或raw编码进行存储,当使用时,如进行加法,则会先将字符串取出,再转化为数。

 

三.列表对象(REDIS_LIST)

     列表对象根据数据大小,及数据数量来决定是使用压缩列表(ziplist)或双向链表(linkedlist)作为底层实现。当每个数据元素所保存的数据长度小于64字节,且数据数量小于512个则使用压缩列表存储,否则使用双向链表。

1.压缩链表存储(REDIS_ENCODING_ZIPLIST)

     使用压缩链表做为底层数据结构实现,关于压缩链表参考博文《Redis(五)压缩列表》

2.双向链表存储(REDIS_ENCODING_LINKEDLIST)

     redisObjest指向一个双线链表的数据结构,其中链表中的每个节点都是一个字符串对象(REDIS_STRING),根据存储的数据不同,每个字符串对象再自己决定编码方式。

3.优缺点对比

  • 压缩列表更节约内存,且数据数量较少时,更利于同时载入到缓存行中,即cache miss更少,从而有着更高效的访问速率。
  • 双向链表更适于保存大量数据

 

四.哈希对象(REDIS_HASH)

     哈希对象根据亦根据数据元素的大小及数据元素的数量来决定使用压缩列表(ziplist)或字典(hashtable)

1.压缩链表存储(REDIS_ENCODING_ZIPLIST)

     使用压缩列表存储时,会将一个键值对存储在相邻的两个节点上,前一个节点存储键,后一个节点存储值,新添加的键值对会被放到压缩列表的尾部。

2.字典存储(REDIS_ENCODING_HT)

     Redis中的字典是使用哈希表实现的,当使用字典存储时,哈希对象中每个键值对都使用一个字典键值对进行存储,且此时字典的每个键和值都是一个字符串对象。

3.优缺点对比

    二者的优缺点与列表对象中使用压缩列表和双向链表的优缺点相同。

 

五.集合对象(REDIS_SET)

    集合对象根据元素的类型及数量决定采用整数集合还是哈希表做为底层实现,且集合对象种不存在重复值。

1.整数集合(REDIS_ENCODING_INTSET)

     采用整数集合有利于高效的存储整数,具体参《Redis(四)整数集合》

2.哈希表(REDIS_ENCODING_HT)

      当元素不为整数且数据量较大时,采用哈希表做为存储可以提高效率,此时的哈希表由于只需保存键,因此每个桶的值链表为空。

    

六.有序集合对象(REDIS_ZSET)

   有序集合对象根据保存的元素大小和数量决定采用压缩列表或跳跃表(还结合了字典以提高分值查找效率)做为底层实现。

1.压缩列表(REDIS_ENCODING_ZIPLIST)

     采用压缩列表存储时,相邻的一对数据保留一份有序集合数据,前一个保存成员,后一个保存分值。

2.跳跃表(与哈希表结合)(REDIS_ENCODING_SKIPLIST)

     当元素过多时有序集合对象会转而使用跳越表进行底层实现,因为跳跃表有较高效的数据查询能力,和范围查询能力(O(logn)),但在根据成员查询所对应的分值时哈希表的效率明显较高(O(1)),而字典(哈希表)以无序的方式保存数据,若要查询某一范围(分值)内的元素则效率较低。因此采用结合的方式效率更高,且哈希表和跳跃表种存储的分值与成员使用的都是同一块内存,并不会造成内存浪费。这两种结构被保存在一个数据结构zset中,zset的结构如下所示:

typedef struct zset{
    zskiplist *zsl; // 指向跳跃表
    dict *dict;     // 字典
} zset;

【综述】:

综合上述各种对象的编码方式,可以总结出:基本每种对象都有两种编码方式,一种是数据在内存中紧密连续的,而另一种是不连续的。紧密连续的结构便于一次性载入缓存行,提高读写速度,而不连续的适合存储大量数据。

七.对象共享

     当多个键都保存了值相同整数值的字符串对象时,将会共享该对象,并进行引用计数。当引用计数为0时销毁该对象。Redis只对包含整数值的字符值进行共享,是因为要共享一个对象必须保证两个对象要保存的值完全相同,整数值比较的时间复杂度为O(1),而字符串的时间复杂度为O(N),其它对象类型的时间复杂度亦不少,因此不进行共享。

    【问】:若两个键共享同一个值对象,那么当某个键要修改其值,那么将如何呢?是像操作系统那样采取写时复制吗?

八.部分源码

1.字符串对象的创建

   包括raw编码,embstr编码字符串对象的创建,整型编码字符串对象的创建,以及如何存储一个数值(long long, double)在字符串对象中。

robj *createObject(int type, void *ptr) {
    robj *o = zmalloc(sizeof(*o));
    o->type = type;
    o->encoding = REDIS_ENCODING_RAW;
    o->ptr = ptr;
    o->refcount = 1;
    o->lru = LRU_CLOCK();
    return o;
}

#define REDIS_SHARED_INTEGERS 10000
// 创建RAW编码的字符串对象
robj *createRawStringObject(char *ptr, size_t len) {
    // 调用sdsnewlen函数创建sds结构,之后调用createObject创建redisObject对象
    // 此时进行两次空间分配
    return createObject(REDIS_STRING,sdsnewlen(ptr,len));
}

// 创建embstr编码的字符串对象
robj *createEmbeddedStringObject(char *ptr, size_t len) {
    // 开辟embstr编码的字符串对象,redis对象和sds对象此时存储在一个连续的地址空间中
    // 因此只需要分配一次空间
    robj *o = zmalloc(sizeof(robj)+sizeof(struct sdshdr)+len+1); //+1是因为'\0'
    // sh指向sds结构起始位置
    struct sdshdr *sh = (void*)(o+1);

    // 初始化redisObject部分
    o->type = REDIS_STRING;
    o->encoding = REDIS_ENCODING_EMBSTR;
    o->ptr = sh+1;
    o->refcount = 1;
    o->lru = LRU_CLOCK();

    // 初始化sds字符串部分
    sh->len = len;
    sh->free = 0;
    if (ptr) {
        memcpy(sh->buf,ptr,len);
        sh->buf[len] = '\0';
    } else {
        memset(sh->buf,0,len+1);
    }
    return o;
}

#define REDIS_ENCODING_EMBSTR_SIZE_LIMIT 39
// 创建字符串对象
// - 若要保存的字符串值长度小于等于39则采用更为高效的embstr编码
// - 若大于39则采用RAW编码
robj *createStringObject(char *ptr, size_t len) {
    if (len <= REDIS_ENCODING_EMBSTR_SIZE_LIMIT)
        return createEmbeddedStringObject(ptr,len);
    else
        return createRawStringObject(ptr,len);
}

// 将数值存储在字符串对象中
robj *createStringObjectFromLongLong(long long value) {
    robj *o;
    // 若要保存的数值在Redis预先构建的共享对象范围内,则增加该共享对象的引用计数并返回该共享对象
    if (value >= 0 && value < REDIS_SHARED_INTEGERS) {
        incrRefCount(shared.integers[value]);
        o = shared.integers[value];
    } else {
	    // 若value可以使用long存储,即小于4字节,则直接使用指针空间存储该值(因为都是4字节)
        if (value >= LONG_MIN && value <= LONG_MAX) {
            o = createObject(REDIS_STRING, NULL);
            o->encoding = REDIS_ENCODING_INT;
            o->ptr = (void*)((long)value);
        } else {
            // 若大于long范围,则将value转换为字符串初始化一个sds,并创建raw编码的字符串对象
            o = createObject(REDIS_STRING,sdsfromlonglong(value));
        }
    }
    return o;
}

// 将long double存储于字符串对象中
robj *createStringObjectFromLongDouble(long double value, int humanfriendly) {
    char buf[256];
    int len;

    if (isinf(value)) {
        if (value > 0) {
            memcpy(buf,"inf",3);
            len = 3;
        } else {
            memcpy(buf,"-inf",4);
            len = 4;
        }
    } else if (humanfriendly) {
    	// 若humanfriendly参数非0则去除格式化后尾部的‘0’字符
        len = snprintf(buf,sizeof(buf),"%.17Lf", value);
        if (strchr(buf,'.') != NULL) {
            char *p = buf+len-1;
            while(*p == '0') {
                p--;
                len--;
            }
            if (*p == '.') len--;
        }
    } else {
    	// 否则保留snprintf的结果
        len = snprintf(buf,sizeof(buf),"%.17Lg", value);
    }
    return createStringObject(buf,len);
}

2.列表对象的创建

// 创建REDIS_ENCODING_LINKEDLIST编码的列表对象
robj *createListObject(void) {
	// 创建列表结构
    list *l = listCreate();
	
    // 创建列表对象,底层指向刚创建的列表结构
    robj *o = createObject(REDIS_LIST,l); 
	
    // 设置列表结构的释放函数(用于释放节点,首先减少引用计数,若为0则释放对象)
    listSetFreeMethod(l,decrRefCountVoid); 
    o->encoding = REDIS_ENCODING_LINKEDLIST; // 设置编码方式
    return o;
}

// 创建REDIS_ENCODING_ZIPLIST编码的列表对象
robj *createZiplistObject(void) {
    unsigned char *zl = ziplistNew(); 		// 创建一个空的压缩列表
    robj *o = createObject(REDIS_LIST,zl); 
    o->encoding = REDIS_ENCODING_ZIPLIST;
    return o;
}

【注】:其它对象的创建方法基本相同,此处不再赘述

3.列表对象的编码转换

    当向列表对象添加节点时,会检查编码方式,若为压缩列表编码,则进一步检查值大小列表长度,若超过了限定值(默认为64字节和512个)则转化为链表编码。

void listTypeTryConversion(robj *subject, robj *value) {
	// 若不是压缩列表编码则无需转换
    if (subject->encoding != REDIS_ENCODING_ZIPLIST) return;
	// 值所占存储大小是否超过了限定值,若是则进行编码转换
    if (sdsEncodedObject(value) &&
        sdslen(value->ptr) > server.list_max_ziplist_value)
            listTypeConvert(subject,REDIS_ENCODING_LINKEDLIST);
}

// 将列表对象subject转化为enc编码
// - 只有当enc为REDIS_ENCODING_LINKEDLIST时才进行转换,即只能转化为列表编码
void listTypeConvert(robj *subject, int enc) {
    listTypeIterator *li;
    listTypeEntry entry;
	// 断言检查是否为列表对象
    redisAssertWithInfo(NULL,subject,subject->type == REDIS_LIST);

	// 若目标编码是列表编码REDIS_ENCODING_LINKEDLIST,则进行转化
    if (enc == REDIS_ENCODING_LINKEDLIST) {
        list *l = listCreate(); // 创建一个空列表
        // 设置列表的节点释放函数:#define listSetFreeMethod(l,m) ((l)->free = (m))
        listSetFreeMethod(l,decrRefCountVoid);

        // 创建一个迭代器li,指向列表对象
        li = listTypeInitIterator(subject,0,REDIS_TAIL);
		// 使用迭代器遍历列表中的redis对象,并将其添加到新建列表l的末尾
        while (listTypeNext(li,&entry)) listAddNodeTail(l,listTypeGet(&entry));
		// 释放迭代器
        listTypeReleaseIterator(li);

		// 设置当前列表对象的编码方式
        subject->encoding = REDIS_ENCODING_LINKEDLIST;
        zfree(subject->ptr);// 释放列表对象中旧的底层数据结构(压缩列表)
        subject->ptr = l; // 使列表对象指向新的底层实现结构
    } else {
        redisPanic("Unsupported list conversion");
    }
}

void listTypePush(robj *subject, robj *value, int where) {
    /* Check if we need to convert the ziplist */
    // - 检查是否为压缩列表编码且数据大小或列表长度是否超过了限定值
    // - 若是则调用listTypeConvert函数转化为列表编码方式
    listTypeTryConversion(subject,value);
    if (subject->encoding == REDIS_ENCODING_ZIPLIST &&
        ziplistLen(subject->ptr) >= server.list_max_ziplist_entries)
            listTypeConvert(subject,REDIS_ENCODING_LINKEDLIST);

    // 执行添加
    if (subject->encoding == REDIS_ENCODING_ZIPLIST) {
       ...
    } else if (subject->encoding == REDIS_ENCODING_LINKEDLIST) {
        ...
    } else {
        redisPanic("Unknown list encoding");
    }
}

【注】:其它对象的编码转换类似,此处不再赘述。

【注】:各种对象进行编码转化的限制值存储在服务器对象RedisServer中,在redis.c的initServerConfig函数中设置默认值,用户可进行更改

一、综合实战—使用极轴追踪方式绘制信号灯 实战目标:利用对象捕捉追踪和极轴追踪功能创建信号灯图形 技术要点:结合两种追踪方式实现精确绘图,适用于工程制图中需要精确定位的场景 1. 切换至AutoCAD 操作步骤: 启动AutoCAD 2016软件 打开随书光盘中的素材文件 确认工作空间为"草图与注释"模式 2. 绘图设置 1)草图设置对话框 打开方式:通过"工具→绘图设置"菜单命令 功能定位:该对话框包含捕捉、追踪等核心绘图辅助功能设置 2)对象捕捉设置 关键配置: 启用对象捕捉(F3快捷键) 启用对象捕捉追踪(F11快捷键) 勾选端点、中心、圆心、象限点等常用捕捉模式 追踪原理:命令执行时悬停光标可显示追踪矢量,再次悬停可停止追踪 3)极轴追踪设置 参数设置: 启用极轴追踪功能 设置角度增量为45度 确认后退出对话框 3. 绘制信号灯 1)绘制圆形 执行命令:"绘图→圆→圆心、半径"命令 绘制过程: 使用对象捕捉追踪定位矩形中心作为圆心 输入半径值30并按Enter确认 通过象限点捕捉确保圆形位置准确 2)绘制直线 操作要点: 选择"绘图→直线"命令 捕捉矩形上边中点作为起点 捕捉圆的上象限点作为终点 按Enter结束当前直线命令 重复技巧: 按Enter可重复最近使用的直线命令 通过圆心捕捉和极轴追踪绘制放射状直线 最终形成完整的信号灯指示图案 3)完成绘制 验证要点: 检查所有直线是否准确连接圆心和象限点 确认极轴追踪的45度增量是否体现 保存绘图文件(快捷键Ctrl+S)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值