# 书接上回
上一篇我们学习的 zset
集合这一数据类型。其内部是由 skiplist
和 hashtable
这种两种数据结构编码的。
如果不记得了,那就来坐穿梭机回去看看吧。 开始穿梭
接下来,我们继续学习一个新的 "数据类型"
, 位图, bitmap
.(注意啦,数据类型,我加了引号!!)
# bitmap
简介
那么什么是 bitmap
, 我们先从名字上来说, bit
,计算机中最小的单位,一个 bit
表示一个二进制位。 map
,映射,图。所以, bitmap
就是表示对二进制位的映射。那具体是怎么回事呢? bitmap
其实就是以字符串的形式对二进制位进行操作,从而达到节约空间占用的作用。
而且, bitmap
,中文的意思就是位图。 不知道你用没用过 C
语言的位图结构,他们的目的都是一样。
可能你还是不理解,别急,往下看。
bitmap
怎么用呢?
恰巧,前几天小编在面试中被问到过这个问题。
面试官说: 有个业务场景:我们这边有 10000 家线下门店,我们要对部分门店进行广告投放,怎么记录某条广告投放给哪家门店里了呢?我们的门店 id
是递增的。
首先我们可以有三张表,广告表 ( G
表),门店表( M
表),广告门店关联表( GM
表)。
如果我们投放广告的时候,把每条广告和每个门店的关联作为一条数据插入到 GM
表中,那么这个表中的数据量增长还是非常快的。如果有 10
万家门店呢?广告的投放和撤销比较频繁。也就是说数据的修改也是比较频繁的。那怎么搞呢?
门店的 id
是递增的,那么我们可以这么搞,用 10001
个二进制位表示 10000
家门店的 id,如果该门店投放了该广告,就把该位置上的数字置为 1
。并且每四个字节转换成一个整数型数,将( 10000/8/4
) 313
个整形数按照一定的规则转换成字符串存储到 GM
表中。表示一条广告已经投放的门店。这样我们就使用了 0.1kb
左右的空间记录了某条广告投放门店的情况。
其实呢,这就是使用 bitmap
进行存储的。
接下来,我们看下如何使用 位图。
# bitmap
的基本命令
SETBIT
setbit key offset value
对 key
所储存的字符串值,设置或清除指定偏移量上的位 ( bit
)。
位的设置或清除取决于 value
参数,可以是 0
也可以是 1
。
当 key
不存在时,自动生成一个新的字符串值。
字符串会进行伸展 ( grown
) 以确保它可以将 value
保存在指定的偏移量上。当字符串值进行伸展时,空白位置以 0
填充。
offset
参数必须大于或等于 0
,小于 2^32
( bit
映射被限制在 512 MB
之内)。
假设我们使用 位图来存储 Redis
这个字符串。
在演示这个命令之前,我们首先来看下 如何使用二进制来表示字符串。
Redis
中的 R
对应 ASCII
码是: 82
. 转化成二进制就是: 0b01010010
, 其他字符依次如下所示:
Redis
中的 e
对应 ASCII
码是: 101
. 转化成二进制就是: 0b01100101
,
Redis
中的 d
对应 ASCII
码是: 100
. 转化成二进制就是: 0b01100100
,
Redis
中的 i
对应 ASCII
码是: 105
. 转化成二进制就是: 0b01101001
,
Redis
中的 s
对应 ASCII
码是: 115
. 转化成二进制就是: 0b01110011
。
看到这里,我要告诉你一个知识点:对于位图的操作,我们可以实现 零存整取
。 什么意思呢?
我来给你演示一下
如上图, R
这个字符,对应的每个二进制上的数。由于位图会自动填充空位为 0,所以我们只需要设置二进制位上为 1
的就可以了。具体命令如下
1 | 127.0.0.1:6379> setbit k87 1 1 |
解释一下,我们根据 R
每个二进制位上数值设置到 k87 中,然后通过 get k87
这个命令,返回了 R
。 这就是所谓的 零存整取
。如果我继续设置剩下的 edis
字符串呢?
1 | 127.0.0.1:6379> setbit k87 9 1 |
这里你就可能说,有谁会这样使用,自己计算出每个位,然后去获取完整的? 是啊,不过这里只是一个例子,来说明 位图 零存整取
功能,接下来,我们接着看 位图 整存零取
的功能。
这里呢,就要介绍 gitbit
这个命令。
GETBIT
GETBIT key offset
对 key
所储存的字符串值,获取指定 offset
上的位 ( bit
).
如果 key
不存在,获取 offset
超出范围,返回 0
.
首先我们设置 一个 string
类型的字符串到 Redis
中,然后依次获取每一位上的值。
可以和下图进行比对。
1 | 127.0.0.1:6379> set k88 Redis |
这个就是 位图提供的 整存零取
的功能了。综合上面的两项功能,我们轻而易举的就可以得出 Redis 的位图是可以 零存零取
的。就是使用 setbit
和 gitbit
命令了。这里就不演示了。
BITCOUNT
bitCount key [start] [end]
计算指定 key
的对应字符串,被设置为 1
的比特位的数量。
还是以上面的 Redis
为例,一共是 19
位 1
. 我们来演示一下
1 | # 不存在的时候,返回0 |
这个适合什么场景下使用呢? 比如,我们要计算某个用户登录天数。第一天登录的时候,我们 可以设置 setbit user1 1 1
, 第二天设置 setbit use1 2 1
, 那么使用 bitcount user1
就能知道该用户总共的登录天数了,也能够计算出该用户在哪天登录了。
BITPOS
bitpos key bit [start] [end]
返回 key 的指定区段内容,第一个 bit 的位置。
比如我们以 Redis
为例,如下图
1 | 127.0.0.1:6379> set key90 Redis |
BITOP
BITOP operation destkey key [key …]
对一个或多个保存二进制位的字符串 key 进行位元操作,并将结果保存到 destkey 上。
operation
可以是 AND
、 OR
、 NOT
、 XOR
这四种操作中的任意一种:
BITOP AND destkey key [key ...]
,对一个或多个 key
求逻辑并,并将结果保存到 destkey
。
BITOP OR destkey key [key ...]
,对一个或多个 key
求逻辑或,并将结果保存到 destkey
。
BITOP XOR destkey key [key ...]
,对一个或多个 key
求逻辑异或,并将结果保存到 destkey
。
BITOP NOT destkey key
,对给定 key
求逻辑非,并将结果保存到 destkey
。
除了 NOT
操作之外,其他操作都可以接受一个或多个 key
作为输入。
这里我们做一个简单例子。
1 | 127.0.0.1:6379> setbit k91_1 0 1 |
BITFIELD
BITFIELD key [GET type offset] [SET type offset value] [INCRBY type offset increment] [OVERFLOW WRAP|SAT|FAIL]
BITFIELD
命令可以将一个 Redis
字符串看作是一个由二进制位组成的数组, 并对这个数组中储存的长度不同的整数进行访问 (被储存的整数无需进行对齐)。 换句话说, 通过这个命令, 用户可以执行诸如 “对偏移量 1234
上的 5
位长有符号整数进行设置”、 “获取偏移量 4567
上的 31
位长无符号整数” 等操作。 此外, BITFIELD
命令还可以对指定的整数执行加法操作和减法操作, 并且这些操作可以通过设置妥善地处理计算时出现的溢出情况。
这是一个比较复杂的命令了。我们依次来看看。
GET type offset
type
是什么呢? type
是返回的数值类型。可选值有: i8
(有符号 8
个 bit
位), i16
(有符号 16
个 bi
t 位), u8
(无符号 8
个 bit
位), u16
(无符号 16
个 bit
位),…
这个的命令的功能就是返回执行的二进制的范围,offset 是指第几个二进制位。
注意:
BITFIELD
命令最大支持 64
位长的有符号整数以及 63
位长的无符号整数, 其中无符号整数的 63
位长度限制是由于 Redis
协议目前还无法返回 64
位长的无符号整数而导致的。
用下面这个例子来演示一下。
1 | 127.0.0.1:6379> set k92 Redis |
结合上面的那张图,我们看下
我们手动的将前 8
个 bit
位装换成 10
进制,就是 82
。高位为 0
,所以此时, u8
和 i8
的值是一样的。同样的 u16
, i16
, u24
, u48
, 大家可以自行验证一下。
SET type offset value
这个命令是和 GET
命令相反的一个命令。
直接用一个例子来演示一下。
1 | 127.0.0.1:6379> bitfield k93 SET u8 0 82 |
INCRBY type offset increment
从制定的偏移位开始,增加 increment
。
什么意思呢?
来演示一下:
字符 A
的 ASCII
码是 65
, R
是 82
,如果从 A
变成 R
,需要增加 17
。
1 | 127.0.0.1:6379> set k94 A |
OVERFLOW WRAP|SAT|FAIL
这是 bitmap
对 INCRBY
命令执行时,发生异常行为的控制。
用户可以通过 OVERFLOW
命令以及以下展示的三个参数, 指定 BITFIELD
命令在执行自增或者自减操作时, 碰上向上溢出( overflow
)或者向下溢出( underflow
)情况时的行为:
WRAP
: 使用回绕( wrap around
)方法处理有符号整数和无符号整数的溢出情况。 对于无符号整数来说, 回绕就像使用数值本身与能够被储存的最大无符号整数执行取模计算, 这也是 C
语言的标准行为。 对于有符号整数来说, 上溢将导致数字重新从最小的负数开始计算, 而下溢将导致数字重新从最大的正数开始计算。 比如说, 如果我们对一个值为 127
的 i8
整数执行加一操作, 那么将得到结果 -128
。
SAT
: 使用饱和计算( saturation arithmetic
)方法处理溢出, 也即是说, 下溢计算的结果为最小的整数值, 而上溢计算的结果为最大的整数值。 举个例子, 如果我们对一个值为 120
的 i8
整数执行加 10
计算, 那么命令的结果将为 i8
类型所能储存的最大整数值 127
。 与此相反, 如果一个针对 i8
值的计算造成了下溢, 那么这个 i8 值将被设置为 -127
。
FAIL
: 在这一模式下, 命令将拒绝执行那些会导致上溢或者下溢情况出现的计算, 并向用户返回空值表示计算未被执行。
我们演示一下这几种情况
1 | # 使用 WRAP 方式,重复执行相同的命令,在一个2bit上,从 0,1,2,3 来回折返。 |
BITFIELD
命令的作用在于它能够将很多小的整数储存到一个长度较大的位图中, 又或者将一个非常庞大的键分割为多个较小的键来进行储存, 从而非常高效地使用内存, 使得 Redis 能够得到更多不同的应用 —— 特别是在实时分析领域: BITFIELD
能够以指定的方式对计算溢出进行控制的能力, 使得它可以被应用于这一领域。
以上就是 Redis
中 bitmap
相关的命令了。下面我简单的来看下 bitmap 这种 "数据结构"
是如何实现的。
注意,这里的数据结构我还是加上了引号。具体为什么呢?
# bitmap
的实现
话不多说,我们直接去看看源码中是怎么实现的。直接搜文件 bit
我们要看 src
目录下的,所以,直接看 bitops.c
这里我们就是以一个命令 setbit
为例简单的来看看 bitmap
的运行过程。
1 |
|
上面代码就是 setBit
命令的实现过程了,这篇文章中我们不做研究我们只看第 27
行. 这一行中有一个 lookupStringForBitCommand
方法。在这里我们猜测一下, bitmap
其实就是一个 OBJ_STRING
类型的结构。
到底是不是呢?
看一下源码。
1 | /** |
在第 17
行和第 23
行。就很清楚了。
所以,在文章一开始加上引号的 数据结构 ,谜题就解开了。
bitmap
并不是一个新的数据结构,本质上是用 STRING
这个数据结构来实现的。
如果你想看 bitmap
的全部源码,那么满足你!!👉👉 点击这里👈👈
公众号里回复 redis源码
,即可获取完整路径哦
虽然 redis 源码不是我写的,但是我看过的都加上注释啦~
除此之外,小白还自己实现了一个 bitmap
。感兴趣的朋友欢迎 star
.👉👉 bitMap 源码,点击这里 👈👈
公众号里回复 bitmap
, 即可获取完整路径哦
当然: JDK 也提供了一个 BitMap
的实现,叫 BitSet
,位于 java.util
包下。其底层使用的是一个 long
类型的数组,一个 long
代表一个 word
。但 BitSet
没有解决上面提到的输入稀疏的问题。谷歌开源的 EWAHCompressedBitMap
解决了输入稀疏的问题。
这里我们总结一下。
Redis 的数据类型 |
底层实现结构 | 文章参考 |
---|---|---|
STRING |
sds |
数据类型之 String |
list |
quicklist , ziplist |
数据类型之 list |
hash |
ziplist , dict |
数据类型之 hash |
set |
dict , intset |
数据类型之 set |
zset |
dict , skipList |
数据类型之 zset |
bitmap |
sds |
数据类型之 bitmap |
# 最后
希望与你一起遇见更好的自己