Stream 不是把每条消息单独存成一个对象。它把很多条消息打包进一段 listpack,再用一棵 radix tree 按消息 ID 把这些 listpack 组织起来。这篇不靠推测:直接用 DUMP 拿到 Stream 的序列化数据,把里面的 key 和 listpack 拆开看,再用几组实验量一量这个结构的内存和查找代价。
写入的 10 个 ID: ['1000-0', '1007-1', '1014-2', '1021-0', '1028-1', '1035-2', '1042-0', '1049-1', '1056-2', '1063-0'] DUMP 长度 249 字节;第 1 字节(RDB 类型号)= 27 RDB 里 listpack 节点数 = 3 (与 XINFO radix-tree-keys = 3 ) 节点 0: key 长 16 字节 = 大端(ms=1000, seq=0);listpack 61 字节,头部记录 61 字节 / 25 个元素 节点 1: key 长 16 字节 = 大端(ms=1028, seq=1);listpack 62 字节,头部记录 62 字节 / 25 个元素 节点 2: key 长 16 字节 = 大端(ms=1056, seq=2);listpack 42 字节,头部记录 42 字节 / 15 个元素 对照第 1、5、9 条的 ID: 1000-0 1028-1 1056-2
RDB 序列化 Stream 时,直接写出的就是「若干个(radix key, listpack)对」。解出来:key 恒为 16 字节,前 8 字节是毫秒、后 8 字节是序号(大端),值就是该节点第一条消息的 ID:第 1、5、9 条的 ID 正好是三个 key。listpack 头部记录的总字节数与实际长度一致;4 条消息的节点有 25 个元素,2 条的有 15 个元素。
这些元素数字与「每条消息固定占 5 个元素、节点另有 5 个元素做头部」的算式吻合(4×5+5=25,2×5+5=15),但那是我对这组数字的解读,没有另外拆解每个元素去验证。
entries radix-keys radix-nodes entries/key MEMORY USAGE B/entry
1 1 1 1.0 306 306.0
50 1 1 50.0 1189 23.8
100 1 1 100.0 2090 20.9
101 2 2 50.5 2167 21.5
1000 10 9 100.0 18828 18.8
10000 100 69 100.0 185870 18.6
100000 1000 671 100.0 1854662 18.510 万条消息只有 1000 个 radix key,平均每个 key 装 100 条;101 条时就变成 2 个。radix-tree-nodes(树内部节点数)比 key 数小,671 对 1000:key 之间共享前缀,树把它们压缩了。每条消息的内存从 1 条时的 306 字节掉到大批量时的 18.5 字节,因为固定开销被节点里的几十上百条消息摊薄。
max-entries radix-keys radix-nodes MEMORY USAGE B/entry
1 10000 77 552053 55.2
10 1000 77 218553 21.9
100 100 68 185675 18.6
1000 10 14 189088 18.9
10000 1 1 188958 18.9把 stream-node-max-entries 从 1 调到 10000(同时关掉字节上限),同样 10000 条:max-entries=1 时 10000 个 key、每条 55.2 字节;10 时降到 21.9 字节;100 时 18.6 字节;再往大调收益消失(18.9 和 18.9 字节,反而略高于 100)。
value bytes radix-keys entries/key
10 10 100.0
100 28 35.7
500 143 7.0
2000 500 2.0默认还有 4096 字节的上限:消息值 10 字节时 1000 条只要 10 个节点,值 100 字节时 28 个,500 字节时 143 个,2000 字节时 500 个(每个节点只装 2 条)。所以「每节点 100 条」只是上限,消息大了先撞到的是字节上限。
相同字段名: 161519 B (16.2 B/条) 每条不同字段名: 317583 B (31.8 B/条) 比值 1.97
同样是 10000 条、每条 2 个字段:字段名都相同时 161519 字节(16.2 字节/条),每条字段名都不一样时 317583 字节(31.8 字节/条),约 1.97 倍。字段名相同的消息不用重复存字段名,这是 Stream 比「每条一个 Hash」省内存的原因之一(这里只对比了 Stream 自身两种写法,没有对比 Hash)。
N= 1000 radix-keys= 10 20000 次点查 85.3 ms ( 4.3 µs/次) N= 100000 radix-keys= 1000 20000 次点查 86.2 ms ( 4.3 µs/次) N= 1000000 radix-keys= 10000 20000 次点查 89.5 ms ( 4.5 µs/次)
流从 1000 条涨到 100 万条(radix key 从 10 涨到 10000),点查耗时从 4.3 µs 到 4.5 µs,几乎不变。注意:这里含一次管道里的网络往返摊销,只能说明「差别很小」,不能拆出树查找本身的耗时;100 万条那一组,被查的 ID 取自流的头部和尾部各 1 万条,不是全范围均匀随机。