今天看啥  ›  专栏  ›  VoV106435

Redis入门(一)字符串

VoV106435  · 掘金  ·  · 2021-04-15 16:06
阅读 12

Redis入门(一)字符串


字符串常见命令

// 添加一个key为name,value为tom 的字符串
127.0.0.1:6379> set name "tom"
OK
// 获取key的值
127.0.0.1:6379> get name
"tom"
// 判断key是否存在
127.0.0.1:6379> exists name 
(integer) 1
127.0.0.1:6379> exists age
(integer) 0

// 批量写入多个key
127.0.0.1:6379> mset name "tom" age 20 sex man
OK
// 批量读取多个key
127.0.0.1:6379> mget name age sex
1) "tom"
2) "20"
3) "man"

// 设置失效时间
127.0.0.1:6379> setex name 5 "Tony"
OK
// 5秒后
127.0.0.1:6379> get name
(nil)

// 如果key不存在,设置value,返回1
127.0.0.1:6379> setnx name "tom"
(integer) 1
// key存在了,无法设置返回0
127.0.0.1:6379> setnx name "Tony"
(integer) 0

复制代码

数据结构

struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; 
    char buf[];
};

struct __attribute__ ((__packed__)) sdshdr8 {
	// 已经使用的数组长度	
    uint8_t len; 
	// 不包含对象头和空字符,数组总长度(减去len后就是之前版本的free属性)
    uint8_t alloc;
	// flag 一个字节 其中第三位存储的类型,高5位来记录小于32个字节的长度
    unsigned char flags; 
	// 字符数组
    char buf[];
};
复制代码

这里说下flag字段,看Redis为了节省空间如何“丧心病狂”的节约内存使用。看下面两个问题

  1. 如果不管存的是短字符、还是长字符都占用相同大小的头部?
  2. 短字符来说,长度为1个字节的字符串,而头部的len和alloc字段就占用两个字节有必要吗?

对于第一个问题,sdshdr5结构体中,没有len和alloc字段。那么怎么标识长度呢?别急,再看第二个问题,怎么知道字符长度是几个字节呢?(1个字节?、2个字节?4个字节?8个字节?还是说少于1个字节)。

image.png 所以sdshdr5结构体的flag的低3位来区分类型。高5位来存字符的长度,保存长度小于32短字符串。

image.png sdshdr8,sdshdr16,sdshdr32,sdshdr64都是用len和alloc字段,flag字段只是采用低3位来记录类型。高5位就是闲置的。

内存分配

扩容

下面代码有部分删减,只截取了重要部分,完整代码请参考redis-5.0.1

sds sdsnewlen(const void *init, size_t initlen) {
    void *sh;
    sds s;
    char type = sdsReqType(initlen);
    /* Empty strings are usually created in order to append. Use type 8
     * since type 5 is not good at this. */
    if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
	// 根据不同类型得到本次扩容的长度
    int hdrlen = sdsHdrSize(type);
    unsigned char *fp; /* flags pointer. */

    sh = s_malloc(hdrlen+initlen+1);
    if (sh == NULL) return NULL;
    if (init==SDS_NOINIT)
        init = NULL;
    else if (!init)
	// 非初始化,即扩容
        memset(sh, 0, hdrlen+initlen+1);
   // 空间预分配
    s = (char*)sh+hdrlen;
    fp = ((unsigned char*)s)-1;
    switch(type) {
        case SDS_TYPE_5: {
            *fp = type | (initlen << SDS_TYPE_BITS);
            break;
        }
        case SDS_TYPE_8: {
            SDS_HDR_VAR(8,s);
            sh->len = initlen;
            sh->alloc = initlen;
            *fp = type;
            break;
        }
    }
    if (initlen && init)
        memcpy(s, init, initlen);
    s[initlen] = '\0';
    return s;
}
复制代码
#define SDS_MAX_PREALLOC (1024*1024)
 len = sdslen(s);
    sh = (char*)s-sdsHdrSize(oldtype);
    newlen = (len+addlen);
    if (newlen < SDS_MAX_PREALLOC)
        newlen *= 2;
    else
        newlen += SDS_MAX_PREALLOC;
复制代码
  • SDS的alloc小于SDS_MAX_PREALLOC时,即1M大小,那么新数组是原数组长度的一倍,并且会预分配扩容后的容量,举个例子,现在数组长度是8字节,已经使用了6个字节,现在需要添加8个字节,那么新的字符数组长度就是 14,然后预分配一倍的长度,最后的14+14+1=29个字节,因为动态数据最后一个字节都是'\0'的空字符,所以+1是空字符长度。

  • SDS的alloc大于1M时,扩容后会预分配1M的内存,例如,现在数组长度是2M大小,需要添加3M的字符串,那么扩容后容量就是2+3+1=6M,再加上1个字节。

这种预分配好处就是避免连续重新分配内存。

惰性释放

“惰性空间释放用于优化SDS的字符串缩短操作:当SDS的API需要缩短SDS保存的字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是更新len属性,将未使用的字节的数量记录起来,并等待将来使用。

个人拙见,如有不对,勿喷,还请指出。




原文地址:访问原文地址
快照地址: 访问文章快照