redis中数据类型的使用,并发问题,list重复插入问题,redis使用实例-简单消息队列和排名统计_深山猿的博客-CSDN博客_redis 不重复的list

java对象应该存成string还是map类型:https://segmentfault.com/a/1190000040032006

redis内存占用评估:
https://juejin.cn/post/6886726965030551559
https://kernelmaker.github.io/Redis-StringMem

Redis中的Key-Value 键值对的想必大家都用到过,我们一般在业务开发中,或多或少都会使用到Redis来进行数据的缓存、非关系存储、甚至当做消息队列,数据中转站等。其中Key-Value键值对的操作更是为大家广泛使用,不知道大家工作中,有没有遇到过需要对Redis 内存使用量进行评估。 前段时间,公司的redis一个集群内存告警,原来业务方估算的大约4GB的内存使用量,数据还未完全灌倒Redis中,就耗费了将近30GB,当时使用的就是Key-value 写。为什么估算的量和真正的使用量差距会这么大?Key-Value键值对在Redis中是以怎样的结构进行存储的呢?下面如无特殊说明,我们以Redis3.0版本进行分析。

​ 首先看下Redis中Key-Value键值对是怎样存储的概览图,共涉及DictEntry、RedisObject、SDS 3种结构,字符串编码、对象共享机制,以及jemalloc 内存分配。

dictEntry 字典

​ Redis 通过对Key 进行Hash计算,然后锁定对应的hash槽(table),Hash槽指向对应的dictEntry,dictEntry持有key、val以及Hash冲突时链表的下个字典节点的指针,dictEntry结构占用8+8+8=24字节。

/*
 * 哈希表节点
 */
typedef struct dictEntry {
    
    // 键
    void *key;

    // 值
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
    } v;

    // 指向下个哈希表节点,形成链表
    struct dictEntry *next;

} dictEntry;
复制代码

redisObject

​ RedisObject结构持有数据对象的元数据和数据对象指针,其中type(4bit)标识该数据是哪种数据类型,redis目前定义了String、List、Set、Zset、Hash5中类型,用户也可以自己拓展。encoding(4bit)标识数据的编码类型,目前定义了raw、int、embstr、hashtable、zipmap、linkedList、ziplist、intset、skiplist 8种(redis后续版本有调整),今天聊的主要涉及type=String,encoding包含raw、int、embstr这几种。lru(24bit)对象最后一次的访问时间,主要用于redis lru、lfu淘汰策略使用。refcount 引用计数,当refcount 为0 ,标识对象可以释放。ptr指针,指向真正的数据对象。RedisObject结构占用(4+4+24)/8 +4+8 = 16字节。

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;
复制代码

SDS(Simple Dynamic String,简单动态字符串)

​ Redis实现了自定义的字符串结构,较与C语言字符串相比,可以高效的计算长度、执行追加操作等,几乎所有的 Redis 模块中都用了 sds。

/*
 * 保存字符串对象的结构
 */
struct sdshdr {
    
    // buf 中已占用空间的长度
    int len;

    // buf 中剩余可用空间的长度
    int free;

    // 数据空间
    char buf[];
};
复制代码

如上述结构所示,字符串对象结构体,占用4+4=8个字节,sds字符串使用/0结尾,占用1个字节,也就是计算一个sds字符串对象的所占用字节数是4+4+1+len+free=9+len+free。

字符串编码

​ 字符串编码涉及到raw、int、embstr 三种编码类型,他们之间的区别主要如下图,embstr相对于raw,主要是redisobject 和sds字符串是一块连续的内存,目的是减少内存碎片。int编码格式将ptr指针(8字节)直接赋值与数据值,目的减少内存寻址。 

对象共享

​ Redis服务启动后,会内置数值型的共享对象,默认情况下0~9999,如果value是数值型,且在这个范围内,那么直接使用共享对象替代redisObject 和sds,并将过程中的redisObject的refcount-1;共享对象的引用+1;

上图a的值2,属于共享对象,被引用了3次。

Jemalloc

​ Redis 默认采用Jemalloc内存分配器 (ptmalloc、tcmalloc和jemalloc 内存分配器的差异)jemalloc 在 64 位系统中,将内存空间划分为小、大、巨大三个范围;当redis申请内存时,jemalloc 会根据划分申请对应的大小的空间,例如当要申请12byte的空间,jemalloc会分配16byte。 

验证环节

前言:

​ 介绍完了上述3种数据结构和2中机制后,我们进行实战验证,看看Redis是怎么运用这些机制的(大家验证时,请使用单节点实例,cluster模式会有些差异)。Redis3.0 版本,当字符串的长度小于等于39时,会采用embstr格式,大于39则编码格式为raw。

​ 我们先回顾下redis中Key-Value键值对存储的形式,根据这个图来计算键值对存储所消耗的内存空间。由图所示,dictEntry所占用空间是固定的 3*8(3个指针)=24-->32byte,key受sds内存分配机制影响(目测SDS字符串首次分配时,不会预分配空闲空间,即dsd->free = 0)

当value 是字符串时

set a 123456a
复制代码

此时占用内存为32(dictEntry)+16(key sds)+16(value redisObject )+ 16(value sds) = 80byte,如下图,符合预期。

当value 是数值时

set a 1234567
复制代码

预测 32(dictEntry)+16(key sds)+16(value redisObject) = 64byte

可结果却是80byte,这是为什么呢?原来在redis接收到请求后,会对请求入参value创建redisObject字符串,此时他的规则是大于39为raw编码,小于等于39位embstr编码,这种场景下,运行时的value是embstr编码格式,尽管他是数值型的,在setCommand函数中,会再次对value进行编码,此时会将redisObject的ptr指针直接赋值1234567 value值,但是应该是基于内存碎片的考虑,并未释放embstr编码格式下的sds字符串所占用的空间,因此计算公式应该为 32(dictEntry)+16(key sds)+16(value redisObject )+ 16(value sds) = 80byte。

当value是数值时,且数值小于范围0~10000内时

set a 1
复制代码

这个场景在没看源码之前,也是困扰了好久,其实这里使用到了共享对象机制,value不占用额外的内存空间,因此占用内存为 32(dictEntry)+16(key sds) = 48byte

主要源码

  1. 当redis接收到请求后,会给key 和val创建redisObject(源码networking#processMultibulkBuffer方法中调用了createStringObject函数),作为运行中的数据对象(并不是存储对象),根据字符串长度与阈值39的关系,采用raw或者embstr编码。此时例子中的key=a,value = abcde 都是redisObject对象,且采用了embstr编码。

    createStringObject源码如下

#define REDIS_ENCODING_EMBSTR_SIZE_LIMIT 39
robj *createStringObject(char *ptr, size_t len) {
    if (len <= REDIS_ENCODING_EMBSTR_SIZE_LIMIT)
        return createEmbeddedStringObject(ptr,len);
    else
        return createRawStringObject(ptr,len);
}
复制代码
  1. 对value进行编码,由前面createStringObject 知晓,目前value只有raw 或者embstr 两种编码格式,此处主要是对value是数值的场景进行优化。

    如果value在0~9999范围,则使用共享对象,那么最终该操作,将不会申请额外的内存。

    如果不在0~9999范围,则将redisObject中的ptr指针直接转为value。

    /* 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. */
        redisAssertWithInfo(NULL,o,o->type == REDIS_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. */
        // 只在字符串的编码为 RAW 或者 EMBSTR 时尝试进行编码
        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 21 chars is not
         * representable as a 32 nor 64 bit integer. */
        // 对字符串进行检查
        // 只对长度小于或等于 21 字节,并且可以被解释为整数的字符串进行编码
        len = sdslen(s);
        if (len <= 21 && 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. */
            if (server.maxmemory == 0 &&
                value >= 0 &&
                value < REDIS_SHARED_INTEGERS)
            {
                decrRefCount(o);
                incrRefCount(shared.integers[value]);
                return shared.integers[value];
            } else {
                if (o->encoding == REDIS_ENCODING_RAW) sdsfree(o->ptr);
                o->encoding = REDIS_ENCODING_INT;
                o->ptr = (void*) value;
                return o;
            }
        }
    
    复制代码

    最后:一个渣渣程序员,上面是结合一些资料和源码的主观理解,如果有不对或者疑问的地方,欢迎留言探讨,让我们轻松完爆Redis!期待你的加入


作者:泠小墨
链接:https://juejin.cn/post/6886726965030551559
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

另一篇文章:

redis 一组kv实际内存占用计算

之前帮公司DBA同事调研Redis内存占用的问题,比如redis在执行一条”set aaa bbb”命令后,这组键值对”aaa” -> “bbb”自身占用6个字节,那为了存储它们redis实际需要占用多少字节呢?本文以redis(tag 2.8.20)和64位机器为参考来一探究竟

SDS

redis对字符串做了自己的封装,叫sds,定义如下:

typedef char *sds;

struct sdshdr {
    unsigned int len;
    unsigned int free;
    char buf[];
};

其实就是给字符串最前面多加两个unsigned int来保存字符串信息,len是总长度,free是当前可用长度,所以假设当前有一个字符串”aaa”,那么通过sds来存它最少需要多少个字节呢,很简单4(len)+4(free)+3(“aaa”)+1(‘\0’) = 12,也就是说通过sds来保存一个字符串,会在字符串实际占用之上再多占用9个字节

Object

sds仅是对字符串的封装,在其之上,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;

每个robj占用((4b(type)+4b(encoding)+24b(lru))/8)+4(refcount)+8(ptr) = 16字节,ptr指向所存内容,以”aaa”为例,包成sds占用12字节,再包成robj,实际占用16+12 = 28字节,其中ptr指向sds,所以对字符串的robj的内存占用公式可以总结为”N+9+16”,N为字符串长度

注: robj的ptr并不总是保存实际内容,假设字符为”123”,当然type为REDIS_STRING,redis会在tryObjectEncoding函数中判断robj的ptr指向的sds能不能转化成整数,假如可以,那么而直接将ptr的值赋为123,并释放之前的sds,此时encoding为REDIS_ENCODING_INT表明这个ptr保存的是一个整数,所以实际占用仅为一个robj的大小,即16字节

计算

对sds有了初步了解之后,我们就可以开始计算了,当redis接收到客户端”set aaa bbb”命令之后,

第一步就是参数解析,直接看关键代码:

首先是协议解析,调用栈是readQueryFromClient -> processInputBuffer -> processMultibulkBuffer,在processMultibulkBuffer中会对每个参数调用createStringObject生成robj,先来看createStringObject的实现:

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;

    /* Set the LRU to the current lruclock (minutes resolution). */
    o->lru = server.lruclock;
    return o;
}

那么对于”set”,”aaa”,”bbb”会生成3个robj,各占28个字节,不过对于set这个robj仅仅是用来查找命令lookupCommand使用,后来会释放,对”aaa”这个robj也是会用新的”aaa”的sds来存储,robj也会释放,前两个不计算在内,所以目前仅有”bbb”总共占56字节

第二步,命令的执行,调用栈是processCommand -> call -> proc(此处为setcommand) -> setGenericCommand -> setKey,最终数据是在setKey中被存在db中,这里有一点特殊说明一下,在setcommand调用setGenericCommand之前会调用

c->argv[2] = tryObjectEncoding(c->argv[2]);

这里会对命令中的value进行上面说的tryObjectEncoding,此处argv[2]是包含”bbb”的robj,所以这里tryObjectEncoding后,这个robj不会变小,但如果此处是包含”123”的robj,那么经过tryObjectEncoding后,大小会从28变为16(具体原因参考Object一节  部分)

接着往下看,setKey的定义如下:

void setKey(redisDb *db, robj *key, robj *val) {
    if (lookupKeyWrite(db,key) == NULL) {
        dbAdd(db,key,val);
    } else {
        dbOverwrite(db,key,val);
    }
    incrRefCount(val);
    removeExpire(db,key);
    signalModifiedKey(db,key);
}

可以看到我们会将”aaa”和”bbb”的robj指针作为第二、三参数传给它,这里假设这个key之前不存在,那么会调用dbAdd把他插入到db中(db实际是个hash表)

void dbAdd(redisDb *db, robj *key, robj *val) {
    sds copy = sdsdup(key->ptr); // 将key的robj转换为对应的sds,在dict中的key用
    //sds的形式存
    int retval = dictAdd(db->dict, copy, val);

    redisAssertWithInfo(NULL,key,retval == REDIS_OK);
    if (val->type == REDIS_LIST) signalListAsReady(db, key);
}

int dictAdd(dict *d, void *key, void *val)
{
    dictEntry *entry = dictAddRaw(d,key); //生成新的dictEntry并赋值key字段

    if (!entry) return DICT_ERR;
    dictSetVal(d, entry, val); //给dictEntry的v字段赋值,指向包含"bbb"的robj
    return DICT_OK;
}

dictEntry *dictAddRaw(dict *d, void *key)
{
    int index;
    dictEntry *entry;
    dictht *ht;

    if (dictIsRehashing(d)) _dictRehashStep(d);

    /* Get the index of the new element, or -1 if
     * the element already exists. */
    if ((index = _dictKeyIndex(d, key)) == -1)
        return NULL;

    /* Allocate the memory and store the new entry */
    ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];
    entry = zmalloc(sizeof(*entry)); //生成新的dictEntry
    entry->next = ht->table[index]; //插入到index位置的桶中
    ht->table[index] = entry;
    ht->used++;

    /* Set the hash entry fields. */
    dictSetKey(d, entry, key); //给dictEntry的key字段赋值,指向"aaa"的sds
    return entry;
}

dbAdd调用dictAdd,最终由dictAdd将一个sds和一个object插入到db中,说是插入,其实就是对这组键值对调用dictAddRaw生成一个dictEntry,并把他插入到按key求hash值索引到的桶中,说到这里已经明确了,这组键值对最终实际保存的位置就是在dictEntry中,它的大小就是最终实际大小,来看定义

typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;
} dictEntry;

一个dictEntry的大小是8(key)+8(v)+8(next) = 24字节,key是一个”aaa”的sds指针,v是一个指向包含”bbb”的robj的指针,next是指向对应桶中第二个dictEntry的指针

结论

  • 在执行”set aaa bbb”命令后,redis会用24(dictEntry)+12(sds(“aaa”))+28(robj(“bbb”)) = 64字节来存储
  • 在执行”set aaa 10000”命令后,redis会用24(dictEntry)+12(sds(“aaa”))+16(robj(“10000”)) = 52字节来存储(redis对整数10000之内robj创建了shared object,也就是说如果这里不是10000而是123的话,不会为123新创建robj而是直接增加shared object中已有123的robj的计数,这样空间占用更小)
  • 上面说的64和52只是redis申请的字节数,实际占用还要根据具体allocator来看,感兴趣的话可以参考我的”TCMalloc”系列来进一步阅读^^

补充

在第一步协议解析之后,redis会对每个参数生成对应的robj并且存储在redisClient的argv中,执行完命令之后会调用resetClient来释放这个argv中的每个robj,那岂不是那些被存在dictEntry的robj都被释放了?其实不会,先看resetClient实现:

void resetClient(redisClient *c) {
    freeClientArgv(c);
    c->reqtype = 0;
    c->multibulklen = 0;
    c->bulklen = -1;
    /* We clear the ASKING flag as well if we are not inside a MULTI. */
    if (!(c->flags & REDIS_MULTI)) c->flags &= (~REDIS_ASKING);
}

static void freeClientArgv(redisClient *c) {
    int j;
    for (j = 0; j < c->argc; j++)
        decrRefCount(c->argv[j]);
    c->argc = 0;
    c->cmd = NULL;
}

resetClient调用freeClientArgv来释放argv中的每个robj,真正的操作只是减引用计数,只有为0时才真正释放,所以为了保证dictEntry中的robj不被释放,肯定有地方把robj的引用计数加1了,具体在哪里呢,value的引用计数是在setKey函数里加的:

void setKey(redisDb *db, robj *key, robj *val) {
    if (lookupKeyWrite(db,key) == NULL) {
        dbAdd(db,key,val);
    } else {
        dbOverwrite(db,key,val);
    }
    incrRefCount(val); //对val的引用计数加1
    removeExpire(db,key);
    signalModifiedKey(db,key);
}

那么key的呢?它在哪里加?事实上,它没有加,最终会在resetClient中释放,不过在dbAdd函数中,会对key生成一个新的sds并把它存入dictEntry中,所以释放argv里的robj没有影响,具体如下:

void dbAdd(redisDb *db, robj *key, robj *val) {
    sds copy = sdsdup(key->ptr); //对key生成一个新的sds
    int retval = dictAdd(db->dict, copy, val);

    redisAssertWithInfo(NULL,key,retval == REDIS_OK);
    if (val->type == REDIS_LIST) signalListAsReady(db, key);
 }

好了,这些细节其实对本篇的主题理解无影响,不过细扣这些可以让我们更加深刻的理解redis,还是很有必要的,也许这是在给我的强迫症找借口,O(∩_∩)O哈哈~

总结

这篇讲的东西切入点很小,平时大伙有可能都忽略它的存在,不过学习这些细节也会很有收获的,至少我觉得是个好习惯^^

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐