一,BitMap用法
1.1 引言
假如我们用一张表来存储用户签到信息,其结构应该如下
字段 | 说明 |
---|---|
id | 主键 |
user_id | 用户id |
year | 签到的年 |
month | 签到的月 |
date | 签到的日期 |
is_backup | 是否补签 |
假如有1千万用户,平均每人每年签到10次,则这张表一年数据量为1亿条,对于数据库来讲压力非常大是不可取的。
解决办法:
-
模拟签到卡,按月统计用户签到信息,签到记录为1未签到为0
把每一个bit位对应当月的每一天,形成了映射关系。用0/1标识业务状态,这种思路就叫位图(BitMap)
Redis中是利用string 类型数据结构实现BitMap,因此最大上限是512M,转换为bit是2^32个bit位
1.2 BitMap相关命令
BitMap的操作命令有:
-
SetBit
:向指定位置(offset)存入一个0或1- 语法:
SetBit key offset value
- 语法:
-
GetBit
:获取指定位置(offset)的bit值- 语法:
GetBit key offset
- 语法:
-
BitCount
:统计BitMap中值为1的bit位的数量- 语法:
BitCount key [start end]
- 语法:
-
BitField
:操作(查询,修改,自增)BitMap中bit数组中指定位置(offset)的值-
语法
bashBitField key [Get type offset] [Set type offset value] [IncrBy type offset increment] [OverFlow Wrap|Sat|Fail ] #参数说明 # key:要操作的 Redis 键。 # [Get type offset]:获取指定类型和偏移量的位字段值。 # [Set type offset value]:设置指定类型和偏移量的位字段值。 # [IncrBy type offset increment]:对指定类型和偏移量的位字段值进行递增操作。 # [Overflow Wrap|Sat|Fail]:指定溢出行为,适用于递增操作。 # ---------------------------------------- # 数据类型type # 分为i和u # i:有符号 # u:无符号 # i2就代表要查询2个bit位,从offset开始,且返回有符号的数字 # u2就代表要查询2个bit位,从offset开始,且返回没有符号的数字 #-------------------------------------------------- # offset:位字段的偏移量,以位为单位。 # value:要设置的值。 # increment:递增的值。 # Overflow:溢出行为,Wrap(环绕)、Sat(饱和)或 Fail(失败)。 # --------------------------------------------------- # 将 mykey 键中,从偏移量 0 开始的 8 位有符号整数设置为 127。 BITFIELD mykey SET i8 0 127 # 取 mykey 键中,从偏移量 0 开始的 8 位有符号整数的值。 BITFIELD mykey GET i8 0 # 对 mykey 键中,从偏移量 0 开始的 8 位有符号整数的值递增 1。 BITFIELD mykey INCRBY i8 0 1 # 对 mykey 键中,从偏移量 0 开始的 8 位无符号整数的值递增 255,溢出时采取饱和处理。 BITFIELD mykey INCRBY u8 0 255 OVERFLOW SAT
-
GET 操作返回请求的位字段值(十进制)。
-
SET 操作返回旧值。
-
INCRBY 操作返回递增后的新值。
-
-
-
BitField_ro
:获取BitMap中bit数组,以十进制形式返回,和BitField用法一样 -
BitOp
:将多个BitMap的结果做位运算(与,或,异或)-
语法
bashBitOp operation destKey key [key...] # operation:位运算操作类型。可以是以下之一: # AND:按位与操作 # OR:按位或操作 # XOR:按位异或操作 # NOT:按位非操作(仅适用于单个键) # destKey:存储操作结果的目标键。 # key:要进行运算的源键,可以指定一个或多个源键(在 AND、OR、XOR 操作中),或者仅一个源键(在 NOT 操作中)。 # ---------------------------------------------------------------------------- # 对 key1、key2 和 key3 进行按位与操作,将结果存储到 destKey 中。 BITOP AND destKey key1 key2 key3 # 对 key1 和 key2 进行按位或操作,将结果存储到 destKey 中。 BITOP OR destKey key1 key2 # 对 key1 和 key2 进行按位异或操作,将结果存储到 destKey 中。 BITOP XOR destKey key1 key2 # 对 key 进行按位非操作,将结果存储到 destKey 中。 BITOP NOT destKey key # BITOP 命令返回一个整数,表示目标键(destKey)的位图大小,以字节为单位。这个大小是所有参与运算的键中最大的位图大小。
-
-
BitPos
:查找bit数组中指定范围内第一个0或1出现的位置- 语法 :
BitPos key bit [Start] [End]
- 语法 :
二,签到功能

代码实现
java
public Result sign(){
//1.获取当前登录的用户
Long userId=UserHolder.getUser().getId();
//2.获取日期
LocalDateTime now=LocalDateTime.now();
//3.拼接key
String keySuffix=now.format(DateTimeFormatter.ofPattern(":yyyy/MM"));
String key="sign:"+userId+keySuffix;
//4.获取今天是本月的第几天
int dayOfMonth=now.getDayOfMonth();
//5.写入redis setBit key offset 1
stringRedisTemplate.opsForValue().setBit(key,dayOfMonth-1,true);
return Result.ok();
}
三,签到统计
连续签到天数:
-
从最后 一次签到开始向前 统计,直到遇到第一次未签到为止,计算总签到的次数,就是连续签到天数。
拿到本月到今天为止的所有签到数据:
BitField key get u[dayOfMonth] 0
如何从后向前遍历每个bit位
- 与1做And运算,就能得到最后一个bit位了
- 将数字向右移1位,下一位bit位就成了最后一位bit位了。
代码实现:
java
public Result signCount(){
//1.获取当前登录的用户
Long userId=UserHolder.getUser().getId();
//2.获取日期
LocalDateTime now=LocalDateTime.now();
//3.拼接key
String keySuffix=now.format(DateTimeFormatter.ofPattern(":yyyy/MM"));
String key="sign:"+userId+keySuffix;
//4.获取今天是本月的第几天
int dayOfMonth=now.getDayOfMonth();
//5.获取本月截至今天为止所有的签到记录,返回的是一个10进制的数字
//BitField key get u[dayOfMonth] 0
List<Long> result = stringRedisTemplate.opsForValue().bitField(
key,
BitFieldSubCommands.create()
.get(BitFieldType.unsigned(dayOfMonth))
.valueAt(0)
);
if(result==null || result.isEmpty()){
//没有签到结果
return Result.ok(0);
}
Long num=result.get(0);
if(num == null || num == 0){
//没有签到结果
return Result.ok(0);
}
//6.循环遍历
int count=0;
while(true){
//1.让这个数和1做And位运算,得到数字的最后一位bit位
if((num & 1) == 0){
//2.判断这个bit位是否位0
//如果为0 说明没有签到,结束
break;
}else{
//如果不为0,说明已签到,计数器+1
count++;
}
//把数字右移一位,抛弃最后一个bit位,继续下一个bit位
num>>>=1;
}
return Result.ok(count);
}