当字符串超过7个字节时,字符串的Redis int表示更大,否则更小

use*_*273 22 int optimization redis

我正在努力尽可能地减少Redis的物体尺寸,我花了整整一周的时间来试验它.

在测试不同的数据表示时,我发现字符串"hello"的int表示形成了一个较小的对象.它可能看起来不是很多,但是如果你有很多数据,它可以在使用几GB内存和几十GB内存之间产生差异.

请看以下示例(如果需要,您可以自己尝试):

> SET test:1 "hello"
> debug object test:1
> Value at:0xb6c9f380 refcount:1 encoding:raw serializedlength:6 lru:9535350 lru_seconds_idle:7
Run Code Online (Sandbox Code Playgroud)

特别是你应该看看serializedlength,在这种情况下是6(字节).

现在,看看它的以下int表示:

> SET test:2 "857715"
> debug object test:2
> Value at:0xb6c9f460 refcount:1 encoding:int serializedlength:5 lru:9535401 lru_seconds_idle:2
Run Code Online (Sandbox Code Playgroud)

如你所见,它导致一个字节更短的对象(注意还编码:int我认为这意味着以更有效的方式处理整数).

使用字符串"hello w"(稍后您将看到为什么我没有使用"hello world"),当它表示为int时,我们得到更大的节省:

> SET test:3 "hello w"
> SET test:4 "857715023" <- Int representation. Notice that I inserted a "0", if I don't, it results in a bigger object and the encoding is set to "raw" instead (after all a space is not an int).
>
> debug object test:3
> Value at:0xb6c9f3a0 refcount:1 encoding:raw serializedlength:8 lru:9535788 lru_seconds_idle:6
> debug object test:4
> Value at:0xb6c9f380 refcount:1 encoding:int serializedlength:5 lru:9535809 lru_seconds_idle:5
Run Code Online (Sandbox Code Playgroud)

只要你没有超过7个字节的字符串,它看起来很酷.看看"hello wo"int表示会发生什么:

> SET test:5 "hello wo"
> SET test:6 "85771502315"
>
> debug object test:5
> Value at:0xb6c9f430 refcount:1 encoding:raw serializedlength:9 lru:9535907 lru_seconds_idle:9
> debug object test:6
> Value at:0xb6c9f470 refcount:1 encoding:raw serializedlength:12 lru:9535913 lru_seconds_idle:5
Run Code Online (Sandbox Code Playgroud)

如您所见,int(12个字节)大于字符串表示(9个字节).

我的问题是,当你将一个字符串表示为一个int时,幕后会发生什么,它会在你达到7个字节之前变小?

有没有办法像"list-max-ziplist-entries/list-max-ziplist-value"那样增加这个限制,或者是一种优化这个过程的聪明方法,这样它总能(或几乎)产生一个更小的对象比一个字符串?

UPDATE

我已经进一步尝试了其他技巧,你实际上可以拥有比字符串更小的整数,而不管它的大小,但这将涉及数据结构建模的更多工作.

我发现如果你将字符串的int表示形式分成大约8个数字的块,它最终会变小.

以"Hello World Hi Universe"为例,创建一个字符串int SET:

> HMSET test:7 "Hello" "World" "Hi" "Universe"
> HMSET test:8 "74111114" "221417113" "78" "2013821417184"
Run Code Online (Sandbox Code Playgroud)

结果如下:

> debug object test:7
> Value at:0x7d12d600 refcount:1 encoding:ziplist serializedlength:40 lru:9567096 lru_seconds_idle:296
>
> debug object test:8
> Value at:0x7c17d240 refcount:1 encoding:ziplist serializedlength:37 lru:9567531 lru_seconds_idle:2
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,我们将int集缩小了3个字节.

问题在于如何组织这样的事情,但它表明它仍然可能.

不过,不知道这个限制设置在何处.~700K持久使用内存(即使内部没有数据)也让我觉得有一个专门用于优化int集的预定义"池".

UPDATE2

我想我已经在Redis源中找到了这个intset"池"的定义.

redis.h文件的第81行,def REDIS_SHARED_INTEGERS设置为10000

REDISH_SHARED_INTEGERS

我怀疑它是定义intset字节长度限制的那个.

我必须尝试用更高的值重新编译它,看看我是否可以使用更长的int值(如果它是我想到的那个,它最有可能分配更多的内存).

UPDATE3

我要感谢Antirez的回复!没想到.

当他告诉我时,len!=内存使用情况.

我进一步完成了实验,发现对象已经被轻微压缩(序列化)了.我可能错过了Redis文档中的内容.

确认来自使用命令redis-memory-for-key 分析Redis密钥,该密钥实际上返回内存使用而不是序列化长度.

例如,让我们使用之前使用的"hello"字符串和int,看看结果是什么:

~ # redis-memory-for-key test:1
Key             "test:1"
Bytes               101
Type            string
~ #
~ # redis-memory-for-key test:2
Key             "test:2"
Bytes           87
Type            string
Run Code Online (Sandbox Code Playgroud)

你可以注意到intset比字符串(101字节)更小(87字节).

UPDATE4

令人惊讶的是,较长的intset似乎会影响其序列化长度,但不影响内存使用情况.

这使得实际构建一个2digit-char映射成为可能,同时它仍然比字符串更具内存效率,甚至没有对其进行分块.

通过2digit-char映射,我的意思是不是将"hello"映射到"85121215",而是将它映射到固定长度为2的数字,如果数字<10,则为"0",如"0805121215".

然后,通过将每两位数分开并将它们转换为等效的char来继续自定义脚本:

08 05 12 12 15
\  |  |   |  /
 h e  l   l o
Run Code Online (Sandbox Code Playgroud)

这足以避免消歧(如"o"和"ae"都会产生数字"15").

我将通过创建另一个集合并因此分析其内存使用情况来向您展示这一点,就像我之前做的那样:

> SET test:9 "0805070715"

Unix shell
----------
~ # redis-memory-for-key test:9
Key             "test:9"
Bytes           87
Type            string
Run Code Online (Sandbox Code Playgroud)

你可以看到我们在这里赢得了记忆.

用Smaz压缩的相同"hello"字符串进行比较:

>>> smaz.compress('hello')
'\x10\x98\x06'

// test:10 would be unfair as it results in a byte longer object
SET post:1 "\x10\x98\x06"

~ # redis-memory-for-key post:1
Key            "post:1"
Bytes          99
Type           string
Run Code Online (Sandbox Code Playgroud)

LSe*_*rni 4

我的问题是,当您将字符串表示为 int 时,幕后发生了什么,它会变小,直到达到 7 个字节?

请注意,您作为测试 #6 提供的整数实际上不再编码为整数,而是原始编码:

设置测试:6“85771502315”

值:0xb6c9f470 refcount:1 编码:原始序列化长度:12 lru:9535913 lru_seconds_idle:

因此我们看到“原始”值占用一个字节加上其字符串表示的长度。在内存中,您可以得到该值加上该值的开销。

我怀疑整数编码将数字编码为 32 位整数;那么它总是需要 5 个字节,其中 1 个字节用于说明其类型,4 个字节用于存储 32 位。

一旦溢出 32 位的最大可表示整数(即 20 亿或 4,具体取决于是否使用符号),您就需要恢复为原始编码。

所以很可能

2147483647 -> five bytes     (TYPE_INT 0x7F 0xFF 0xFF 0xFF)
2147483649 -> eleven bytes   (TYPE_RAW '2'  '1'  '4'  '7'  '4'  '8'  '3'  '6'  '4'  '9')
Run Code Online (Sandbox Code Playgroud)

现在,如果只使用 ASCII 集,如何压缩字符串表示形式?

可以得到字符串(140个字符):

When in the Course of human events it becomes necessary for one people
to dissolve the political bands which have connected them with another
Run Code Online (Sandbox Code Playgroud)

并将每个字符转换为六位表示;基本上是它在字符串中的索引

"ABCDEFGHIJKLMNOPQRSTUVWXYZ01234 abcdefghijklmnopqrstuvwxyz56789."
Run Code Online (Sandbox Code Playgroud)

这是您可以使用的所有字符的集合。

现在,您可以将四个这样的“纯文本字符”编码为三个“二进制字符”,这是一种“反向 Base 64 编码”;Base64 编码将获取三个二进制字符并创建一个四字节的 ASCII 字符序列。

如果我们将其编码为整数组,我们将节省一些字节 - 可能会减少到 130 字节 - 但代价是更大的开销。

通过这种“反向base64”编码,我们可以将140个字符分成35组,每组四个字符,成为一个35x3 = 105个二进制字符的字符串,原始编码为106个字节。

只要我重复一遍,您就永远不会使用上述范围之外的字符。如果这样做,您可以将范围扩大到 128 个字符和 7 位,从而节省 12.5% 而不是 25%;140 个字符将变成 126 个字符,原始编码为 127 个字节,您将节省 (141-127) = 14 个字节。

压缩

如果您有更长的字符串,您可以压缩deflate()它们(即,您使用诸如orgzencode()或 之类的函数gzcompress())。要么直;在这种情况下,上述字符串将变为 123 字节。容易做。

压缩许多小字符串:鲁布·戈德堡方法

由于压缩算法会学习,并且一开始它们不敢假设任何内容,因此小字符串不会被大幅压缩。可以这么说,它们“一切都开始了”。就像发动机一样,冷运转时性能较差。

如果您有这些字符串来自的文本“语料库”,您可以使用一种耗时的技巧来“预热”压缩引擎,并可能使其性能加倍(或更好)。

假设您有两个字符串,COMMON并且TARGET(第二个是您感兴趣的)。如果你进行 z 压缩,COMMON你会得到,比如说,ZCMN。如果你压缩TARGET你会得到ZTRGT.

但正如我所说,由于 gz 压缩算法是面向流的,并且它会随着时间的推移而学习,因此任何文本后半部分的压缩率(假设两半之间没有奇怪的统计分布变化)总是明显高于上半场的情况。

因此,如果你要压缩,比如说COMMONTARGET,你会得到ZCMGHQI

请注意,字符串的第一部分(直到几乎末尾)与之前相同。事实上,如果你压缩COMMONFOOBAR,你会得到类似的东西ZCMQKL。即使我们将重叠区域视为完全属于第二根字符串,第二部分的压缩效果也比以前更好。

这就是窍门。给定一个字符串族 ( TARGET, FOOBAR, CASTLE BRAVO),我们不是压缩字符串,而是压缩这些带有大前缀的字符串的串联。然后我们从结果中丢弃公共压缩前缀。因此TARGET取自COMMONTARGET(即ZCMGHQI) 的压缩,并变为GHQI代替ZTRGT,增益为 20%。

解码器执行相反的操作:给定GHQI,它首先应用公共压缩前缀ZCM它必须知道);然后对结果进行解码,最后丢弃常见的未压缩前缀,只需预先知道其长度即可。

所以上面的第一句话(140个字符)经过自身压缩就变成了123个;如果我采用声明的其余部分并将其用作前缀,它会压缩为 3355 字节。这个前缀加上我的140个字节就变成了3409个字节,其中3352个是常见的,还剩下57个字节。

代价是在编码器中存储一次未压缩的前缀,在解码器中存储一次压缩的前缀,并且整个 thingamajig 的运行速度慢了五倍,我现在可以将这 140 个字节减少到 57 个字节,而不是 123 个字节 - 不到一半前。

这个技巧对于小字符串非常有效。对于较大的公司来说,这种优势并不值得付出痛苦。此外,不同的前缀会产生不同的结果。最好的前缀是包含大多数可能出现在字符串池中的序列的前缀,按长度递增排序。

额外的好处:压缩前缀还兼作一种弱加密,因为如果没有它,您就无法轻松解码压缩字符串,即使您可能能够恢复其中的某些部分。