Redis 源码--省内存大法--intset和ziplist

今天把这两货放在一起来看看,因为Redis是把数据都放在了内存里,所以涉及到内存的占用,基本就是能省则省,对于一些小容量的redis对象来说,redis底层会选择一些压缩数据结构而不是大容量的结构来存储他们,今天要写的就属于这种类型。分别是Intset和ziplist.

先来看看intset吧,顾名思义,这玩意就是一个整数集合,用来实现少量整数的集合。下面是它的类型定义。

typedef struct intset {
 
    // 编码方式
    uint32_t encoding;

    // 集合包含的元素数量
    uint32_t length;

    // 保存元素的数组
    int8_t contents[];

} intset;

intset是一种具有一致类型数据的set,它里面的数据可以是int_16 int_32 int_64 的,为了区分他们,需要一个encoding来记录contents数组里面存放的是何种数据。
当我们需要放入的数据超出了当前set的encoding所能表示的范围,那么我们就需要对intset进行升级,也就是upgrade方法
哦对了,下面的代码里会出现一个定义在endianconv中的反转字节函数。它是这么定义的。以intrev32ifbe为例。

uint32_t intrev32(uint32_t v) {
    memrev32(&v);
    return v;
}
void memrev32(void *p) {
    unsigned char *x = p, t;

    t = x[0];
    x[0] = x[3];
    x[3] = t;
    t = x[1];
    x[1] = x[2];
    x[2] = t;
}

对于为什么要使用这种函数,注释里是这么说的。
Redis tries to encode everything as little endian (but a few things that need

  • to be backward compatible are still in big endian) because most of the
  • production environments are little endian, and we have a lot of conversions
  • in a few places because ziplists, intsets, zipmaps, need to be endian-neutral
  • even in memory, since they are serialied on RDB files directly with a single
  • write(2) without other additional steps.
    简单来说,在redis的RDB持久化方式中,ziplists, intsets等是直接被写到文件中去的,所以这要求他们在内存中是大小端中性(endian-neutral),而大部分环境是小端法的,于是对于这些压缩的数据结构,redis总是采取小端法的方式来存放字节,那么对于大端法的机器来说,想要得到正确的数,便需要端转换。需要注意的是,反转的仅仅是作为参数传入的在栈上的字节,对于结构体自身的字节并未发生改变。
static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
    
    // 当前的编码方式
    uint8_t curenc = intrev32ifbe(is->encoding);

    // 新值所需的编码方式
    uint8_t newenc = _intsetValueEncoding(value);

    // 当前集合的元素数量
    int length = intrev32ifbe(is->length);

    // 根据 value 的值,决定是将它添加到底层数组的最前端还是最后端
    // 注意,因为 value 的编码比集合原有的其他元素的编码都要大
    // 所以 value 要么大于集合中的所有元素,要么小于集合中的所有元素
    // 因此,value 只能添加到底层数组的最前端或最后端
    int prepend = value < 0 ? 1 : 0;

    /* First set new encoding and resize */
    // 更新集合的编码方式
    is->encoding = intrev32ifbe(newenc);
    // 根据新编码对集合(的底层数组)进行空间调整
    // T = O(N)
    is = intsetResize(is,intrev32ifbe(is->length)+1);

    /* Upgrade back-to-front so we don't overwrite values.
     * Note that the "prepend" variable is used to make sure we have an empty
     * space at either the beginning or the end of the intset. */
    // 根据集合原来的编码方式,从底层数组中取出集合元素
    // 然后再将元素以新编码的方式添加到集合中
    // 当完成了这个步骤之后,集合中所有原有的元素就完成了从旧编码到新编码的转换
    // 因为新分配的空间都放在数组的后端,所以程序先从后端向前端移动元素
    // 举个例子,假设原来有 curenc 编码的三个元素,它们在数组中排列如下:
    // | x | y | z | 
    // 当程序对数组进行重分配之后,数组就被扩容了(符号 ? 表示未使用的内存):
    // | x | y | z | ? |   ?   |   ?   |
    // 这时程序从数组后端开始,重新插入元素:
    // | x | y | z | ? |   z   |   ?   |
    // | x | y |   y   |   z   |   ?   |
    // |   x   |   y   |   z   |   ?   |
    // 最后,程序可以将新元素添加到最后 ? 号标示的位置中:
    // |   x   |   y   |   z   |  new  |
    // 上面演示的是新元素比原来的所有元素都大的情况,也即是 prepend == 0
    // 当新元素比原来的所有元素都小时(prepend == 1),调整的过程如下:
    // | x | y | z | ? |   ?   |   ?   |
    // | x | y | z | ? |   ?   |   z   |
    // | x | y | z | ? |   y   |   z   |
    // | x | y |   x   |   y   |   z   |
    // 当添加新值时,原本的 | x | y | 的数据将被新值代替
    // |  new  |   x   |   y   |   z   |
    // T = O(N)
    while(length--)
        _intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc));

    /* Set the value at the beginning or the end. */
    // 设置新值,根据 prepend 的值来决定是添加到数组头还是数组尾
    if (prepend)
        _intsetSet(is,0,value);
    else
        _intsetSet(is,intrev32ifbe(is->length),value);

    // 更新整数集合的元素数量
    is->length = intrev32ifbe(intrev32ifbe(is->length)+1);

    return is;
}

这里的upgradeAndAdd看上去很长,但其实无非就是根据值决定放在首部还是尾部(因为set是有序的),由于整个过程是需要重新移动数据的,所以复杂度应该是O(N),这个复杂度只在小的时候是可以接受的,好在我们本来就是需要能尽量减少内存的使用,这也可以视为是一种 time space trade off.
剩下的方法也差不多,在此基础上,对于set进行插入,查找,删除。
接下来可以来看看ziplist了。很相似的,ziplist是一种压缩型的列表,它记录尾部,从尾到头来遍历整个list。

非空 ziplist 示例图

area        |<---- ziplist header ---->|<----------- entries ------------->|<-end->|

size          4 bytes  4 bytes  2 bytes    ?        ?        ?        ?     1 byte
            +---------+--------+-------+--------+--------+--------+--------+-------+
component   | zlbytes | zltail | zllen | entry1 | entry2 |  ...   | entryN | zlend |
            +---------+--------+-------+--------+--------+--------+--------+-------+
                                       ^                          ^        ^
address                                |                          |        |
                                ZIPLIST_ENTRY_HEAD                |   ZIPLIST_ENTRY_END
                                                                  |
                                                        ZIPLIST_ENTRY_TAIL

每个 ziplist 节点的前面都带有一个 header ,这个 header 包含两部分信息:

 *
 * 1)前置节点的长度,在程序从后向前遍历时使用。
 *
 * 2)当前节点所保存的值的类型和长度。

对于前置长度,为了要省内存,丧心病狂的使用了两种编码。

 *
 * 1) 如果前置节点的长度小于 254 字节,那么程序将使用 1 个字节来保存这个长度值。
 *
 * 2) 如果前置节点的长度大于等于 254 字节,那么程序将使用 5 个字节来保存这个长度值:
 *    a) 第 1 个字节的值将被设为 254 ,用于标识这是一个 5 字节长的长度值。
 *    b) 之后的 4 个字节则用于保存前置节点的实际长度。
当然,对于本节点自身的长度,也是不放过的:
1) 如果节点保存的是字符串值,
 *    那么这部分 header 的头 2 个位将保存编码字符串长度所使用的类型,
 *    而之后跟着的内容则是字符串的实际长度。
 *
 * |00pppppp| - 1 byte
 *      String value with length less than or equal to 63 bytes (6 bits).
 *      字符串的长度小于或等于 63 字节。
 * |01pppppp|qqqqqqqq| - 2 bytes
 *      String value with length less than or equal to 16383 bytes (14 bits).
 *      字符串的长度小于或等于 16383 字节。
 * |10______|qqqqqqqq|rrrrrrrr|ssssssss|tttttttt| - 5 bytes
 *      String value with length greater than or equal to 16384 bytes.
 *      字符串的长度大于或等于 16384 字节。
 *
 * 2) 如果节点保存的是整数值,
 *    那么这部分 header 的头 2 位都将被设置为 1 ,
 *    而之后跟着的 2 位则用于标识节点所保存的整数的类型。
 *
 * |11000000| - 1 byte
 *      Integer encoded as int16_t (2 bytes).
 *      节点的值为 int16_t 类型的整数,长度为 2 字节。
 * |11010000| - 1 byte
 *      Integer encoded as int32_t (4 bytes).
 *      节点的值为 int32_t 类型的整数,长度为 4 字节。
 * |11100000| - 1 byte
 *      Integer encoded as int64_t (8 bytes).
 *      节点的值为 int64_t 类型的整数,长度为 8 字节。
 * |11110000| - 1 byte
 *      Integer encoded as 24 bit signed (3 bytes).
 *      节点的值为 24 位(3 字节)长的整数。
 * |11111110| - 1 byte
 *      Integer encoded as 8 bit signed (1 byte).
 *      节点的值为 8 位(1 字节)长的整数。
 * |1111xxxx| - (with xxxx between 0000 and 1101) immediate 4 bit integer.
 *      Unsigned integer from 0 to 12. The encoded value is actually from
 *      1 to 13 because 0000 and 1111 can not be used, so 1 should be
 *      subtracted from the encoded 4 bit value to obtain the right value.
 *      节点的值为介于 0 至 12 之间的无符号整数。
 *      因为 0000 和 1111 都不能使用,所以位的实际值将是 1 至 13 。
 *      程序在取得这 4 个位的值之后,还需要减去 1 ,才能计算出正确的值。
 *      比如说,如果位的值为 0001 = 1 ,那么程序返回的值将是 1 - 1 = 0 。
 * |11111111| - End of ziplist.
 *      ziplist 的结尾标识

看到这么多的格式,是不是要崩溃了。。。其实我的感觉也是差不多的,为了省内存,这也是没有办法的事情。说了这么多,当前面的结点长度改变时,有时还会发生更蛋疼的事情,那就是前面结点的长度超过我之前记录的一个字节所能容纳的范围了,于是我就是变成四个字节了,这可没那么轻松,又得重新分配内存了,最糟糕的事情是一个结点的更新如同蝴蝶效应一般,造成了之后的结点的长度都超出了一个字节所能表示(255字节)的范围,那么我得连锁更新,不过,从概率学的角度来说,那并不是很容易发生。
写到这里,我也觉得很蛋疼,但是没有办法作为一种跑在内存里吃内存的NOSQL,为了省内存,redis也是拼了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345