Redis 亿级用户信息存储实践:bitmap 位图存储_java


bitmap简介

8 个 bit 组成一个 Byte,所以 bitmap 极大的节省储存空间

你可以把它理解为一个特殊处理过的 字符串
key代表业务属性、标签。一个 bit 位来表示某个元素对应的值或者状态。

bitmap 并不是一种数据结构,实际上它就是字符串,但是可以对字符串的位进行操作。

bitmap有自己的一套命令。可以把bitmap想象成一个以bit为单位的数组,数组的每个单元存储0和1,数组的下标叫做偏移量。

Redis 提供 setbit,getbit,bitcount等几个 bitmap 相关命令。但其实 setbit 等命令只不过是在 set 上的扩展而已。

setbit 命令介绍

指令

SETBIT key offset value

复杂度 O(1)

设置或者清空 key 的 value(字符串)在 offset 处的 bit 值(只能只 0 或者 1)。

空间占用、以及第一次分配空间需要的时间
在一台 2010MacBook Pro 上

offset 为 2^32-1(分配 512MB)需要~ 300ms
offset 为 2^30-1(分配 128MB)需要~ 80ms
offset 为 2^28-1(分配 32MB)需要~ 30ms
offset 为 2^26-1(分配 8MB)需要 8ms。
– <来自官方文档: ​​​https://redis.io/commands/setbit​​​>
大概的空间占用计算公式是:($offset/8/1024/1024)MB

Available since 2.2.0.

Time complexity: O(1)

Sets or clears the bit at offset in the string value stored at key.

The bit is either set or cleared depending on value, which can be either 0 or 1.

When key does not exist, a new string value is created. The string is grown to make sure it can hold a bit at offset. The offset argument is required to be greater than or equal to 0, and smaller than 232 (this limits bitmaps to 512MB). When the string at key is grown, added bits are set to 0.

Warning: When setting the last possible bit (offset equal to 232 -1) and the string value stored at key does not yet hold a string value, or holds a small string value, Redis needs to allocate all intermediate memory which can block the server for some time. On a 2010 MacBook Pro, setting bit number 232 -1 (512MB allocation) takes ~300ms, setting bit number 230 -1 (128MB allocation) takes ~80ms, setting bit number 228 -1 (32MB allocation) takes ~30ms and setting bit number 226 -1 (8MB allocation) takes ~8ms. Note that once this first allocation is done, subsequent calls to ​​SETBIT​​ for the same key will not have the allocation overhead.

Redis bitmap 的命令

bitmap的命令

常用命令 作用
1、​​​getbit key offset​​​ 用于获取Redis中指定key对应的值,中对应offset的bit
2、​​​setbit key key offset value​​​ 用于修改指定key对应的值,中对应offset的bit
3、 ​​​bitcount key [start end]​​​ 用于统计字符串被设置为1的bit数
4、​​​bitop and/or/xor/not destkey key [key …]​​ 用于对多个key求逻辑与/逻辑或/逻辑异或/逻辑非

设置 offset 位的 01 值

127.0.0.1:6379> setbit user10001 0 1
(integer) 0
127.0.0.1:6379> setbit user10001 3 1
(integer) 0
127.0.0.1:6379> getbit user10001 0
(integer) 1
127.0.0.1:6379> getbit user10001 1
(integer) 0
127.0.0.1:6379> getbit user10001 2
(integer) 0
127.0.0.1:6379> getbit user10001 3
(integer) 1
127.0.0.1:6379> getbit user10001 7
(integer) 1

批量设置 offset 位的 01 值

使用 u1 类型批量设置 offset 位的 01 值:

127.0.0.1:6379> bitfield user10001 set u1 2 1 set u1 9 1 set u1 10 1
1) (integer) 0
2) (integer) 0
3) (integer) 0
127.0.0.1:6379> getbit user10001 10
(integer) 1
127.0.0.1:6379> getbit user10001 9
(integer) 1
127.0.0.1:6379> getbit user10001 8
(integer) 0
127.0.0.1:6379> getbit user10001 7
(integer) 1

bitmap的应用场景


Redis 亿级用户信息存储实践:bitmap 位图存储_java_02


使用经验:
type = string,BitMap 是 sting 类型,最大 512 MB。
注意 setbit 时的偏移量,可能有较大耗时
位图不是绝对好。

通过 bitcount可以很快速的统计,比传统的关系型数据库效率高很多

1、比如统计年活跃用户数量

用户的ID作为offset,当用户在一年内访问过网站,就将对应offset的bit值设置为“1”;

通过bitcount 来统计一年内访问过网站的用户数量

2、比如统计三天内活跃用户数量

时间字符串作为key,比如 “190108:active“ “190109:active”“190110:active” ;

用户的ID就可以作为offset,当用户访问过网站,就将对应offset的bit值设置为“1”;

统计三天的活跃用户,通过bitop or 获取一周内访问过的用户数量

3、连续三天访问的用户数量 bitop and

4、三天内没有访问的用户数量 bitop not

5、统计在线人数 设置在线key:“online:active”,当用户登录时,通过setbit设置

bitmap的优势,以统计活跃用户为例

每个用户id占用空间为1bit,消耗内存非常少,存储1亿用户量只需要12.5M

使用场景: 统计活跃用户

使用时间作为 cacheKey,然后用户 ID 为 offset,如果当日活跃过就设置为 1
那么我该如果计算某几天/月/年的活跃用户呢(暂且约定,统计时间内只有有一天在线就称为活跃),有请下一个 redis 的命令
命令

BITOP operation destkey key [key ...]

说明:对一个或多个保存二进制位的字符串 key 进行位元操作,并将结果保存到 destkey 上。
说明:BITOP 命令支持 AND 、 OR 、 NOT 、 XOR 这四种操作中的任意一种参数

//日期对应的活跃用户
$data = array(
'2020-01-10' => array(1,2,3,4,5,6,7,8,9,10),
'2020-01-11' => array(1,2,3,4,5,6,7,8),
'2020-01-12' => array(1,2,3,4,5,6),
'2020-01-13' => array(1,2,3,4),
'2020-01-14' => array(1,2)
);
//批量设置活跃状态
foreach($data as $date=>$uids) {
$cacheKey = sprintf("stat_%s", $date);
foreach($uids as $uid) {
$redis->setBit($cacheKey, $uid, 1);
}
}

$redis->bitOp('AND', 'stat', 'stat_2020-01-10', 'stat_2020-01-11', 'stat_2020-01-12');

//总活跃用户:6
echo "总活跃用户:" . $redis->bitCount('stat') . PHP_EOL;

$redis->bitOp('AND', 'stat1', 'stat_2020-01-10', 'stat_2020-01-11', 'stat_2020-01-14') . PHP_EOL;

//总活跃用户:2
echo "总活跃用户:" . $redis->bitCount('stat1') . PHP_EOL;

$redis->bitOp('AND', 'stat2', 'stat_2020-01-10', 'stat_2020-01-11') . PHP_EOL;

//总活跃用户:8
echo "总活跃用户:" . $redis->bitCount('stat2') . PHP_EOL;

假设当前站点有 5000W 用户,那么一天的数据大约为 50000000/8/1024/1024=6MB

布隆过滤器

bitmap - Redis布隆过滤器 (应对缓存穿透问题)

举例:比如爬虫服务器在爬取电商网站的商品信息时,首先经过缓存,如果缓存查不到,再去数据库获取信息,因为爬虫的效率很高,且sku很有可能是不存在或者已下架的,就会造成缓存穿透,大量请求被发送到数据库,导致服务器受到影响。

此时,可以在缓存层之前,添加一个布隆过滤器,布隆过滤器看作是一个bitmap,sku作为offset值,如果商品真实存在,bit值设为1。首先将商品数据初始化,当有请求时,通过getbit判断sku是否有效。如果布隆过滤器认为商品不存在,就拒绝访问,这样就可以保护存储层。

Data structures are nothing different. They are like the bookshelves of your application where you can organize your data. Different data structures will give you different facility and benefits. To properly use the power and accessibility of the data structures you need to know the trade-offs of using one.

大意是不同的数据结构有不同的适用场景和优缺点,你需要仔细权衡自己的需求之后妥善适用它们,布隆过滤器就是践行这句话的代表。

参考资料

​https://zhuanlan.zhihu.com/p/136337229​

​https://www.imooc.com/article/298707​

​https://www.jianshu.com/p/4c8e119f35db​