「Redis」Redis Bitmap 「 位图 还债篇 」

不积跬步,无以至千里;不积小流,无以成江海。

码字不易,点赞再看。

前言

欠下的债总规要还,上篇介绍基础数据结构中有提到 bitmap。移步至: 传送门

本篇先从一道面试题开始,有这么一个场景:拥有 数亿 用户的电商平台,如何统计月活??

你用 set 记录活跃用户的id,没毛病,确实也可以达到效果,但是使用这种数据结构真的妥当吗??

针对如此宝贵的内存,能省点,不香吗??

以 1亿 用户为例: 用户id 用int存储, 做个比对,引出今天的主角 bitmap。


「Redis」Redis Bitmap 「 位图 还债篇 」

不用我说,已经看出来优势了吧,没错这就是位图牛逼的地方。

正文

Redis官方文档对于位图的介绍如下:

位图不是一个真实的数据类型,而是定义在字符串类型上的面向 位 的操作集合。由于字符串类型是二进制安全的二进制大对象,并且最大长度是 512MB,适合于设置 2^32个不同的位。


位操作分为两组:常量时间单个位的操作,像设置一个位为 1 或者 0,或者获取该位的值。对一组位的操作,例如:在 给定的位 范围内计算 设置位 的数目。


位图的最大优势是:是一种非常显著的节省空间来存储信息的方式。例如,在一个系统中,不同用户由递增的用户 ID 来表示,可以使用 512MB 的内存来表示 40亿 用户的单个位信息(例如他们是否需要接收信件)。

简而言之,位图 是操作 比特位

的,其优点是节省 内存空间。

位图的常用操作如下:
  • setbit 设置特定key对应的比特位的值。
  • getbit 获取特定key对应的比特位的值。
  • bitcount统计给定key对应的字符串比特位为1的数量。

Redis 的位数组是 自动扩充,如果设置了某个偏移位置超出了现有的内容范围,就会 自动 将位数组进行零扩充。

接下来我们使用 redis-cli 设置第一个字符,也就是 位数组 的前 8 位,我们只需要设置值为 1 的位,如 h (二进制:0 1 1 0 1 0 0 0) 字符只有 1/2/4 位需要设置。

<code>

127.0

.0

.1

:6379>

 

setbit

 

s

 

1

 

1

(integer)

 

0

127.0

.0

.1

:6379>

 

setbit

 

s

 

2

 

1

(integer)

 

0

127.0

.0

.1

:6379>

 

setbit

 

s

 

4

 

1

(integer)

 

0

127.0

.0

.1

:6379>

 

get

 

s

"h"

/<code>

上面这个例子可以理解为「零存整取」,同样我们还也可以「零存零取」,「整存零取」。「零存」就是使用 setbit 对位值进行逐个设置,「整存」就是使用字符串一次性填充所有位数组,覆盖掉旧值。

零存零取

<code>

127.0

.0

.1

:6379>

 

setbit

 

a

 

1

 

1

(integer)

 

0

127.0

.0

.1

:6379>

 

setbit

 

a

 

2

 

1

(integer)

 

0

127.0

.0

.1

:6379>

 

setbit

 

a

 

4

 

1

(integer)

 

0

127.0

.0

.1

:6379>

getbit

 

a

 

1

  

//获取某个具体位置的值

 

0

/1

(integer)

 

1

/<code>

整存零取

<code>

127.0

.0

.1

:6379>

 

set

 

b

 

h

OK

127.0

.0

.1

:6379>

 

getbit

 

b

 

1

(integer)

 

1

127.0

.0

.1

:6379>

 

getbit

 

b

 

2

(integer)

 

1

127.0

.0

.1

:6379>

 

getbit

 

b

 

3

(integer)

 

0

127.0

.0

.1

:6379>

 

getbit

 

b

 

4

(integer)

 

1

/<code>

如果对应位的字节是不可打印字符,redis-cli 会显示该字符的 16 进制形式。

<code>

127.0

.0

.1

:6379>

 

setbit

 

x

 

0

 

1

(integer)

 

0

127.0

.0

.1

:6379>

 

setbit

 

x

 

1

 

1

(integer)

 

0

127.0

.0

.1

:6379>

 

get

 

x

"\xc0"

/<code>
统计和查找

Redis 提供了位图统计指令 bitcount 和位图查找指令 bitpos,bitcount 用来统计指定位置范围内 1 的个数,bitpos 用来查找指定范围内出现的第一个 0 或 1。

比如我们可以通过 bitcount 统计用户一共签到了多少天,通过 bitpos 指令查找用户从哪一天开始第一次签到。如果指定了范围参数[start, end],就可以统计在某个时间范围内用户签到了多少天,用户自某天以后的哪天开始签到。

遗憾的是, start 和 end 参数是 字节索引,也就是说指定的位范围必须是 8 的倍数,这点比较坑,而不能任意指定。这很奇怪,不是很能理解 Antirez 为什么要这样设计。因为这个设计,我们无法直接计算某个月内用户签到了多少天,而必须要将这个月所覆盖的字节内容全部取出来 (getrange 可以取出字符串的子串) 然后在内存里进行统计,这个非常繁琐。

还可以将放入位图的offset统一乘以8(一个字节占8比特),这样一来就可以直接用redis的bitcount来统计对应索引范围内的比特值为1的数量了,当然这种方案的缺点也相当明显,就是浪费内存,因为原先只需要1比特存储的数据,现在需要8比特存储,所以这种方案不能很好地利用位图索引节省存储空间的特点。

接下来我们简单试用一下 bitcount 指令和 bitpos 指令:

<code>

127.0

.0

.1

:6379>

 

set

 

w

 

hello

OK

127.0

.0

.1

:6379>

 

bitcount

 

w

(integer)

 

2

1127.0

.0

.1

:6379>

 

bitcount

 

w

 

0

 

0

  

//

 

第一个字符中

 

1

 

的位数

(integer)

 

3

127.0

.0

.1

:6379>

 

bitcount

 

w

 

0

 

1

  

//

 

前两个字符中

 

1

 

的位数

(integer)

 

7

127.0

.0

.1

:6379>

 

bitpos

 

w

 

0

  

//

 

第一个

 

0

 

(integer)

 

0

127.0

.0

.1

:6379>

 

bitpos

 

w

 

1

  

//

 

第一个

 

1

 

(integer)

 

1

127.0

.0

.1

:6379>

 

bitpos

 

w

 

1

 

1

 

1

  

//

 

从第二个字符算起,第一个

 

1

 

(integer)

 

9

127.0

.0

.1

:6379>

 

bitpos

 

w

 

1

 

2

 

2

  

//

 

从第三个字符算起,第一个

 

1

 

(integer)

 

17

/<code>


点关注 不迷路

以上就是这篇的全部内容,能看到这里的都是 人才。

如果你从本篇内容有收获,求 点赞,求 关注,求 转发 ,让更多的人学习到。

如果本文有任何错误,请批评指教,不胜感激 !


分享到:


相關文章: