位图

😀

位图

位图,即大量bit组成的一个数据结构(每个bit只能是0和1),主要适合在一些场景下,进行空间的节省。
它是一个字符串类型的数据结构,实际上是对字符串的每个比特位进行操作。
例如一些大量的bool类型的存取,一个用户365天的签到记录,签到了是1,没签到是0,如果用普通的key/value进行存储,当用户量很大的时候,需要的存储空间是很大的。
在 Redis 的位图中,每个位可以设置为 0 或 1,并且可以将多个位组合在一起形成一个按位存储的数据结构。位图的长度是动态可变的,最大支持 2^32 - 1 个比特位。
使用位图可以实现一些常见的应用场景,例如:

统计用户的访问次数或活跃度。
进行布隆过滤器(Bloom Filter)等高效的数据结构的实现。
压缩和存储稀疏的布尔数据。
实现位图索引,用于高效地进行集合操作,如交集、并集和差集。

BitMap用法

相关操作命令
SETBIT key offset value:将指定偏移量 offset 处的位设置为给定的值 value(0 或 1)。
GETBIT key offset:获取指定偏移量 offset 处的位的值。
BITCOUNT key [start end]:计算给定范围内(默认整个位图)的位为 1 的数量。
BITOP operation destkey key [key …]:对多个位图执行逻辑操作(AND、OR、NOT、XOR),并将结果存储到目标位图 destkey 中。

例如,修改某个bit位上的数据:

  • offset:要修改第几个bit位的数据
  • value:0或1
    如果要签到就可以利用上面的这个命令,例如这个月的第1、2、3、6、7、8几天签到了,就可以这样:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    # 第1天签到
    SETBIT bm 0 1
    # 第2天签到
    SETBIT bm 1 1
    # 第3天签到
    SETBIT bm 2 1
    # 第6天签到
    SETBIT bm 5 1
    # 第7天签到
    SETBIT bm 6 1
    # 第8天签到
    SETBIT bm 7 1
    最终Redis中保存的效果:

    那如果我们要查询签到记录怎么办?

那就是要读取BitMap中的数据,可以用这个命令,这个命令比较复杂,是一个组合命令,可以实现查询、修改等多种操作。不过我们只关心读取,所以只看第一种操作,GET即可:

1
BITFIELD key GET encoding offset
  • key:就是BitMap的key
  • GET:代表查询
  • encoding:返回结果的编码方式,BitMap中是二进制保存,而返回结果会转为10进制,但需要一个转换规则,也就是这里的编码方式
    • u:无符号整数,例如 u2,代表读2个bit位,转为无符号整数返回
    • i:有符号整数,例如 i2,代表读2个bit位,转为有符号整数返回
  • offset:从第几个bit位开始读取,例如0:代表从第一个bit位开始

例如,我想查询从第1天到第3天的签到记录,可以这样:

可以看到,返回的结果是7. 为什么是7呢?

签到记录是 11100111,从0开始,取3个bit位,刚好是111,转无符号整数,刚好是7

签到案例

如果我们签到的话,保存到数据库需要多条数据。数据量多了的话就比较占用内存,所以这里我们可以优化一下,签到为1,没签到为0,只需要用1或者0来记录即可,Redis中提供了用二进制来存储的办法,BitMap结构,大概意思就是你存的时候,我用二进制来从左往右存,取的时候转换为十进制的数

我们用java取的时候,如果要查看连续签到几天,比如11010111,后面三个1是连续签到的,我们需要先把十进制转换为二进制,然后转换为字符串,之后反转,遇到第一个0就结束,然后就能知道是连续签到几天了

计算连续签到天数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
 String key = 签到记录的key的前缀,比如sign:uid:110:202301
+ userId
+ now.format(DateUtils.SIGN_DATE_SUFFIX_FORMATTER);//当前日期格式化
// 计算offset(哪一天,获取当前时间角标,需要-1)
int offset = now.getDayOfMonth() - 1;
// 2.计算连续签到天数
int signDays = countSignDays(key, now.getDayOfMonth());
//方案一
// 1.获取本月从第一天开始,到今天为止的所有签到记录
List<Long> result = redisTemplate.opsForValue()
.bitField(key, BitFieldSubCommands.create().get(
BitFieldSubCommands.BitFieldType.unsigned(len)).valueAt(0));
if (CollUtils.isEmpty(result)) {
return 0;
}
//方案一
//获取到里面的结果,但是现在是十进制的
Long aLong = result.get(0);
//转成字符串
String string = Integer.toBinaryString(aLong.intValue());
//如果前面都是0,那么转成二进制的话,就都是1111了,前面0就没有了
//原来的值——补多长(总共多少位)——补什么
String s = StrUtil.padPre(string, len + 1, "0");
//反转
String reverse = StrUtil.reverse(s);
int days = reverse.indexOf("0");
if (days == -1) {
return len;
}
return days;

//方案二
// 1.获取本月从第一天开始,到今天为止的所有签到记录
int num = result.get(0).intValue();
// 2.定义一个计数器
int count = 0;
// 3.循环,与1做与运算,得到最后一个bit,判断是否为0,为0则终止,为1则继续
while ((num & 1) == 1) {//判断最低位是否为1
// 4.计数器+1
count++;
//在每次循环中,变量 num 会右移 1 位,也就是将其二进制表示向右移动一位,丢弃最低位,倒数第二位成为新的最低位。
// 5.把数字右移一位,最后一位被舍弃,倒数第二位成了最后一位
num >>>= 1;
}
return count;