#目前通行系统项目中有一个新需求【通过对通行记录数据定时分析,查询出长时间没 有刷卡/刷脸通行的学生】
#一看到通行签到相关,就想到了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,代表签到了。