Redis-BitMap实现签到功能
文章目录
为什么需要 bitmap
痛点:在 项目开发中经常会用到签到的功能,如果用户有量很少,签到信息直接存储在数据库中还是很合理的,但是随着用户量的增加,100 万用户,就算一个用户一年签到 20 次就已经 2000 万数据了,增长的非常快,并且查询的时候效率也低。
为了解决这个问题可以用 Bitmap 的数据结构。
用 1 来表示签到,用 0 来表示没有签到。
对于一个月来说,从第一个开始签到。一个月最多 31 天,用 31bit 来表示用户签到。一个月只需要两个字节。
一个用户一个月签到的信息也就只有一条,这样大大减少了数据库的压力。
布隆过滤器底层也是 bitmap。 在 redis 中使用 string 来实现 bitmap。最大存储上线 512,最大时 2 的 32 次方比特位。
BitMap 的操作
功能分析:
对于用户签到数据,如果直接采用数据库存储,当出现高并发访问时,对数据库压力会很大,例如双十一签到活动。这时候应该采用缓存,以减轻数据库的压力,Redis是高性能的内存数据库,适用于这样的场景。
另外如果系统的用户量很多,每次签到都插入一条记录,那么数据库表增长就很快。如果系统两百万用户,一个月平均签到十次,那么就是两千万数据量,mysql 一张表大概也就是两千万。
签到出来的数据
setbit bm1 0 1 在第0个位置赋值为1
这个图中可以看出来是第 1、2、7 天签到的。
判断第二天是否签到,getbit 如果等于 1 说明用户在这一天完成了签到功能;
get bm1 1 //判断是否等于1
bitpos 判断开始签到的位置
实现签到的代码 存储在 redis 中的格式
sign:userID:202401,这样每一个用户都是一个 key.
@Override
public Result sign() {
// 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.写入Redis SETBIT key offset 1
stringRedisTemplate.opsForValue().setBit(key, dayOfMonth - 1, true);
return Result.ok();
}
连续签到次数
* 1. 用户签到 * 2. 检查用户是否签到 * 3. 获取当月签到次数 * 4. 获取当月连续签到次数
创建用户的签到数据库表:
功能实现
1. 用户签到实现
@Override
public void signIn() {
// 获取当前登录的用户Id
Long userId = SecurityUtils.getUserId();
//获取日期
LocalDateTime now = LocalDateTime.now();
// 获取当前的月份
String keySuffix=now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
//拼接
String key= SignRedisConstant.SIGN_KEY + userId+keySuffix;
int dayOfMonth = now.getDayOfMonth();
redisTemplate.opsForValue().setBit(key,dayOfMonth-1,true);
}
还需要做异步任务,将数据同步到数据库中去。
待做:用定时任务或者异步操作更新数据库中的签到信息。
2. 检查用户是否签到
将获取 key 的方法封装起来
public class RedisUtil {
public static String getSign() {
Long userId = SecurityUtils.getUserId();
LocalDateTime now = LocalDateTime.now();
String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
return SignRedisConstant.SIGN_KEY + userId + keySuffix;
}
public static LocalDateTime getNow() {
return LocalDateTime.now();
}
}
@Override
public boolean isSignIn() {
String keySuffix = RedisUtil.getSign();
int dayOfMonth = RedisUtil.getNow().getDayOfMonth();
return redisTemplate.opsForValue().getBit(keySuffix, dayOfMonth - 1);
}
3. 获取当月签到次数
@Override
public int getCurrentMonth() {
String keySuffix = RedisUtil.getSign();
String str = (String) redisTemplate.opsForValue().get(keySuffix);
int count = 0;
for (int i = 0; i < str.length(); i++) {
if (str.charAt(i) == '1') {
count++;
}
}
return count;
}
4. 获取当月连续签到次数
从最后一个开始一直找到第一个为 0 的地方,和 1 进行与操作就是拿到最后一个数字的。
@Override
public int getContinuousSignInCount() {
String keySuffix = RedisUtil.getSign();
int dayOfMonth = RedisUtil.getNow().getDayOfMonth();
List<Long> result = redisTemplate.opsForValue().bitField(keySuffix, BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth)).valueAt(0));
if (result == null || result.size() == 0) {
return 0;
}
Long num = result.get(0);
if (num == null || num == 0) {
return 0;
}
int count = 0;
//主要是这一段的逻辑
while (true) {
if ((num & 1) == 0) {
break;
} else {
count++;
}
num = num >> 1;
}
return count;
}