Redis提供了Bitmaps这个“数据类型”可以实现对位的操作:
(1) Bitmaps本身不是一种数据类型, 实际上它就是字符串(key-value) , 但是它可以对字符串的位进行操作。
(2) Bitmaps单独提供了一套命令, 所以在Redis中使用Bitmaps和使用字符串的方法不太相同。 可以把Bitmaps想象成一个以位为单位的数组, 数组的每个单元只能存储0和1, 数组的下标在Bitmaps中叫做偏移量。
首先我们要理解几个命令,特别是最后一个,这些命令可以参考Redis文档。
SETBIT key offset value
设置或者清空key的value(字符串)在offset处的bit值。
SETBIT bit:sign 2 1
GETBIT key offset
返回key对应的string在offset处的bit值,当offset超出了字符串长度的时候,这个字符串就被假定为由0比特填充的连续空间。
GETBIT bit:sign 2
BITCOUNT key [start end]
统计字符串被设置为1的bit数。对一个不存在的 key 进行 BITCOUNT 操作,结果为 0 。
SETBIT bit:sign 2 1 SETBIT bit:sign 5 1 BITCOUNT bit:sign
BITPOS key bit [start] [end]
返回字符串里面第一个被设置为1或者0的bit位。
BITPOS bit:sign 1
BITFIELD key [GET type offset] [SET type offset value] [INCRBY type offset increment] [OVERFLOW WRAP|SAT|FAIL]
BITFIELD命令能操作多字节位域,它会执行一系列操作,并返回一个响应数组,在参数列表中每个响应数组匹配相应的操作。
BITFIELD bit:sign get u1 2
从offset-2开始,取一位,结果为无符号数(u),也可以进行多个操作
BITFIELD bit:sign get u1 2 get u1 5
其次是如何在我们的代码中使用,由于目前代码都集成Spring使用RedisTemplate来操作,这里使用RedisTemplate来演示。
代码中的位运算操作,可以百度“Java语言位运算符详解”参考别人文章理解。
/** * 用户签到功能 */ private static final String USER_SIGN = "USER_SIGN:%d:%s"; private static String buildSignKey(Long uid, LocalDate date) { return String.format(USER_SIGN, uid, formatDate(date)); } @Test public void testBit() { String bitKey = buildSignKey(100000L, LocalDate.now()); // 当前签到情况 LocalDate date = LocalDate.now(); redisTemplate.delete(bitKey); // offset是从0开始的,因此2号签到,offset要标记为1 int todayOffset = date.getDayOfMonth() - 1; redisTemplate.opsForValue().setBit(bitKey, 3, true); // 4号签到 redisTemplate.opsForValue().setBit(bitKey, 4, true); // 5号签到 redisTemplate.opsForValue().setBit(bitKey, 5, true); // 6号签到 redisTemplate.opsForValue().setBit(bitKey, 6, true); // 7号签到 redisTemplate.opsForValue().setBit(bitKey, todayOffset, true); // 当天签到 // 当月-累计签到 Long count = (Long) redisTemplate.execute((RedisCallback<Long>) con -> con.bitCount(bitKey.getBytes())); System.out.println("当月-累计签到:" + count); // 当月-首次签到日期 long bitPosition = (Long) redisTemplate.execute((RedisCallback) cbk -> cbk.bitPos(bitKey.getBytes(), true)) + 1; System.out.println("当月-首签日期:" + bitPosition); // LinkedHashMap 有序Map Map<String, Boolean> signMap = new LinkedHashMap<>(date.getDayOfMonth()); // Long 64位 List<Long> list = redisTemplate.opsForValue().bitField(bitKey, BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType .unsigned(date.lengthOfMonth())).valueAt(0)); if (null != list && list.size() > 0) { // 由低位到高位,为0表示未签,为1表示已签 long v = list.get(0) == null ? 0 : list.get(0); // 例如这个月一共31天,那么取到的二进制就是31个的0或1的组合 System.out.println("签到二进制位输出:" + Long.toBinaryString(v)); for (int i = date.lengthOfMonth(); i > 0; i--) { LocalDate d = date.withDayOfMonth(i); // 按位操作向右位移1,再移动回来,再移回来时是用0补位,如果补位后相等说明这个位本来就是0,所以这里判断是不相等,不相等即为1,说明签到了 signMap.put(formatDate(d, "yyyy-MM-dd"), v >> 1 << 1 != v); // 是右移后赋值,意思是向右移动一位出去,此时变量v的值将发生变化 v >>= 1; } } System.out.println("当月签到情况:" + JSON.toJSONString(signMap, true)); int signCount = 0; boolean isBreak = false; //定义一个死循环,有断签就结束循环 while (true) { if (isBreak) break; //判断是否可以跳出循环 list = redisTemplate.opsForValue().bitField(bitKey, BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType .unsigned(date.getDayOfMonth())).valueAt(0)); if (list != null && list.size() > 0) { // 取低位连续不为0的个数即为连续签到次数,需考虑当天尚未签到的情况 long v = list.get(0) == null ? 0 : list.get(0); for (int i = 0; i < date.getDayOfMonth(); i++) { if (v >> 1 << 1 == v) { if (i > 0) { // 低位为0且非当天说明连续签到中断了 isBreak = true; //断签,准备跳出循环 break; } } else { signCount += 1; } v >>= 1; } //当月签到情况获取完,日期调整为上一个月继续获取 date = date.minusMonths(1).with(TemporalAdjusters.lastDayOfMonth()); } } System.out.println("累计签到:" + signCount); // 这个累计就是从当天开始往前推 // 不同用法,下面更直观。获取 1bit 下标手动指定 BitFieldSubCommands command = BitFieldSubCommands.create() .get(BitFieldSubCommands.BitFieldType.unsigned(1)).valueAt(3) .get(BitFieldSubCommands.BitFieldType.unsigned(1)).valueAt(4) .get(BitFieldSubCommands.BitFieldType.unsigned(1)).valueAt(5) .get(BitFieldSubCommands.BitFieldType.unsigned(1)).valueAt(6) .get(BitFieldSubCommands.BitFieldType.unsigned(1)).valueAt(todayOffset); list = redisTemplate.opsForValue().bitField(bitKey, command); System.out.println("固定区间取值:" + JSON.toJSONString(list, true)); System.out.println("今天是否签到:" + redisTemplate.opsForValue().getBit(bitKey, todayOffset)); } private static String formatDate(LocalDate date, String pattern) { return date.format(DateTimeFormatter.ofPattern(pattern)); } private static String formatDate(LocalDate date) { return formatDate(date, "yyyyMM"); }
打印输出内容参考
当月-累计签到:5 当月-首签日期:4 签到二进制位输出:1111000000000000001000000 当月签到情况:{ "2023-02-28":false, "2023-02-27":false, "2023-02-26":false, "2023-02-25":false, "2023-02-24":false, "2023-02-23":false, "2023-02-22":true, "2023-02-21":false, "2023-02-20":false, "2023-02-19":false, "2023-02-18":false, "2023-02-17":false, "2023-02-16":false, "2023-02-15":false, "2023-02-14":false, "2023-02-13":false, "2023-02-12":false, "2023-02-11":false, "2023-02-10":false, "2023-02-09":false, "2023-02-08":false, "2023-02-07":true, "2023-02-06":true, "2023-02-05":true, "2023-02-04":true, "2023-02-03":false, "2023-02-02":false, "2023-02-01":false } 累计签到:1 固定区间取值:[ 1, 1, 1, 1, 1 ] 今天是否签到:true
END
Java小强
未曾清贫难成人,不经打击老天真。
自古英雄出炼狱,从来富贵入凡尘。
发表评论: