什么是签到?
人员考勤是一种管理和监控员工出勤情况的方式,主要用于记录和跟踪员工的工作时间和出勤情况,而员工通过每日的签到就能够实现此功能。
为什么使用reids来实现签到?
为什么我在这里写的是使用Redis实现签到功能,而不是使用数据库完成签到功能呢?
我们都知道,无论使用什么进行存储,计算都是需要考虑占用内存以及性能。使用数据库进行存储我做了一个使用数据库实现签到功能的表,可看下图:
由图,我存储了一个用户一天的签到数据,然后看下图:
使用数据库存储一个用户一天的签到记录占用了0.02MB 那么 一千万个用户 一年就需要占用
0.02 * 10000000 * 365 = 73000000 (MB)
所以需要 73000000 / 1024 / 1024 = 69.618(TB)
所以大家认为使用数据库来实现签到功能划算嘛?
下面就让我们来欢迎今天的主角!!!Redis
重点
首先,我们要知道使用reids这个玩意,那我们具体通过采用什么来存储?没错 就是采用BitMap
它是一种位图,存储的是0和1 对于计算机而言,计算位运算是最快的。BitMap的最大位数可以容纳2^32(2的32次方)而我们每个月最大天数就31天 也就是31位(bit)。简而言之 约等于 4 字节(byte)。
使用redis的BitMap存储一个用户一个月的签到记录占用了4字节 所以在一千万个用户 一年的签到记录就 4 * 10000000 * 12 = 480000000(byte)
所以需要 480000000 / 1024 / 1024 = 457.763(MB)
使用redis的BitMap进行开发签到功能
1、封装实体
import lombok.Data;
import lombok.ToString;
@Data
@ToString
public class Sign {
private Long userId;
private String dateStr;
}
2、用户在点击签到按钮后 传用户id到后端接收 相关代码如下:
@RestController
@RequestMapping("/sign")
public class SignController {
@Autowired
private SignService service;
@PostMapping
public AjaxResult doSignIn(@RequestBody Sign sign){
return service.doSign(sign.getUserId(),sign.getDateStr());
}
}
3、获取当天的日期并进行签到 相关代码如下:
如在进行签到的时候,首先获取了当天日期 如2023-09-01
通过获取的日期知道了是1号,又因为redis的BitMap下标是从0开始,所以需要对 1 进行 “减1”
//获取当前日期
Date date = getDate(dateStr);
//根据当前日期获取是属于这个月的第N号日子 因为redis的BitMap下标是从0开始的
int day = DateUtil.dayOfMonth(date) - 1;
//构建redis对应的key值
String signKey = buildSignKey(userId,date);
//查看指定日期是否已经签到
boolean isSign = redisCache.isSign(signKey, day);
if (isSign){
return new AjaxResult(400,"当前已完成签到,无需再签");
}
//签到
redisCache.Sign(signKey,day);
/**
* 构建redis key值如 user:sign:userId:202309 例子:user:sign:1:202309
* @param userId
* @param date
* @return
*/
private String buildSignKey(Long userId, Date date) {
return String.format("user:sign:%d:%s",userId,DateUtil.format(date,"yyyyMM"));
}
通过redis命令 getbit user:sign:1:202309 0 的结果返回如果是1 则表示已经签到,若为0则表示未签到
/**
* 查看指定日期是否已签到 redis命令:getbit user:sign:1:202309 0
* @param signKey
* @param day
* @return
*/
public boolean isSign(String signKey,int day)
{
return redisTemplate.opsForValue().getBit(signKey,day);
}
在未签到的情况下则对使用命令 setbit user:sign:1:202309 0 1 将第0位改为1
/**
* 签到 setbit user:sign:1:202309 0 1
* @param signKey
* @param day
* @return
*/
public boolean Sign(String signKey,int day)
{
return redisTemplate.opsForValue().setBit(signKey,day,true);
}
使用BitMap统计本月总签到次数、连续签到次数
1、总签到次数
由于BitMap提供了bitCount的方法可以快速的统计总签到次数
bitCount user:sign:1:202309
而我们所需做的无非是传入 构建的redis的key值,然后使用key进行统计
2、连续签到次数
对于连续签到次数的基于今天之前的连续签到次数 如下(以今天28号为例子):
最后一个“1”则为28号签到,但连续签到天数是为4天。因为是以当天的上一天为基准计算的。若以今天为基准,则可能出现28号的今天.倘若查看时且时间还在28号的00:00:00到23:59:59范围内,当天还没签到,但可以签到。这样就会显示没有连续签到???这样是不合理的,所以是以当天的上一天为基准。
那如何判断某个位上是否为“1”或者“0?
位运算是计算机计算的最快的一种运算方式,当这样一组数据它先进行右移,则最低位消失,最高位补“0”.然后重新左移,最高位消失,最低位补“0”。这样就可以获得新的一组数据。 如下:
初始化数据:11001110
第一次右移:01100111
第一次左移:11001110
若新的一组数据与原先一致,则表示最低位为0.,当测过一位,便将数据右移,为下一次准备
当上一次结束后,则如下:
初始化数据:1100111
第一次右移:0110011
第一次左移:1100110
由此可知:当最低位为“1”时,经过这样操作后,是会与原先数据不一致。
具体相关代码如下:
private int getContinueSignCount(Long userId, Date today) {
//获取日期对应的天数 今天多少号
int day = DateUtil.dayOfMonth(today);
//构建reids key
String signKey = buildSignKey(userId,today);
BitFieldSubCommands bitFieldSubCommands = BitFieldSubCommands.create()
.get(BitFieldSubCommands.BitFieldType.unsigned(day)).valueAt(0);
List<Long> list = redisCache.getStatus(signKey, bitFieldSubCommands);
if (list == null || list.isEmpty()){
return 0;
}
int signCount = 0;
long l = list.get(0) == null ? 0 : list.get(0);
//位移计算 连续签到次数
for (int i = day; i > 0; i--) { // 表示位移操作的次数
//右移,在 左移 仍然与本身相等,则 最低位是0 所以是未签到
if (l >> 1 << 1 == l){
//当天 是一种特殊情况 可能是还没签到
if (i != day){
break;
}
}else{
//与本身不等,则最低为是1 表示签到
signCount++;
}
l >>= 1;
}
return signCount;
}
/**
* 为统计连续签到次数做数据准备
* @param signKey
* @return
*/
public List<Long> getStatus(String signKey, BitFieldSubCommands bitFieldSubCommands)
{
return redisTemplate.opsForValue().bitField(signKey,bitFieldSubCommands);
}