#目前通行系统项目中有一个新需求【通过对通行记录数据定时分析,查询出长时间没 有刷卡/刷脸通行的学生】
#一看到通行签到相关,就想到了redis的位图,理由也有很多帖子说明了,最大优点占用空间小。
一.redis命令行
SETBIT:设置指定偏移量上的位值。语法:SETBIT <key> <offset> <value>
使用中key可以为 xxx前缀 + :+ 用户编号 + :+ yyyyMM 的格式,设置该人员在该月的签到情况。
当执行以下语句获得一个字符时
假如为2024年10月2号,则执行setbit sign:001:202410 1 1 。偏移量为 1 。
获取当前key的值为 @
使用 字符串二进制转换 得知,二进制为 0100000 ,八位数字,第二位为 1 ,代表2号签到过。
GETBIT:获取指定偏移量上的位值。语法:
GETBIT <key> <offset>
BITCOUNT:统计指定键的位中设置为1的位数。语法:
BITCOUNT <key>
BITPOS:找到第一个设置为1的位。语法:
BITPOS <key> <offset>
也可以使用get / set 直接对整个位图进行设置。
二.代码实现
1.设置指定key的位图
在项目中,使用中key可以为 xxx前缀 + :+ 用户编号 + :+ yyyyMM 的格式,设置该人员在该月的签到情况。
redisTemplate.opsForValue().setBit(key, offset, value);
参数说明:
key
:要操作的 Redis 键。offset
:要设置的位的偏移量(0-based index),即从零开始的位位置。value
:要设置的布尔值,true
表示设置为1
,false
表示设置为0
。
redisTemplate.opsForValue().setBit("sign:3123000901:202411" , 11 ,true);
2.获取查询时间段内的所有月份(后续用到)
/**
* 统计时间段内的所有月份
* @param startDate 开始时间,格式为yyyy-MM-dd
* @param endDate 结束时间,格式为yyyy-MM-dd
* @return 包含所有月份的列表,格式为yyyyMM
*/
public static List<String> getMonthsInRange(String startDate, String endDate) {
// 定义日期格式化器
DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
DateTimeFormatter monthFormatter = DateTimeFormatter.ofPattern("yyyyMM");
// 解析开始和结束日期
LocalDate start = LocalDate.parse(startDate, dateFormatter);
LocalDate end = LocalDate.parse(endDate, dateFormatter);
// 保存所有月份的列表
List<String> months = new ArrayList<>();
// 逐月增加,直到超过结束日期
LocalDate current = start.withDayOfMonth(1); // 从当月第一天开始
while (!current.isAfter(end)) {
months.add(current.format(monthFormatter));
current = current.plusMonths(1); // 增加一个月
}
return months;
}
3.已知获取位图时是byte[] ,将 byte[] 转换为二进制字符串(后续用到)
/**
* 将 byte[] 转换为二进制字符串
* @param bytes 字节数组
* @return 二进制字符串
*/
public static String byteArrayToBinaryString(byte[] bytes) {
StringBuilder binaryString = new StringBuilder();
for (byte b : bytes) {
// 将每个字节转换为二进制字符串,并补齐为8位
String binary = String.format("%8s", Integer.toBinaryString(b & 0xFF)).replace(' ', '0');
binaryString.append(binary);
}
return binaryString.toString();
}
4.将字符串按0补齐到指定长度,补齐到该月份的天数(后续用到)
/**
* 将字符串按0补齐到指定长度
* @param input 原始字符串
* @param length 目标长度
* @return 补齐后的字符串
*/
public static String padStringWithZeros(String input, int length) {
StringBuilder paddedString = new StringBuilder(input);
while (paddedString.length() < length) {
paddedString.append("0");
}
return paddedString.toString();
}
5.计算最长连续未通行天数,按1切割(后续用到)
/**
* 计算最长连续未通行天数
* @param bitmap 通行记录的位图值
* @return 最长连续的0序列长度
*/
public static int longestZeroSequence(String bitmap) {
// 分割字符串并找出最长的连续零序列
String[] zeroSequences = bitmap.split("1");
int longestZeroLength = 0;
for (String seq : zeroSequences) {
longestZeroLength = Math.max(longestZeroLength, seq.length());
}
return longestZeroLength;
}
6.具体实现代码
public List<DevPassRecord> passWarning(DevQueryParam param) {
//计算时间段
List<String> months = getMonthsInRange(param.getStartTime(), param.getEndTime());
//获取该辅导员下所有学生id 例 3123000067 sql语句查询
List<String> list = Arrays.asList("3123000067" , "3123000901");
Integer a = Integer.valueOf(param.getStartTime().substring(8, 10));
// 使用 DateTimeFormatter 解析 yyyy-MM-dd 格式
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
YearMonth yearMonth = YearMonth.parse(param.getEndTime(), formatter);
// 返回该月份的天数
int i1 = yearMonth.lengthOfMonth();
//获取查询时间段中开始时间的天数(后续用到)
Integer b = i1 - Integer.valueOf(param.getEndTime().substring(8, 10));
StringBuilder ids = new StringBuilder();
for (int i = 0; i < list.size(); i++){
//该用户时间段内,通行记录字符串
String dayStr = "";
for (String m : months) {
//查询redis中该key的位图
String key = "sign:" + list.get(i) + ":" + m;
byte[] bitmapBytes = redisTemplate.getConnectionFactory().getConnection().get(key.getBytes());
int daysInMonth = getDaysInMonth(m);
if(Objects.equals(bitmapBytes , null)) {
StringBuilder sb = new StringBuilder(daysInMonth);
for (int j = 0; j < daysInMonth; j++) {
sb.append('0');
}
dayStr = dayStr + sb;
}else {
String s = byteArrayToBinaryString(bitmapBytes);
String s1 = padStringWithZeros(s, daysInMonth);
dayStr = dayStr + s1;
}
}
//总长度 减去 开始月份未统计的天数 减去 结束时间未统计的天数 为最终统计情况。
dayStr = dayStr.substring(a-1, dayStr.length() - b);
System.out.println("总---" + dayStr);
int num = longestZeroSequence(dayStr);
System.out.println("最长天数---" + num);
if(num >= param.getDays()){
//大于参数天数的人员id,则视为长时间未通行,需要预警
ids.append(list.get(i)).append(",");
}
}
System.out.println("------>" + ids);
return null;
}
三.运行结果
例如分别设置偏离量为 3 ,11,12,18.分别对应代表4号,12号,13号,19号签到。
参数:查询1号到20号的签到情况,天数超过2天时预警。
结果:字符串总长度为20,分别在第4,12,13,19的位置为1,代表签到了。