Redis源码剖析(八)--对象系统

对象的类型与编码

在 Redis 中新创建一个键值对时, 我们至少会创建两个对象, 一个对象用作键值对的键(键对象), 另一个对象用作键值对的值(值对象)。Redis 中的每个对象都由一个 redisObject 结构表示:

typedef struct redisObject {

    // 类型
    unsigned type:4;

    // 编码
    unsigned encoding:4;

    // 对象最后一次被访问的时间
    unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */

    // 引用计数
    int refcount;

    // 指向实际值的指针
    void *ptr;

} robj;

对象类型

对象的type属性记录了对象的类型,type属性的值有以下几种:


类型 


对象名称


REDIS_STRING


字符串对象


REDIS_LIST


列表对象


REDIS_HASH


哈希对象


REDIS_SET


集合对象


REDIS_ZSET


有序集合对象

编码与底层实现

对象的encoding属性记录了对象使用的编码,即底层使用的数据结构,encoding的值有以下几种:


编码常量


底层数据结构


REDIS_ENCODING_INT


long 类型的整数


REDIS_ENCODING_EMBSTR


embstr 编码的简单动态字符串


REDIS_ENCODING_RAW


简单动态字符串


REDIS_ENCODING_HT


字典


REDIS_ENCODING_LINKEDLIST


双端链表


REDIS_ENCODING_ZIPLIST


压缩列表


REDIS_ENCODING_INTSET


整数集合


REDIS_ENCODING_SKIPLIST


跳跃表


字符串对象

值对象编码

字符串对象的编码可以是 int 、 raw 或者 embstr

如果字符串对象保存的是整数值, 那么这个整数值保存在 ptr属性(将 void* 转换成 long ) ,对象编码为int

如果字符串对象保存的是字符串值, 并且字符串长度大于 39 字节, 那么该字符串值使用简单动态字符串(SDS)来保存, 对象编码为 raw 。

如果字符串对象保存的字符串值, 并且字符串长度小于等于 39 字节, 那么字符串对象将使用 embstr 编码来保存字符串值。

字符串对象编码的部分代码如下:

robj *tryObjectEncoding(robj *o) {
    long value;
    sds s = o->ptr;
    size_t len;

    // 对字符串进行检查
    // 只对长度小于或等于 21 字节,并且可以被解释为整数的字符串进行编码
    len = sdslen(s);
    if (len <= 21 && string2l(s,len,&value)) {
    ......      o->ptr = (void*) value;      .......
    }

    // 尝试将 RAW 编码的字符串编码为 EMBSTR 编码,#define REDIS_ENCODING_EMBSTR_SIZE_LIMIT 39

    if (len <= REDIS_ENCODING_EMBSTR_SIZE_LIMIT) {
    ......
    }

    // 这个对象没办法进行编码,尝试从 SDS 中移除所有空余空间
    if (o->encoding == REDIS_ENCODING_RAW &&
        sdsavail(s) > len/10)
    {
        o->ptr = sdsRemoveFreeSpace(o->ptr);
    }
    return o;
}

EMBSTR编码时,直接申请一段连续的空间,空间中依次包含 redisObject 和 sdshdr 两个结构:

robj *createEmbeddedStringObject(char *ptr, size_t len) {
    robj *o = zmalloc(sizeof(robj)+sizeof(struct sdshdr)+len+1);
    ......
    return o;
}

编码的转换

当我们对 embstr 编码的字符串对象执行任何修改命令时, 程序会先将对象的编码从 embstr 转换成 raw , 然后再执行修改命令。下面给出APPEND命令部分代码:

void appendCommand(redisClient *c) {
    size_t totlen;
    robj *o, *append;

    // 取出键相应的值对象
    o = lookupKeyWrite(c->db,c->argv[1]);

    // 检查追加操作之后,字符串的长度是否符合 Redis 的限制
    append = c->argv[2];
    totlen = stringObjectLen(o)+sdslen(append->ptr);
    if (checkStringLength(c,totlen) != REDIS_OK)
            return;

    // 执行追加操作,此处会进行编码的转换
    o = dbUnshareStringValue(c->db,c->argv[1],o);
    o->ptr = sdscatlen(o->ptr,append->ptr,sdslen(append->ptr));
    totlen = sdslen(o->ptr);
}

dbUnshareStringValue函数实现:

robj *dbUnshareStringValue(redisDb *db, robj *key, robj *o) {
    redisAssert(o->type == REDIS_STRING);
    if (o->refcount != 1 || o->encoding != REDIS_ENCODING_RAW) {
        robj *decoded = getDecodedObject(o);
        //创建raw编码对象
        o = createRawStringObject(decoded->ptr, sdslen(decoded->ptr));
        decrRefCount(decoded);
        dbOverwrite(db,key,o);
    }
    return o;
}

列表对象

编码转换

列表对象的编码可以是 ziplist 或者 linkedlist

列表对象使用 ziplist 编码:

  1. 列表对象保存的所有字符串元素的长度都小于 64 字节
  2. 列表对象保存的元素数量小于 512 个

不能满足这两个条件的列表对象需要使用 linkedlist 编码。以上两个条件的上限值可以通过配置文件中的list-max-ziplist-value 和 list-max-ziplist-entries 选项进行修改。

ziplist编码的列表对象:

linkedlist编码的列表对象:

编码转换的部分代码如下:

void listTypeTryConversion(robj *subject, robj *value) {if (sdsEncodedObject(value) &&
        // 看字符串是否过长
        sdslen(value->ptr) > server.list_max_ziplist_value)
            // 将编码转换为双端链表
            listTypeConvert(subject,REDIS_ENCODING_LINKEDLIST);
}

// PUSH操作
void listTypePush(robj *subject, robj *value, int where) {

    // 是否需要转换编码?
    listTypeTryConversion(subject,value);

    if (subject->encoding == REDIS_ENCODING_ZIPLIST &&
        ziplistLen(subject->ptr) >= server.list_max_ziplist_entries)
            listTypeConvert(subject,REDIS_ENCODING_LINKEDLIST);

    // ZIPLIST
    if (subject->encoding == REDIS_ENCODING_ZIPLIST) {
        int pos = (where == REDIS_HEAD) ? ZIPLIST_HEAD : ZIPLIST_TAIL;
        // 取出对象的值,因为 ZIPLIST 只能保存字符串或整数
        value = getDecodedObject(value);
        subject->ptr = ziplistPush(subject->ptr,value->ptr,sdslen(value->ptr),pos);
        decrRefCount(value);

    // 双端链表
    } else if (subject->encoding == REDIS_ENCODING_LINKEDLIST) {
        if (where == REDIS_HEAD) {
            listAddNodeHead(subject->ptr,value);
        } else {
            listAddNodeTail(subject->ptr,value);
        }
        incrRefCount(value);

    // 未知编码
    } else {
        redisPanic("Unknown list encoding");
    }
}

阻塞行为

待填坑


哈希对象

哈希对象的编码可以是 ziplist 或者 hashtable 。

当哈希对象可以同时满足以下两个条件时, 哈希对象使用 ziplist 编码:

  1. 哈希对象保存的所有键值对的键和值的字符串长度都小于 64 字节
  2. 哈希对象保存的键值对数量小于 512 个

不能满足这两个条件的哈希对象需要使用 hashtable 编码。以上两个条件的上限值可以通过配置文件中的 hash-max-ziplist-value 和  hash-max-ziplist-entries 选项进行修改。

ziplist编码的哈希对象:

另一方面, hashtable 编码的哈希对象使用字典作为底层实现, 哈希对象中的每个键值对都使用一个字典键值对来保存:

  • 字典的每个都是一个字符串对象
  • 字典的每个都是一个字符串对象

hashtable编码的哈希对象:


集合对象

集合对象的编码可以是 intset 或者 hashtable 

当集合对象可以同时满足以下两个条件时, 对象使用 intset 编码:

  1. 集合对象保存的所有元素都是整数值
  2. 集合对象保存的元素数量不超过 512 个

不能满足这两个条件的集合对象需要使用 hashtable 编码。元素数量的上限值是可以通过配置文件 set-max-intset-entries 选项进行修改。

intset编码的集合对象:

hashtable编码的集合对象


有序集合对象

有序集合的编码可以是 ziplist 或者 skiplist 

当使用ziplist编码作为有序集合对象底层实现时,每个集合元素使用两个压缩列表节点来保存, 第一个节点保存元素的成员(member), 而第二个元素则保存元素的分值(score)。压缩列表内的集合元素按分值从小到大进行排序。

ziplist编码的有序集合对象如下:

skiplist 编码的有序集合对象使用 zset 结构作为底层实现:

typedef struct zset {

    zskiplist *zsl;

    dict *dict;

} zset;

zset 结构中的 zsl 跳跃表按分值从小到大保存了所有集合元素: 跳跃表节点的 object 属性保存了元素的成员,  score 属性则保存了元素的分值。 ZRANK、 ZRANGE 等命令就是基于跳跃表 API 来实现的。

除此之外, zset 结构中的 dict 字典为有序集合创建了一个从成员到分值的映射: 字典的保存了元素的成员, 而字典的则保存了元素的分值。 通过字典, 可以用 O(1) 复杂度查找给定成员的分值。

skiplist编码的有序集合对象如下:

这两种数据结构都会通过指针来共享相同元素的成员和分值,不会因此而浪费额外的内存。


内存回收

Redis 使用引用计数技术实现的内存回收机制。引用计数信息由 redisObject 结构的 refcount 属性记录。

以list的PUSH操作为例:

void listTypePush(robj *subject, robj *value, int where) {
    ......
    // ZIPLIST
    if (subject->encoding == REDIS_ENCODING_ZIPLIST) {
        ......
        decrRefCount(value);

    // 双端链表
    } else if (subject->encoding == REDIS_ENCODING_LINKEDLIST) {
        ......
        incrRefCount(value);
    }
}
incrRefCount的实现,当引用计数为0时会释放对象内存:
void decrRefCount(robj *o) {

    if (o->refcount <= 0) redisPanic("decrRefCount against refcount <= 0");

    // 释放对象
    if (o->refcount == 1) {
        switch(o->type) {
        case REDIS_STRING: freeStringObject(o); break;
        case REDIS_LIST: freeListObject(o); break;
        case REDIS_SET: freeSetObject(o); break;
        case REDIS_ZSET: freeZsetObject(o); break;
        case REDIS_HASH: freeHashObject(o); break;
        default: redisPanic("Unknown object type"); break;
        }
        zfree(o);

    // 减少计数
    } else {
        o->refcount--;
    }
}

对象的空转时长

redisObject 结构中的 lru 属性记录了对象最后一次被命令程序访问的时间。可以通过OBJECT IDLETIME 命令可以打印出给定键的空转时长。

除了可以被 OBJECT IDLETIME 命令打印出来之外, 键的空转时长还有另外一项作用: 如果服务器打开了 maxmemory 选项, 并且服务器用于回收内存的算法为 volatile-lru 或者 allkeys-lru , 那么当服务器占用的内存数超过了 maxmemory 选项所设置的上限值时, 空转时长较高的那部分键会优先被服务器释放, 从而回收内存。



Redis源码剖析(八)--对象系统

原文地址:https://www.cnblogs.com/lizhimin123/p/10167867.html

时间: 2024-08-24 18:04:25

Redis源码剖析(八)--对象系统的相关文章

【Redis源码剖析】 - Redis之数据库redisDb

原创作品,转载请标明:http://blog.csdn.net/xiejingfa/article/details/51321282 今天,我们来讨论两点内容:一是Redis是如何存储类型对象的,二是Redis如何实现键的过期操作. 本文介绍的内容主要涉及db.c和redis.h两个文件. 1.redisDb介绍 Redis中存在"数据库"的概念,该结构由redis.h中的redisDb定义.我们知道Redis提供string.list.set.zset.hash五种数据类型的存储,在

【Redis源码剖析】 - Redis内置数据结构值压缩字典zipmap

原创作品,转载请标明:http://blog.csdn.net/Xiejingfa/article/details/51111230 今天为大家带来Redis中zipmap数据结构的分析,该结构定义在zipmap.h和zipmap.c文件中.我把zipmap称作"压缩字典"(不知道这样称呼正不正确)是因为zipmap利用字符串实现了一个简单的hash_table结构,又通过固定的字节表示节省空间.zipmap和前面介绍的ziplist结构十分类似,我们可以对比地进行学习: Redis中

Redis源码剖析和注释(八)--- 对象系统(redisObject)

Redis 对象系统 1. 介绍 redis中基于双端链表.简单动态字符串(sds).字典.跳跃表.整数集合.压缩列表.快速列表等等数据结构实现了一个对象系统,并且实现了5种不同的对象,每种对象都使用了至少一种前面的数据结构,优化对象在不同场合下的使用效率. 双端链表源码剖析和注释 简单动态字符串(SDS)源码剖析和注释 字典结构源码剖析和注释 跳跃表源码剖析和注释 整数集合源码剖析和注释 压缩列表源码剖析和注释 快速列表源码剖析和注释 2. 对象的系统的实现 redis 3.2版本.所有注释在

Redis源码剖析和注释(十八)--- Redis AOF持久化机制

Redis AOF持久化机制 1. AOF持久化介绍 Redis中支持RDB和AOF这两种持久化机制,目的都是避免因进程退出,造成的数据丢失问题. RDB持久化:把当前进程数据生成时间点快照(point-in-time snapshot)保存到硬盘的过程,避免数据意外丢失. AOF持久化:以独立日志的方式记录每次写命令,重启时在重新执行AOF文件中的命令达到恢复数据的目的. Redis RDB持久化机制源码剖析和注释 AOF的使用:在redis.conf配置文件中,将appendonly设置为y

Django Rest Framework源码剖析(八)-----视图与路由

一.简介 django rest framework 给我们带来了很多组件,除了认证.权限.序列化...其中一个重要组件就是视图,一般视图是和路由配合使用,这种方式给我们提供了更灵活的使用方法,对于使用者而言不同的视图具有不同的功能,这样我们可以根据需求定制自己视图.以下是官网传送门:http://www.django-rest-framework.org/api-guide/views/ 在之前的文章中,由于参杂了权限.认证等(如果不了解请看博客的以前的文章),但在本章中基本可以不使用,所进使

【Redis源码剖析】 - Redis持久化之RDB

原创作品,转载请标明:http://blog.csdn.net/xiejingfa/article/details/51553370 Redis是一个高效的内存数据库,所有的数据都存放在内存中.我们知道,内存中的信息会随着进程的退出或机器的宕机而消失.为此,Redis提供了两种持久化机制:RDB和AOF.这两种持久化方式的原理实际上就是把内存中所有数据的快照保存到磁盘文件上,以避免数据丢失. 今天我们主要来介绍一下RDB持久化机制RDB的实现原理,涉及的文件为rdb.h和rdb.c. RDB的主

【Redis源码剖析】 - Redis IO操作之rio

原创作品,转载请标明:http://blog.csdn.net/Xiejingfa/article/details/51433696 Reids内部封装了一个I/O层,称之为rio.今天我们就来简单介绍一下rio模块的具体实现. 本文主要涉及rio.h和rio.c两个文件. 1.rio结构体 关于文件读写操作和buffer的操作主要基于rio对象进行操作,我们先来看看rio结构体的定义,如下: /* 系统IO操作的封装 */ struct _rio { /* Backend functions.

【Redis源码剖析】 - Redis数据类型之有序集合zset

原创作品,转载请标明:http://blog.csdn.net/Xiejingfa/article/details/51231967 这周事情比较多,原本计划每周写两篇文章的任务看来是完不成了.今天为大家带来有序集合zset的源码分析. Redis中的zset主要支持以下命令: zadd.zincrby zrem.zremrangebyrank.zremrangebyscore.zremrangebyrank zrange.zrevrange.zrangebyscore.zrevrangebys

【Redis源码剖析】 - Redis数据类型之列表List

原创作品,转载请标明:http://blog.csdn.net/Xiejingfa/article/details/51166709 今天为大家带来Redis五大数据类型之一 – List的源码分析. Redis中的List类型是一种双向链表结构,主要支持以下几种命令: lpush.rpush.lpushx.rpushx lpop.rpop.lrange.ltrim.lrem.rpoplpush linsert.llen.lindex.lset blpop.brpop.brpoplpush Li