前言
在很多社交类APP中,签到功能似乎成了标配,签到功能一方面可以促进APP中用户的活跃度,而且可以绑定一些促销活动刺激用户消费等关联功能
设计实现与分析
但从此功能的实现上来说,似乎并不是太难,我们完全可以通过创建一个如下简单的表实现
和上一篇一样的思路,设想你的APP的用户量是像QQ或微信那种量级的,每天签到的人数加起来该有多少?你的这张表够不够承载一年365天这么庞大用户体量的数据
就算可以借助mysql的索引功能加速查询,但是总有一天,该表所在的服务器会有撑不住的一天,而使用那些分库分表之类的方案来解决这个签到这样的非核心业务功能似乎又显得奇葩
有没有一种更好的方案来解决这个问题呢?答案是肯定的,就是使用redis提供的bitmaps的数据结构来处理
bitmaps简介
- Redis的Bitmaps这个“数据结构”可以实现对位的操作。Bitmaps本身不是一种数据结构,实际上就是字符串,但是它可以对字符串的位进行操作
- 可把Bitmaps想象成一个以位为单位数组,数组中的每个单元只能存0或者1,数组的下标在bitmaps中叫做偏移量
- 单个bitmaps的最大长度是512MB,即2^32个比特位
- bitmaps的最大优势是节省存储空间。比如在一个以自增id代表不同用户的系统中,我们只需要512MB空间就可以记录40亿用户的某个单一信息,相比mysql节省了大量的空间
- 有两种类型的位操作:一类是对特定bit位的操作,比如设置/获取某个特定比特位的值。另一类是批量bit位操作,例如在给定范围内统计为1的比特位个数
- Bitmap是一串连续的2进制数字(0或1),每一位所在的位置为偏移(offset),在bitmap上可执行AND,OR,XOR以及其它位操作
Bitmaps使用场景
1、存储与关系型数据关联的主键信息,适合大数据量的数据
2、一些热点数据的实时分析,统计计算等
比如像本篇开篇谈到的,统计用户的签到情况,连续签到情况,统计网站独立IP每日的访问数量,以及说由签到衍生的送积分,积分排行榜功能的实现都可以考虑使用Bitmaps
Bitmaps常用操作命令
- 设置值
setbit key offset value
- 获取值
getbit key offset
- 获取Bitmaps指定范围值为1的个数 (bitmaps中的保存的值为二进制的0,1,最大长度31位)
bitcount key [start] [end]
- Bitmaps间的运算
bitop:对两个不同字串进行位运算。可进行的运算有AND, OR, XOR以及NOT
- 计算Bitmaps中第一个值为targetBit的偏移量,比如可以查找1出现的第一个索引位置
bitpos key targetBit [start][end]
上面谈到bitmaps中的保存的值为二进制的0,1,最大长度31位,因此可以借助这个特点,来实现我们的签到功能,下面先通过命令行的操作来演示下
假如以当前2021年2月为例说明,key的格式为: user:sign:userId:202102
1~3天签到
4~6天跳过,未签到,未签到可以不用设置,bitmaps数据结构会自动为这几个位置补0,然后从第7到10天签到
设置成功后,来通过命令查看下存储进去的值
getbit user:sign:userId:202102 索引
这样就可以找到某一天当前这个用户是否进行了签到,1表示签到,0未签到
查找当前用户这个月签到过的次数,使用bitcount命令
bitcount user:sign:userId:202102
查找首次签到的那一天,和未签到的那一天
通过索引,我们可以定位到某个月中首次签到或者缺失的那一天
更多的命令,可以查阅相关权威资料来尝试和加深理解
有了上面对于bitmaps数据结构的了解,接下来通过代码来演示下如何实现签到的相关功能
功能实现
借助上一篇搭建好的演示项目,由于这里只需要操作redis,只需编写业务侧相关代码即可
- 需求1,统计某个月的签到情况,即签到的次数
提供签到接口,只有签到了,才能统计签到情况,签到在命令行中演示过,只需利用setbit命令即可实现,对应的key的格式为,user:sign:userId:yyyyMM
public Object doSign(int userId,String dateStr) {
//获取日期
Date date = getDate(dateStr);
//获取传入的日期对应的天数,即保存到redis的bitmaps的偏移量,索引位置
int offset = DateUtil.dayOfMonth(date) - 1;
String signKey = getSignKey(userId,date);
//检查当前这个时间是否签到
Boolean hasSign = redisTemplate.opsForValue().getBit(signKey, offset);
if(hasSign){
throw new RuntimeException("已经签到了,下次再来吧");
}
redisTemplate.opsForValue().setBit(signKey,offset,true);
return "sign success";
}
private String getSignKey(int userId, Date date) {
return "user:sign:".concat(String.valueOf(userId)).concat(":").concat(DateUtil.format(date,"yyyyMM"));
}
private Date getDate(String dateStr) {
if(StringUtils.isEmpty(dateStr)){
return new Date();
}
return DateUtil.parseDate(dateStr);
}
提供一个接口
@GetMapping("/doSign")
public Object sign(int userId,String dateStr){
return signService.doSign(userId,dateStr);
}
启动项目,反复调用下面接口,通过传入的日期,标识3月份不同日期的签到情况
localhost:8089/doSign?userId=1112&dateStr=2021-03-01
localhost:8089/doSign?userId=1112&dateStr=2021-03-02
localhost:8089/doSign?userId=1112&dateStr=2021-03-03
localhost:8089/doSign?userId=1112&dateStr=2021-03-06
localhost:8089/doSign?userId=1112&dateStr=2021-03-07
localhost:8089/doSign?userId=1112&dateStr=2021-03-08
localhost:8089/doSign?userId=1112&dateStr=2021-03-09
统计签到次数,
public Object doSign(int userId,String dateStr) {
//获取日期
Date date = getDate(dateStr);
//获取传入的日期对应的天数,即保存到redis的bitmaps的偏移量,索引位置
int offset = DateUtil.dayOfMonth(date) - 1;
String signKey = getSignKey(userId,date);
//检查当前这个时间是否签到
/*Boolean hasSign = redisTemplate.opsForValue().getBit(signKey, offset);
if(hasSign){
throw new RuntimeException("已经签到了,下次再来吧");
}
redisTemplate.opsForValue().setBit(signKey,offset,true);*/
Long labourDayCount = (Long) redisTemplate.execute((RedisCallback<Long>) con -> con.bitCount(signKey.getBytes()));
return labourDayCount;
}
继续调用接口,在上面的操作中,我们签到了7天
需求2,统计某个月连续签到的情况
public Object doSign(int userId,String dateStr) {
//获取日期
Date date = getDate(dateStr);
//获取传入的日期对应的天数,即保存到redis的bitmaps的偏移量,索引位置
int offset = DateUtil.dayOfMonth(date) - 1;
String signKey = getSignKey(userId,date);
//检查当前这个时间是否签到
Boolean hasSign = redisTemplate.opsForValue().getBit(signKey, offset);
if(hasSign){
throw new RuntimeException("已经签到了,下次再来吧");
}
redisTemplate.opsForValue().setBit(signKey,offset,true);
//连续签到的天数
int sequenceSignCount = getSignCount(userId,date);
return sequenceSignCount;
}
/**
* 获取连续签到的天数
* @param userId
* @param date
* @return
*/
private int getSignCount(int userId, Date date) {
int count = DateUtil.dayOfMonth(date);
String signKey = getSignKey(userId,date);
BitFieldSubCommands bitFieldSubCommands = BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(count)).valueAt(0);
List<Long> list = redisTemplate.opsForValue().bitField(signKey, bitFieldSubCommands);
int signCount = 0;
long v = list.get(0) == null? 0: list.get(0);
for(int i=count;i>0;i--){
if(v >> 1 <<1 == v){
if(i != count) break;
}else {
signCount++;
}
v >>= 1 ;
}
return signCount;
}
在上面调用签到接口时候,注意到已经连续签到了4天,这样修改之后,我们再次调同样的接口试试,这时,截止到3月10号,连续签到了5天
不妨再调用一次,这时发现截止到3月11号,连续签到了6天
其他的情况有兴趣的话可以继续测试
需求3,统计某个月每天的签到情况
简单分析一下,最终返回的数据格式大致为一个某个月从第一天到最后一一天的签到有序集合,可以考虑使用有序map,以时间为key,1(签到)或0为值即可满足需求
//localhost:8089/getSignInfoOfMonth?userId=1112&dateStr=2021-03-01
@GetMapping("/getSignInfoOfMonth")
public Object getSignInfoOfMonth(int userId,String dateStr){
return signService.getSignInfoOfMonth(userId,dateStr);
}
业务实现
public Object getSignInfoOfMonth(int userId,String dateStr){
//获取传入参数的日期
Date date = getDate(dateStr);
String signKey = getSignKey(userId,date);
Map<String,Object> infoMap = new TreeMap<>();
//获取传入时间对应的月份的总天数
int dayOfMonth = DateUtil.lengthOfMonth(DateUtil.month(date) +1,DateUtil.isLeapYear(DateUtil.year(date)));
BitFieldSubCommands bitFieldSubCommands = BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth)).valueAt(0);
List<Long> list = redisTemplate.opsForValue().bitField(signKey, bitFieldSubCommands);
if(list == null || list.isEmpty()){
return infoMap;
}
long v = list.get(0) == null? 0: list.get(0);
//通过不断的移位操作来判断某一天是否签到
for(int i=dayOfMonth;i>0;i--){
LocalDateTime localDateTime = LocalDateTimeUtil.of(date).withDayOfMonth(i);
boolean flag = v >> 1 <<1 !=v;
infoMap.put(DateUtil.format(localDateTime,"yyyy-MM-dd"),flag == true ? 1:0);
v >>=1;
}
return infoMap;
}
接口调用:http://localhost:8089/getSignInfoOfMonth?userId=1112&dateStr=2021-03-01
通过这种方式就可以将某个月每天的签到情况返回给前端进行数据展示
需要源码的同学可自行下载:https://download.csdn.net/download/zhangcongyi420/15499799
本篇通过案例演示了如何使用bitmap这种数据结构进行签到功能的实现,希望对看到的同学提供一个解决问题的思路,本篇到此结束,最后感谢观看!