我正在尽可能地缩小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
特别是,您应该查看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
正如您所看到的,它导致了一个字节更短的对象(请注意encoding:int,我认为这表明可以以更有效的方式处理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
只要字符串不超过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
正如您所看到的,int(12字节)比字符串表示(9字节)大。
我的问题是,当你把一个字符串表示为int时,它会变小,直到达到7个字节,幕后会发生什么?
有没有一种方法可以像"list max ziplist entries/list max ziplist-value"那样增加这个限制,或者有一种聪明的方法可以优化这个过程,使它总是(或几乎)产生比字符串更小的对象?
更新
我进一步尝试了其他技巧,实际上,无论字符串大小,都可以使用比字符串更小的int,但这将涉及到数据结构建模方面的更多工作。
我发现,如果你把一个字符串的int表示拆分成块,每个块有~8个数字,它最终会更小。
以单词"Hello World Hi Universe"为例,创建字符串和intSET:
> HMSET test:7 "Hello" "World" "Hi" "Universe"
> HMSET test:8 "74111114" "221417113" "78" "2013821417184"
结果如下:
> 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
正如您所看到的,我们得到的int集小于3字节。
这个问题将是如何组织这样的事情,但它表明这是可能的。
不过,我不知道这个限制是在哪里设定的。内存的持续使用量约为700K(即使内部没有数据),这让我认为有一个预定义的"池"专门用于优化int集。
更新2
我想我已经在Redis源代码中找到了这个intset"pool"的定义位置。
在文件redis.h的81行,defredis_SHARED_INTEGERS设置为1000
REDISH_SHARED_INTEGERS
我怀疑它定义了intset字节长度的限制。
我必须试着用一个更高的值重新编译它,看看我是否可以使用一个更长的int值(如果我想的是它,它很可能会分配更多的内存)。
更新3
我要感谢Antirez的回复!没想到。
正如他让我注意到的那样内存使用情况。
我在实验中更进一步,发现对象已经被稍微压缩(序列化)了。我可能错过了Redis文档中的一些内容。
确认来自于使用Redis memory for keykey命令分析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
正如您所注意到的,intset比字符串(101字节)小(87字节)。
更新4
令人惊讶的是,更长的intset似乎会影响其序列化长度,但不会影响内存使用。。
这使得实际构建2位字符映射成为可能,同时它仍然比字符串更有内存效率,甚至不需要对其进行分块
通过2位字符映射,我的意思是,我们不是将"hello"映射到"85121215",而是将其映射到每个固定长度为2的数字,如果digital<10,如"0805121215"。
然后,自定义脚本会将每两个数字分开,并将它们转换为等效的字符:
08 05 12 12 15
| | | /
h e l l o
这足以避免歧义(比如"o"one_answers"ae"都会导致数字"15")。
我将通过创建另一个集并像以前一样分析其内存使用情况来向您展示这一点:
> SET test:9 "0805070715"
Unix shell
----------
~ # redis-memory-for-key test:9
Key "test:9"
Bytes 87
Type string
你可以看到我们在这里赢得了一场记忆胜利。
用Smaz压缩的相同"hello"字符串进行比较:
>>> smaz.compress('hello')
'x10x98x06'
// test:10 would be unfair as it results in a byte longer object
SET post:1 "x10x98x06"
~ # redis-memory-for-key post:1
Key "post:1"
Bytes 99
Type string
我的问题是,当您代表字符串作为int,它会更小,直到达到7个字节?
请注意,作为测试#6提供的整数实际上不再编码作为整数,但作为原始:
SET测试:6"85771502315">
值位于:0xb6c9f470引用计数:1编码:原始序列化长度:12 lru:9535913 lru_seconds_idle:
因此,我们看到一个"原始"值占用一个字节加上其字符串表示的长度。在内存中你得到了这个加上价值的开销。
我怀疑,整数编码将数字编码为32位整数;那么它将永远需要五个字节,一个字节用于说明其类型,四个字节用于存储这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')
现在,如果只使用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
并将每个字符转换为六位表示;基本上它在字符串中的索引
"ABCDEFGHIJKLMNOPQRSTUVWXYZ01234 abcdefghijklmnopqrstuvwxyz56789."
这是您可以使用的所有字符的集合。
您现在可以将四个这样的"纯文本字符"编码为三个"二进制字符",类似于"反向64进制编码";base64编码将获得三个二进制字符,并创建一个ASCII字符的四字节序列。
如果我们把它编码为整数组,我们会节省几个字节——也许会把它记下来到130字节-以更大的开销为代价。
使用这种类型的"反向base64"编码,我们可以获得140个字符到35组四个字符,这些字符变成35x3=105个二进制字符的字符串,原始编码为106字节。
只要,我重复一遍,因为永远不要使用超出范围的字符。如果你这样做,你可以将范围扩大到128个字符和7位,从而节省12.5%而不是25%;140个字符将变为126,原始编码为127个字节,然后保存(141-127)=14个字节。
压缩
如果您有更长的字符串,则可以压缩它们(即,使用deflate()
、gzencode()
或gzcompress()
等函数)。要么是直的;在这种情况下,上述字符串变为123字节。操作简单。
压缩许多小字符串:Rube-Goldberg方法
由于压缩算法是学习的,而且一开始他们什么都不敢假设,所以小字符串不会被大大压缩。可以说,他们"都开始了"。就像发动机一样,冷运转时性能较差。
如果你有这些字符串来自的文本"语料库",你可以使用一个耗时的技巧来"预热"压缩引擎,并可能使其性能翻倍(或更好)。
假设您有两个字符串,COMMON
和TARGET
(第二个是您感兴趣的字符串)。如果你对COMMON
进行z压缩,你会得到ZCMN
。如果压缩TARGET
,则会得到ZTRGT
。
但正如我所说,由于gz压缩算法是面向流的,并且它随着时间的推移而学习,因此任何文本的后半部分的压缩率(前提是两半之间没有异常的统计分布变化)总是明显高于前半部分。
所以,如果你压缩,比如说COMMONTARGET
,你会得到ZCMGHQI
。
请注意,字符串的第一部分,直到几乎结束,都与以前相同。事实上,如果压缩COMMONFOOBAR
,就会得到类似ZCMQKL
的内容。第二部分比以前压缩得更好,即使我们将重叠区域计算为完全属于第二个字符串。
这就是诀窍。给定一个字符串族(TARGET
、FOOBAR
、CASTLE BRAVO
),我们压缩的不是字符串,而是那些带有大前缀的字符串的级联。然后我们从结果中丢弃公共压缩前缀。因此,TARGET
是从COMMONTARGET
(即ZCMGHQI
)的压缩中获得的,并且变为GHQI
而不是ZTRGT
,具有20%的增益。
解码器执行相反的操作:给定GHQI
,它首先应用公共压缩前缀ZCM
(,它必须知道);然后对结果进行解码,最后丢弃公共的未压缩前缀,只需要事先知道前缀的长度。
因此,上面的第一句话(140个字符)在被自己压缩时变成了123;如果我将Declaration的其余部分用作前缀,它将压缩到3355个字节。这个前缀加上我的140个字节就变成了3409个字节,其中3352个是常见的,剩下57个字节。
以将未压缩的前缀存储在编码器中一次,将压缩的前缀保存在解码器中一次为代价,以及整个程序的运行速度是原来的五倍,我现在可以将这140个字节减少到57,而不是123,还不到以前的一半。
这个技巧适用于小字符串;对于较大的公司来说,优势不值得付出代价。此外,不同的前缀会产生不同的结果。最好的前缀是那些包含可能出现在字符串池中的大多数序列的前缀,这些前缀按增加的长度排序。
额外的好处是:压缩前缀还兼作一种弱加密,因为如果没有它,你就无法轻松解码压缩字符串,即使可能能够恢复其中的一些片段。