100 more of those BITFIELDs

Salvatore Sanfilippo

再来 100 个 BITFIELD

原文由 Salvatore Sanfilippo 发布,订阅该博客

今天 Redis 满 7 岁了。为了小小地纪念一下,过去这几天我进行了一场充满乐趣的编程马拉松,实现了一个全新的、甚至有点疯狂的命令——BITFIELD。

这个命令的核心想法并不新鲜,过去我和其他人都曾提出过,但从未认真对待过,这个想法总显得有点奇怪。Redis 本来就已经提供了位操作:不少用户非常喜欢它,这是一种以紧凑的方式表示大量数据的有效手段。不过到目前为止,我们都是逐位来处理的——设置、检测、获取某一位,统计某个范围内被置位的位数等等。

那如果实现位域(bitfield)呢?无论是短还是长、任意位宽的整数,放在任意偏移位置上,这样我就可以把一个 Redis 字符串当作由 5 位有符号整数组成的数组来用,一点空间都不浪费。

几天前,来自 Redis Labs 的 Yoav Steinberg 以一种更严肃的方式提议了一组针对存储在位偏移上的任意位宽整数的操作命令。看到那封邮件时我笑了,因为这也算是我的一个秘密梦想。以 Yoav 的提议为起点,再结合其他 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 的位置递增一个 8 位无符号整数(也就是位图中的第 101 位)。

不过,还有另一种指定偏移量的方式,就是在偏移量前加上“#”,意思是:“把字符串当作指定大小的计数器数组,操作第 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

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

评论