Redis BitField 详解:原理、命令与实战

· 1 分钟阅读 ·

Redis中存在许多方便又有用的数据结构,这也是它作为缓存中间件如此流行的重要原因之一。

最常见的例如String、Hash、List、Set,几乎每个学过Redis的人都知道。除了这些之外,还有一些不那么常用但很有趣的数据结构,例如BitMap、HyperLogLog、BloomFilter等,它们在特定场景下往往能发挥意想不到的效果。

今天我们就来学习一个比较特殊的数据结构:BitField

从BitMap说起

在介绍BitField之前,我估计BitMap是更为知名的一种数据结构。这种数据结构通过一个二进制位的0和1来表示一个状态值,可以实现比特维度的SET、GET和统计操作。

一个典型的使用案例是利用BitMap来统计月度的签到数据:哪一天签到了就在对应的比特位上SET为1,然后通过Redis提供的BITCOUNT命令拿到值为1的比特位数量,也就实现了签到天数的统计。

理解了BitMap的使用场景后,我们可以继续延伸思考一下:

  • 如果我要存的某个用户数据不只有0和1两个状态怎么办?例如我想存取用户年龄(0-127)。
  • 如果我想存储用户的多个数值类型的数据怎么办?例如用户等级(0-15)、用户年龄(0-127)、当月签到天数(0-31)。

答案其实有许多种。要存储一个纯数值类型的数据,依旧可以使用BitMap存储并通过位运算来实现,或者直接使用String存储;要存储多个数值类型的数据,也可以使用BitMap自行进行位运算处理,或者更容易的方式是使用List或者Hash结构存储。

在这些容易想到的答案之外,其实我们还有一个方案:Redis的BitField

如果说BitMap控制的粒度精确到了每个bit,那么BitField控制的粒度则是一组bit

命令与核心概念

BitField常用的命令是这样的:

# GET
BITFIELD key GET encoding offset
# SET
BITFIELD key SET encoding offset value
# INCRBY
BITFIELD key INCRBY encoding offset increment

当然,这些子命令也可以灵活组合到同一个长命令中,Redis中执行起来都是原子性的。

我们可以看到BITFIELD命令使用起来有两个比较陌生的概念:encoding、offset。

encoding

encoding代表了编码,也就是这组比特采用的数字类型,有uN和iN两种:

  • uN:无符号N位。比如u4代表无符号四位比特,从0000到1111,即0-15;
  • iN:有符号N位。比如i8表示有符号8位比特,取值从-128到127。

需要注意的是,无符号编码最大支持到u63,有符号编码最大支持到i64。

offset

offset代表的是比特的偏移量。比如你使用BitField存储了4位用于变量1、4位用于变量2,那么使用0作为offset可以定位到变量1的值,使用4作为offset可以定位到变量2的值。

还有一种特殊写法:如果你存储的各个数据大小都是一样的,offset可以使用#N的方式直接定位到第N个变量(从0开始计数),此时真正的偏移量等于N × 编码位宽

实战一:压缩存储用户信息

直接拿我们之前举的用户数据来设计一下:

字段编码取值范围offset
等级u40-150
年龄u70-1274
签到天数u50-3111
bits:  xxxx | xxxxxxx | xxxxx
        u4      u7       u5

仅仅用了16个比特位,我们就能存储一条用户的信息。

使用BitField插入一条用户信息的命令是这样的:

# 设置用户0001的数据:等级0,年龄33,签到天数10
BITFIELD user:0001 SET u4 0 0 SET u7 4 33 SET u5 11 10

设置用户0001的等级+1是这样的:

BITFIELD user:0001 INCRBY u4 0 1

实战二:批量存储学生成绩

再举一个例子,假如我们要存储100名学生的成绩(0-100),每个成绩用u7就足够了。由于所有字段的大小一致,正好可以用#N来定位:

bits:   xxxxxxx | xxxxxxx | xxxxxxx | ...
          #0        #1        #2

设置第59名学生(下标从0开始)的成绩为60分,命令是这样的:

BITFIELD class:0001 SET u7 #59 60

通过以上两个案例我们可以看到,如果应用到一组数值型的数据上,使用BitField大大节省了空间,而且有方便直接操作一组比特的命令。

溢出机制

BitField还有一个比较特殊的机制:溢出机制。相关的命令是这样的:

BITFIELD key OVERFLOW WRAP|SAT|FAIL

BitField控制的是指定编码的一组比特位,如果数值超出了范围会怎么样?我们可以选择不同的溢出机制:

  • WRAP(默认机制):代表循环,也就是最大值+1变成最小值,最小值-1变成最大值,这种行为和C语言中的整型溢出机制是一致的。
  • SAT:代表饱和,也就是最大值+1还是最大值,最小值-1依旧是最小值。
  • FAIL:代表失败。这种情况下命令不会执行成功,返回值将是NULL(nil)。

还有一点需要说明,一个OVERFLOW命令的影响范围是其后跟随的子命令列表中的SET和INCRBY命令,直到碰到下一个OVERFLOW命令。

总结

以上就是Redis中BitField的相关机制介绍了。

总的来说,以后如果需要存储和操作一组大小确定、且长度相同的数值类型数据,完全可以考虑使用BitField来节省空间。

当然,可读性和可维护性也很重要。如果你对位运算相关的概念还是一知半解,对内存的占用也不敏感,那使用Hash等数据结构也无可厚非。永远不要为了技术本身而使用技术,在合适的场景下使用BitField才能如虎添翼。