假设我们用一个数据库表来存储用户的签到信息
所以,我们采取01的方式存储用户的签到信息
这样一个用户一个月的签到情况就可以用一个最多31bit,3字节的的二进制串表示,高效还节省空间
而这种每一个bit对应当月中的每一天,形成的映射关系,用0、1标示业务状态,叫做位图(BitMap),所以redis中利用String类型数据结构实现BitMap,存储的最大上限是512M。也就是2^32bit
BitMap相关命令
假设我们在第1、2、3、4、7、8天分别签到会得到一个二进制的字符串 ,中间没有签到的天数默认为0
基于BItMap的签到功能
由于签到功能的实现只牵扯当前用户和一个二进制字符串,所以这个方式实现的是无参的,只需要获取当前登陆用户的id,并记录签到即可,另外由于BitMap是基于redis的String数据结构,所以被封装在opsforValue方法中
@Override
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 = USER_SIGN_KEY + userId + keySuffix;
// 4.获取今天是本月的第几天
int dayOfMonth = now.getDayOfMonth();
// 5.写入Redis SETBIT key offset 1,因为二进制是从0开始,而dayofMonth是从1开始计算,所以要dayOfMonth - 1,true表示1
stringRedisTemplate.opsForValue().setBit(key, dayOfMonth - 1, true);
return Result.ok();
}
用户点击签到过后,redis中会自动存储1,今天是14号
为什么会有16bit?
因为这是底层的扩容逻辑,不足1字节,自动用0补全1字节
签到统计
问题一:统一连续签到天数?
从最后一次签到开始向前统计,直至遇到第一次未签到位置,计算总签到次数
问题二:如果获得一个月的总签到天数?
BITFIELD key GET u[dayofmonth] 0 计算从0的角标(即一个月的第一天,一个二进制串的第0位)到dayofmonth(今天)的1的数量
问题三:如何从后向前遍历bit位?
与1做与运算(1和任何数做与运算都是这个数本身),就能得到最后一个bit位,随后让字符串右移,下一个bit位成为最后一个bit位
num在代码中的作用:是通过 Redis 的 BITFIELD
命令获取的无符号整数,通过 num & 1
操作检查最后一位bit位,判断当前处理的这一天是否签到。
在计算机中,所有数据(包括整数)在底层都是以二进制形式存储和运算的。因此,在代码中虽然看到的是十进制的 num
(例如 7
、3
、1
等),但 Java 的位运算(如 &
、>>>
)是直接操作其二进制位的,无需显式转换。
@Override
public Result signCount() {
// 1.获取当前登录用户
Long userId = UserHolder.getUser().getId();
// 2.获取日期
LocalDateTime now = LocalDateTime.now();
// 3.拼接key
String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
String key = USER_SIGN_KEY + userId + keySuffix;
// 4.获取今天是本月的第几天
int dayOfMonth = now.getDayOfMonth();
// 5.获取本月截止今天为止的所有的签到记录,返回的是一个十进制的数字
// BITFIELD sign:5:202203 GET u14 0,u14表示无符号的14bit位进行比较,因为今天是14号
List<Long> result = stringRedisTemplate.opsForValue().bitField(
key,
BitFieldSubCommands.create()
.get(BitFieldSubCommands.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) {
// 6.1.让这个数字与1做与运算,得到数字的最后一个bit位 // 判断这个bit位是否为0
if ((num & 1) == 0) {
// 如果为0,说明未签到,结束
break;
}else {
// 如果不为0,说明已签到,计数器+1
count++;
}
// 把数字右移一位,抛弃最后一个bit位,继续下一个bit位
//现右移一位,在赋值给num
num >>>= 1;
}
return Result.ok(count);
}