Redis对象与redisObject超详细分析源码层

目录
  • 一、对象
  • 二、对象的类型及编码
    • redisObject 结构体
  • 三、不同对象编码规则
  • 四、redisObject结构各字段使用范例
    • 4.1 类型检查(type字段)
    • 4.2 多态命令的实现(encoding)
    • 4.3 内存回收和共享对象(refcount)
    • 4.4 对象的空转时长(lru)
  • 五、对象在源码中的使用
    • 5.1 字符串对象
      • 5.1.1字符串对象创建
      • 5.1.2 字符串对象编码
      • 5.1.3 字符串对象解码
      • 5.1.4 redis对象引用计数及自动清理
  • 六、总结

以下内容是基于Redis 6.2.6 版本整理总结

一、对象

前面几篇文章,我们介绍了Redis用到的主要的数据结构,如:sds、list、dict、ziplist、skiplist、inset等。

但是,Redis并没有直接使用这些数据结构来实现key-value数据库,而是基于这些数据结构构建了一个对象系统。包括字符串对象、列表对象、哈希对象、集合对象和有序集合对象五种类型的对象。每种对象都使用了至少一种前面提到的数据结构。

通过对对象的区分,Redis可以在执行命令前判断该对象是否能够执行该条命令。为对象设置不同的数据结构实现,只要是为了提高效率。

二、对象的类型及编码

Redis使用对象来表示数据中的key和value,每当我们在Redis数据库中创建一个新的键值对时,至少会创建两个对象,一个作用域key,另一个作用于value。

举个栗子:set msg “hello world” 表示分别创建了一个字符串对象保存“msg”,另一个字符串对象保存“hello world”:

redisObject 结构体

Redis中的每个对象由 redisObject 结构体来描述,对象的类型、编码、内存回收、共享对象都需要redisObject的支持,redisObject 结构体定义如下:

#define LRU_BITS 24
typedef struct redisObject {
    unsigned type:4;   // 类型
    unsigned encoding:4; // 编码
    unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
                            * LFU data (least significant 8 bits frequency
                            * and most significant 16 bits access time). */
    int refcount;
    void *ptr;
} robj;

下面我们来看看每个字段的含义:

(1)type: 占4个比特位,表示对象的类型,有五种类型。当我们执行type命令时,便是通过type字段获取对象的类型。

/* The actual Redis Object */
#define OBJ_STRING 0    /* String object. */
#define OBJ_LIST 1      /* List object. */
#define OBJ_SET 2       /* Set object. */
#define OBJ_ZSET 3      /* Sorted set object. */
#define OBJ_HASH 4      /* Hash object. */

type命令使用示例:

(2)encoding: 占4个比特位,表示对象使用哪种编码,redis会根据不同的场景使用不同的编码,大大提高了redis的灵活性和效率。

字符串对象不同编码类型示例:

(3)lru: 占 24 个比特位,记录该对象最后一次被访问的时间。千万别以为这只能在LRU淘汰策略中才用,LFU也是复用的个字段。当使用LRU时,它保存的上次读写的24位unix时间戳(秒级);使用LFU时,24位会被分为两个部分,16位的分钟级时间戳和8位特殊计数器,这里就不展开了,详细可以注意我后续的文章。

(4)refcount: 对象的引用计数,类似于shared_ptr 智能指针的引用计数,当refcount为0时,释放该对象。

(5)ptr: 指向对象具体的底层实现的数据结构。

三、不同对象编码规则

四、redisObject结构各字段使用范例

Redis中操作key的命令大致可以分为两类:一种是可以操作任何类型的key,如:del type object等等,另外一种是针对特定类型的key只能使用特定的命令。如:LLEN只能用来获取列表对象的长度。

4.1 类型检查(type字段)

比如对于LLEN命令,Redis服务器在执行命令之前会先检查输入的key对应的的value对象是否为列表类型,即检查该value对象的type类型是不是OBJ_LIST,如果是才会执行LLEN命令。否则就拒绝执行命令并返回操作类型错误。

4.2 多态命令的实现(encoding)

Redis除了会根据value对象的类型来判断对应key能否执行执行命令外,还会根据value对象的**编码方式(encoding字段)**选择正确的方式来执行命令。比如:列表对象的编码方式有quicklist 和 ziplist两种,Redis服务器除了判断对应value对象的类型为列表对象还要根据具体的编码选择正确的LLEN执行。

借用面向对象的术语来说,可以认为LLEN命令是多态的。只要执行LLEN命令的列表键,无论value对象的编码是哪种方式,LLEN命令都可以正常执行。实际上del type 等也是多态命令。他们和LLEN的区别在于,前者是基于类型的多态,后者是基于编码的多态。

4.3 内存回收和共享对象(refcount)

C语言不具备自动回收功能,Redis就通过引用计数实现了自己的内存回收机制。具体是由redisObject结构中的refcount字段记录。对象的引用计数会随着对象的使用状态而不断变化。

创建一个新对象时,refcount会被初始化为1,;当对象被另一个新程序使用时 refcount加1;不被一个程序使用时减1;当refcount==0时,该对象所占的空间会被回收。

引用计数除了被用来实现内存回收外,还被用来实现对象共享。比如:

上面我们创建可一个value为100的key A,并使用object refcount来查看key A的引用计数,会看到此时的refcount为2,这是为什么呢?此时有两个地方引用了这个value对象,一个是持有该对象的服务器程序,另外一个是共享该value对象的key A。如果,我们再创建一个value为100 的 key B,那么键B也会指向这个value对象,使得该对象的引用计数变为3。由此,可以看到,共享value对象的键越多,节约的内存就越多。

在创建键B的时候,服务器考到键B要创建的对象是int编码的字符串对象100,而刚好有个value为100的共享对象匹配,就直接将键B指向该共享对象。因为是整数的字符串对象,直接比较即可,如果共享对象是字符串值的对象,要从头到尾比较每个字符,时间复杂度O(n)。

简单来说就是,要能使用共享对象,需要先验证该共享对象和要创建的目标对象是不是完全一致,如果共享对象保存的值越复杂,消耗的CPU也就越多,所以Redis值对整数类型的字符串对象做了共享。没有共享保存字符串值的字符串对象。

Redis在初始化服务器是,就创建了一万个字符串对象,这些对象包含了0~9999的所有整数值。当你创建了这个范围内的 字符串对象时,服务器就会使用这些共享对象,而不是创建新对象,以节约内存。

4.4 对象的空转时长(lru)

redisObject结构中的lru字段保存,该对象最后一次被访问的时间。 使用object idletime 来查看,注意这个命令不会修改对象的lru属性。

当Redis开启最大内存限制,一般为机器内存的一半,如果redis使用的内存达到这个值,且内存淘汰策略使用的是volatile-lru 或者 allkeys-lru,空转时长较高的那些键会被优先释放。

使用object idletime 查看键的空间时间,单位:秒:

127.0.0.1:6379[1]> keys *
1) "msg"
2) "teacher"
127.0.0.1:6379[1]> object idletime msg
(integer) 71166
127.0.0.1:6379[1]>

五、对象在源码中的使用

5.1 字符串对象

5.1.1字符串对象创建

// code location: src/object.c
#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44
// 创建 strintg 对象
robj *createStringObject(const char *ptr, size_t len) {
	// 如果待保存的字符串的长度小于等于44,使用 embstr 编码
    if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT)
        return createEmbeddedStringObject(ptr,len);
    else // 否则使用 raw 编码
        return createRawStringObject(ptr,len);
}
robj *createEmbeddedStringObject(const char *ptr, size_t len) {
	// 申请 robj + sdshdr + data + 1 的空间
    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;
    if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
        o->lru = (LFUGetTimeInMinutes()<<8) | LFU_INIT_VAL;
    } else {
        o->lru = LRU_CLOCK();
    }
    sh->len = len;
    sh->alloc = len;
    sh->flags = SDS_TYPE_8;
    if (ptr == SDS_NOINIT)
        sh->buf[len] = '\0';
    else if (ptr) {
        memcpy(sh->buf,ptr,len);
        sh->buf[len] = '\0';
    } else {
        memset(sh->buf,0,len+1);
    }
    return o;
}

从 createEmbeddedStringObject 函数可以看到,该对象是robj和sds的结合体,将sds直接放入到robj里,这也是嵌入式编码embstr的由来。

为什么要限制44字节呢?因为robj结构体占16个字节,sdshdr结构体占3个字节,最后结尾的‘\0’占一个字节,限制44个字节,就能保证64个字节里能放下所有内容(16+3+1+44 = 64)。

5.1.2 字符串对象编码

Redis将节省内存做到了极致,它的作者对字符串对象又做了特殊的编码处理,以进一步达到节省空间的目的。编码处理过程及代码注释如下:

/* Try to encode a string object in order to save space */
robj *tryObjectEncoding(robj *o) {
    long value;
    sds s = o->ptr;
    size_t len;
    /* Make sure this is a string object, the only type we encode
     * in this function. Other types use encoded memory efficient
     * representations but are handled by the commands implementing
     * the type. */
    // 这里只对string对象进行编码,其他类型的编码都有对应的具体实现处理
    serverAssertWithInfo(NULL,o,o->type == OBJ_STRING);
    /* We try some specialized encoding only for objects that are
     * RAW or EMBSTR encoded, in other words objects that are still
     * in represented by an actually array of chars. */
    // 非sds string对象,直接返回原对象
    if (!sdsEncodedObject(o)) return o;
    /* It's not safe to encode shared objects: shared objects can be shared
     * everywhere in the "object space" of Redis and may end in places where
     * they are not handled. We handle them only as values in the keyspace. */
    // 如果该对象由其他对象共享,不能编码,如果编码可能影响到其他对象的使用
     if (o->refcount > 1) return o;
    /* Check if we can represent this string as a long integer.
     * Note that we are sure that a string larger than 20 chars is not
     * representable as a 32 nor 64 bit integer. */
    // 检查能否把一个字符串表示为长整型数,长度要小于等于20
    len = sdslen(s);
    if (len <= 20 && string2l(s,len,&value)) {
        /* This object is encodable as a long. Try to use a shared object.
         * Note that we avoid using shared integers when maxmemory is used
         * because every object needs to have a private LRU field for the LRU
         * algorithm to work well. */
         // 如果可以被编码为long型,且编码后的值小于OBJ_SHARED_INTEGERS(10000),且未配
         // 置LRU替换淘汰策略, 就使用这个数的共享对象,相当于所有小于10000的数都是用的同一个robj
        if ((server.maxmemory == 0 ||
            !(server.maxmemory_policy & MAXMEMORY_FLAG_NO_SHARED_INTEGERS)) &&
            value >= 0 &&
            value < OBJ_SHARED_INTEGERS)
        {
            decrRefCount(o);
            incrRefCount(shared.integers[value]);
            return shared.integers[value];
        } else {
            if (o->encoding == OBJ_ENCODING_RAW) {
                sdsfree(o->ptr);
                o->encoding = OBJ_ENCODING_INT;
                o->ptr = (void*) value;
                return o;
            } else if (o->encoding == OBJ_ENCODING_EMBSTR) {
                decrRefCount(o);
                return createStringObjectFromLongLongForValue(value);
            }
        }
    }
    // 不能转为long的字符串
    /* If the string is small and is still RAW encoded,
     * try the EMBSTR encoding which is more efficient.
     * In this representation the object and the SDS string are allocated
     * in the same chunk of memory to save space and cache misses. */
    // 如果字符串的长度太小,小于等于44
    if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT) {
        robj *emb;
        // 如果当前编码是embstr,直接返回原对象,否则转为embstr编码,返回
        if (o->encoding == OBJ_ENCODING_EMBSTR) return o;
        emb = createEmbeddedStringObject(s,sdslen(s));
        decrRefCount(o);
        return emb;
    }
    /* We can't encode the object...
     *
     * Do the last try, and at least optimize the SDS string inside
     * the string object to require little space, in case there
     * is more than 10% of free space at the end of the SDS string.
     *
     * We do that only for relatively large strings as this branch
     * is only entered if the length of the string is greater than
     * OBJ_ENCODING_EMBSTR_SIZE_LIMIT. */
    // 如果前面编码没有成功,这里做最后一步,当编码类型为raw,且它的sds可用空间超过10%,
    // 尝试释放调这部分内存
    trimStringObjectIfNeeded(o);
    /* Return the original object. */
    // 返回原对象
    return o;
}

5.1.3 字符串对象解码

有编码就有解码,实际上只需要那些可以转为整型类型的字符串传进行解码,解码代码及注释如下:

robj *getDecodedObject(robj *o) {
    robj *dec;
	// 如果编码是 embstr 和 raw ,只是把引用计数加一,然后返回原对象
    if (sdsEncodedObject(o)) {
        incrRefCount(o);
        return o;
    }
    // 如果编码是 int 进行解码,返回新的对象
    if (o->type == OBJ_STRING && o->encoding == OBJ_ENCODING_INT) {
        char buf[32];
        ll2string(buf,32,(long)o->ptr);
        dec = createStringObject(buf,strlen(buf));
        return dec;
    } else {
        serverPanic("Unknown encoding type");
    }
}

5.1.4 redis对象引用计数及自动清理

void incrRefCount(robj *o) {
    if (o->refcount < OBJ_FIRST_SPECIAL_REFCOUNT) {
        o->refcount++; // 引用计数加一
    } else {
        if (o->refcount == OBJ_SHARED_REFCOUNT) {
            /* Nothing to do: this refcount is immutable. */
        } else if (o->refcount == OBJ_STATIC_REFCOUNT) {
            serverPanic("You tried to retain an object allocated in the stack");
        }
    }
}
// 减少引用计数
void decrRefCount(robj *o) {
	// 释放空间
    if (o->refcount == 1) {
        switch(o->type) {
        case OBJ_STRING: freeStringObject(o); break;
        case OBJ_LIST: freeListObject(o); break;
        case OBJ_SET: freeSetObject(o); break;
        case OBJ_ZSET: freeZsetObject(o); break;
        case OBJ_HASH: freeHashObject(o); break;
        case OBJ_MODULE: freeModuleObject(o); break;
        case OBJ_STREAM: freeStreamObject(o); break;
        default: serverPanic("Unknown object type"); break;
        }
        zfree(o);
    } else {
        if (o->refcount <= 0) serverPanic("decrRefCount against refcount <= 0");
        if (o->refcount != OBJ_SHARED_REFCOUNT) o->refcount--; // 计数减一
    }
}

六、总结

  • redis对象为所有类型的value提供了统一的封装
  • 为对象的淘汰策略保存相关信息
  • 实现引用计数及内存自动释放功能

到此这篇关于Redis对象与redisObject超详细分析源码层的文章就介绍到这了,更多相关Redis对象与redisObject内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Redis如何存储对象

    目录 Redis存储对象 首先引入依赖 创建一个Jedis实例 方式一:将对象转化为JSON字符串存入redis 方式二:将对象序列化后存到redis 方式三:将对象用Hash数据类型存储 Redis可以存储java对象吗 答案:是可以 Redis存储对象 在以往面试中,多次被问到Redis怎么存储对象,我支支吾吾半天回答不上来,毕竟我只用过String类型,为了避免以后的尴尬,今天花点时间来整理一下. 本人用的Jedis客户端 首先引入依赖         <dependency>    

  • redis如何实现保存对象

    目录 redis保存对象 redis数据结构 保存对象 redis存放对象的两种方式 数据格式 方式一(String) 方式二(hash) redis保存对象 redis数据结构 String——字符串 Hash——字典 List——列表 Set——集合 Sorted Set——有序集合 redisTemplate.opsForValue();//操作字符串 redisTemplate.opsForHash();//操作hash redisTemplate.opsForList();//操作li

  • redis 存储对象的方法对比分析

    redis 存储对象的方法对比 问题背景: 原来项目里面全部是直接redis存储对象的json数据,需要频繁的序列化和反序列化,后来考虑更换项目中的redis存储对象为hash对象存储的,但是获取后不能方便的set get操作,很是蛋疼,怎么才能解决这个问题呢? 1.1 直接存储对象的json 存放redis的时候,直接先用fastJson 或者 jackJson或者Gson把对象序列化为json数据,然后用直接存放,key表示用户id或许和openid,value则是对象的json数据 pub

  • Redis对象与redisObject超详细分析源码层

    目录 一.对象 二.对象的类型及编码 redisObject 结构体 三.不同对象编码规则 四.redisObject结构各字段使用范例 4.1 类型检查(type字段) 4.2 多态命令的实现(encoding) 4.3 内存回收和共享对象(refcount) 4.4 对象的空转时长(lru) 五.对象在源码中的使用 5.1 字符串对象 5.1.1字符串对象创建 5.1.2 字符串对象编码 5.1.3 字符串对象解码 5.1.4 redis对象引用计数及自动清理 六.总结 以下内容是基于Red

  • Spring Boot 员工管理系统超详细教程(源码分享)

    员工管理系统 1.准备工作 资料下载 内含源码 + 笔记 + web素材 源码下载地址: http://xiazai.jb51.net/202105/yuanma/javaguanli_jb51.rar 笔记 素材 源码 1.1.导入资源 将文件夹中的静态资源导入idea中 位置如下 1.2.编写pojo层 员工表 //员工表 @Data @NoArgsConstructor public class Employee { private Integer id; private String l

  • Android Studio做超好玩的拼图游戏 附送详细注释源码

    目录 一.项目概述 二.开发环境 三.需求分析 四.实现过程 1.拼图游戏布局绘制 2.拼图游戏时间计时 3.拼图游戏打乱显示 4.拼图游戏碎片位置切换 5.拼图游戏成功的条件 6.拼图游戏重新开始 五.运行效果 六.项目总结 七.项目源码 一.项目概述 之前有不少粉丝私信我说,能不能用Android原生的语言开发一款在手机上运行的游戏呢? 说实话,使用java语言直接开发游戏这个需求有点难,因为一些比较复杂的游戏都是通过cocos2D或者Unity3D等游戏引擎开发出来的,然后再移植到Andr

  • Android用于加载xml的LayoutInflater源码超详细分析

    1.在view的加载和绘制流程中:文章链接 我们知道,定义在layout.xml布局中的view是通过LayoutInflate加载并解析成Java中对应的View对象的.那么具体的解析过程是哪样的. 先看onCreate方法,如果我们的Activity是继承自AppCompactActivity.android是通过getDelegate返回的对象setContentView,这个mDelegate 是AppCompatDelegateImpl的实例. @Override protected

  • Java CopyOnWriteArrayList源码超详细分析

    目录 一.概述 二.类图 三.核心方法 1.add() 2.set() 3.remove() 4.get() 5.size() 四.总结 一.概述 CopyOnWriteArrayList是基于写时复制技术实现的,适用于读多写少场景下的线程安全的并发容器.读操作永远不会加锁,读读.读写都不会冲突,只有写写需要等待.写操作时,为了不影响其它线程的读取,它会进行一次自我复制,待数据写入完成后再替换array数组.array数组是被volatile修饰的,它被修改后可以被其他线程立刻发现. publi

  • Redis超详细分析分布式锁

    目录 分布式锁 应用场景 使用Redis 实现分布式锁 单机版Redis实现分布式锁 使用原生Jedis实现 使用Springboot实现 分布式锁 为了保证一个方法在高并发情况下的同一时间只能被同一个线程执行,在传统单体应用单机部署的情况下,可以使用Java并发处理相关的API(如ReentrantLcok或synchronized)进行互斥控制.但是,随着业务发展的需要,原单体单机部署的系统被演化成分布式系统后,由于分布式系统多线程.多进程并且分布在不同机器上,这将使原单机部署情况下的并发控

  • React超详细分析useState与useReducer源码

    目录 热身准备 为什么会有hooks hooks执行时机 两套hooks hooks存储 初始化 mount useState mountWorkInProgressHook 更新update updateState updateReducer updateWorkInProgressHook 总结 热身准备 在正式讲useState,我们先热热身,了解下必备知识. 为什么会有hooks 大家都知道hooks是在函数组件的产物.之前class组件为什么没有出现hooks这种东西呢? 答案很简单,

  • Java超详细分析泛型与通配符

    目录 1.泛型 1.1泛型的用法 1.1.1泛型的概念 1.1.2泛型类 1.1.3类型推导 1.2裸类型 1.3擦除机制 1.3.1关于泛型数组 1.3.2泛型的编译与擦除 1.4泛型的上界 1.4.1泛型的上界 1.4.2特殊的泛型上界 1.4.3泛型方法 1.4.4类型推导 2.通配符 2.1通配符的概念 2.2通配符的上界 2.3通配符的下界 题外话: 泛型与通配符是Java语法中比较难懂的两个语法,学习泛型和通配符的主要目的是能够看懂源码,实际使用的不多. 1.泛型 1.1泛型的用法

  • Spring Boot超详细分析启动流程

    目录 一.Spring Boot 工程结构 二.Spring Boot 启动流程 三.Spring Boot 启动流程源码剖析 1.创建一个Spring Boot 工程 2.SpringBootApplication启动入口 3.Spring Boot 初始化分析 4.Spring Boot 启动深入分析 四.总结 一.Spring Boot 工程结构 下载Spring Boot工程源码, 下载地址 模块代码结构: 比较重要的是Spring-boot.Spring-boot-autoconfig

  • SpringBoot整合Redis实现常用功能超详细过程

    目录 1 登陆功能 1.1 基于Session实现登录流程 1.1.1 session共享问题 1.2 Redis替代Session 1.2.1.设计key的结构 1.2.2.设计key的具体细节 1.2.3.整体访问流程 2 缓存功能 2.1 什么是缓存? 2.1.1 为什么要使用缓存 2.1.2 如何使用缓存 2.2.使用缓存 2.2.1 .缓存模型和思路 2.3 缓存更新策略 2.3.1 .数据库缓存不一致解决方案: 2.3.2 .数据库和缓存不一致采用什么方案 2.4 缓存穿透问题的解决

随机推荐