Java小强个人技术博客站点    手机版
当前位置: 首页 >> DB >> 使用Bitmaps位图实现Redis签到

使用Bitmaps位图实现Redis签到

10750 DB | 2023-2-22

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

推荐您阅读更多有关于“ redis Bitmaps 位图 签到 BITFIELD ”的文章

上一篇:Sentinel入门流控编码方式 下一篇:YYYY与yyyy的区别

猜你喜欢

发表评论: