100 more of those BITFIELDs

Salvatore Sanfilippo

再来 100 个这样的 BITFIELD

今天 Redis 已经 7 岁了,为了稍微纪念一下这个时刻,我在过去的几天里进行了一场有趣的编程马拉松,实现了一个名为 BITFIELD 的疯狂新命令。

这个命令的本质并不新鲜,过去我和其他人都曾提出过,但从未以严肃的方式提出,这个想法总显得有点奇怪。我们在 Redis 中已经有了位操作:某些用户非常喜欢它,这是一种以紧凑的方式表示大量数据的好方法。然而到目前为止,我们都是单独处理每一位,设置、测试、获取位,统计某个范围内所有被置位的位数,等等。

那如果实现位域会怎样呢?短的或大的、任意大小的整数,位于任意偏移量上,这样我就可以把一个 Redis 字符串当作一个由 5 位有符号整数组成的数组来使用,而不会浪费任何一点空间。

几天前,来自 Redis Labs 的 Yoav Steinberg(约阿夫·斯坦伯格)以一种更严肃的方式提出了一组针对存储在位偏移上的任意大小整数的命令。读到这封邮件时我笑了,因为这有点算是我的一个秘密梦想。以约阿夫·斯坦伯格的提议为起点,并结合 Redis Labs 其他工程师的反馈,我编写了一个带子命令的单一命令的初步规范,使用类型定义的简短名称,并对溢出语义增加了非常细粒度的控制。

我在几分钟前刚刚完成了第一版实现,因为计划就是在今天发布它,希望 Redis 会领情,知道我们在它生日这天确实干了活。

最终的 BITFIELD 命令支持不同的子命令:

SET <type> <offset> <value> — 设置指定的值并返回其旧值。

GET <type> <offset> — 获取指定的值。

INCRBY <type> <offset> <increment> — 递增指定的计数器。

还有一个名为 OVERFLOW 的额外元命令,用于设置(猜猜是什么)后续命令的溢出语义(因此 OVERFLOW 可以被指定多次):

OVERFLOW SAT — 饱和,即向某一方向溢出时,会将整数饱和至该溢出方向上的最大值。

OVERFLOW WRAP — 这是通常的回绕,但有趣的是,这对有符号整数也同样有效,会向最负或最正的值回绕。

OVERFLOW FAIL — 在此模式下,如果值会导致溢出,则完全不执行该操作。

整数类型可以使用“u”或“i”前缀后跟位数来指定,因此例如 u8、i5、u20 和 i53 都是有效的类型。有一个限制:目前无法指定 u64,因为 Redis 协议目前无法返回 64 位无符号整数。

是时候举几个例子了:为了递增一个 8 位无符号整数,我可以这样做:

127.0.0.1:6379> BITFIELD mykey incrby u8 100 1
1) (integer) 3

这是在偏移量 100 处(位图中的第 101 位)递增一个 8 位无符号整数。

不过还有另一种指定偏移量的方式,即在偏移量前加上“#”,意思是:“将字符串视为指定大小的计数器数组,并设置第 N 个计数器”。基本上这仅仅意味着,如果我对 8 位类型使用 #10,偏移量是通过 8*10 相乘得到的,这样我就可以独立地寻址多个计数器,而无需进行偏移量计算:

127.0.0.1:6379> BITFIELD mykey incrby u8 #0 1
1) (integer) 1
127.0.0.1:6379> BITFIELD mykey incrby u8 #0 1
1) (integer) 2
127.0.0.1:6379> BITFIELD mykey incrby u8 #1 1
1) (integer) 1
127.0.0.1:6379> BITFIELD mykey incrby u8 #1 1
1) (integer) 2

控制溢出的能力也很有趣。例如,一个 1 位的无符号计数器在使用默认的“wrap”溢出策略时,实际上会在 0 和 1 之间切换:

127.0.0.1:6379> BITFIELD mykey incrby u1 100 1
1) (integer) 1
127.0.0.1:6379> BITFIELD mykey incrby u1 100 1
1) (integer) 0
127.0.0.1:6379> BITFIELD mykey incrby u1 100 1
1) (integer) 1
127.0.0.1:6379> BITFIELD mykey incrby u1 100 1
1) (integer) 0

如你所见,它在 0 和 1 之间交替。

饱和也很有用:

127.0.0.1:6379> bitfield mykey overflow sat incrby i4 100 -3
1) (integer) -3
127.0.0.1:6379> bitfield mykey overflow sat incrby i4 100 -3
1) (integer) -6
127.0.0.1:6379> bitfield mykey overflow sat incrby i4 100 -3
1) (integer) -8
127.0.0.1:6379> bitfield mykey overflow sat incrby i4 100 -3
1) (integer) -8

如你所见,每次递减 3 永远不会低于 -8。

请注意,你可以在单个命令中执行多个操作。它总是返回一个结果数组:

127.0.0.1:6379> BITFIELD mykey get i4 100 set u8 200 123 incrby u8 300 1
1) (integer) -8
2) (integer) 123
3) (integer) 7

该命令的预期用途是实时分析、A/B 测试、通过整数的溢出来每次向用户展示略有不同的内容。以共享且内存高效的方式打包如此多的小计数器,可以通过多种方式加以利用,但这就留给 Redis 社区才华横溢的程序员们去探索了。

该命令将在未来几周内向后移植到 Redis 的稳定版本中,因此很快就可以使用了。

对实现感到好奇吗?它可能比你预期的要复杂一些:https://github.com/antirez/redis/commit/70af626d613ebd88123f87a941b0dd3570f9e7d2

原文由 Salvatore Sanfilippo 发布

本文章由 muse-spark-1.2-contributor 进行翻译