1.签到功能的实现思路 

        最近有研究到用户的签到功能,对功能进行设计的时候想到使用msyql存储用户的签到记录,将用户的每日签到记录存储到表中,然后又想到每次签到就往表里面存一条记录,用户量小还好,如果是一些互联网应用,用户量体量比较大的话,每天往表里面新增几万几十万的记录,随着数据的一直增加,就会带来很大的IO消耗,而且这些数据占据的存储空间也会越来越大,甚至会带来性能问题。

       最近在深入学习redis,发现它里面的bitmap数据结构就能很好的用户签到功能的实现,而且签到数据占用的内存也很小,查询统计的性能也不错,很好的解决了常规思路存在的问题。

            bitmap也叫位图,可以理解成二进制的bit数组,数组每位只有两种数字:0或1。使用Reids bitmap实现签到功能的思路就是将对应下标数字中的二进制数字置为1(即标记为签到).

比如: 0   0   0   1   1   0    0    这个这个二进制中第四天和第五天签到了,因为第四为和第五位的值是1.

      redis bitmap 常用命令:

#向指定位置(offset)存入0或1
SETBIT key offset value

#获取指定位置(offset)的bit值
GETBIT key offset

#统计bitmap中(从start到end,如果不写起始位置,就统计整个key)值为1的bit数量
BITCOUNT key [start end]

#操作(查询,修改,自增)Bitmap中指定的位置(offset)的值
BITFIELD key [GET type offset] [SET type offset value] [INCRBY type offset increment] [OVERFLOW WRAP|SAT|FAIL]

#获取Bitmap中的bit数组,并以十进制返回
BITFIELD_RO

  2.java代码实现签到功能

  以下使用RedisTemplate操作Redis

@Autowired
    public RedisTemplate redisTemplate;

  2.1  实现签到功能

 

public void sign() {
        // 组合成key= sign:userId:年月
        String key = buildSignKey(1234,  LocalDate.now());
        // 获取当前日期是当月的第多少天
        LocalDate now = LocalDate.now();
        int dayOfMonth = now.getDayOfMonth();
        // 存到redis中的bitmap中,由于bitmap从0开始,第多少天从1开始,dayOfMonth需要减1
        redisTemplate.opsForValue().setBit(key, dayOfMonth - 1, true);
        log.info("用户签到:" + key);

    }

2.2 查询当天是否有签到

/**
     * 查询当天是否有签到
     *
     * @param uid  用户id
     * @param localDate localDate 对象
     * @return
     */
    public boolean checkSign(int uid, LocalDate localDate) {
        return redisTemplate.opsForValue().getBit(buildSignKey(uid, localDate), localDate.getDayOfMonth() - 1);
    }

2.3 获取连续签到的次数

/**
     * 获取连续签到的次数
     * @return
     */
    @Override
    public int getSign() {
        //用户Uid
        String userId = "1234";
        //获取当前日期
        LocalDateTime now = LocalDateTime.now();
        String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
        // 组合成key= sign:userId:年月
        String key = buildSignKey(1234,  LocalDate.now());;
        // 获取当前日期是当月的第多少天
        int dayOfMonth = now.getDayOfMonth();
        // 获取本月截止今天为止的所有签到记录,返回的是一个十进制的数字
        List<Long> result = redisTemplate.opsForValue().bitField(key, BitFieldSubCommands.create()
                .get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth)).valueAt(0));
        log.info("查询数据:{}", result);
        if (result == null || result.isEmpty()) {
            return 0;
        }
        Long number = result.get(0);
        int count = 0;
        if (number == null || number == 0) {
            return 0;
        }
        while (true) {
            // 让这个数字与1做与运算,得到数字的最后一位bit,并判断这个bit是否为0
            if ((number & 1) == 0) {
                // 如果为0说明不是连续签到,直接终止
                break;
            } else {
                // 如果不为0,那就为1,说明有签到,继续下次循环,并且计数器加一
                count++;
            }
            // 把数字右移一位,抛弃最后一位的bit,继续下一个bit
            number = number >>> 1; // 也可以写成 number >>>= 1
        }
        return count;
    }

2.4 获取当月首次签到日期

/**
     * 获取当月首次签到日期
     *
     * @param uid  用户ID
     * @param date 日期
     * @return 首次签到日期
     */
    public LocalDate getFirstSignDate(int uid, LocalDate date) {
        long bitPosition = (Long) redisTemplate.execute((RedisCallback) cbk -> cbk.bitPos(buildSignKey(uid, date).getBytes(), true));
        return bitPosition < 0 ? null : date.withDayOfMonth((int) bitPosition + 1);
    }

2.5 获取本月签到信息

/**
     * 获取本月签到信息
     * @param uid
     * @param localDate
     * @return
     */
    public Map<String, String> getSignInfo(int uid, LocalDate localDate) {
        Map<String, String> signMap = new LinkedHashMap<>(localDate.getDayOfMonth());
        int lengthOfMonth = localDate.lengthOfMonth();
        //获取Bitmap中的bit数组,并以十进制返回
        List<Long> bitFieldList = (List<Long>) redisTemplate.execute((RedisCallback<List<Long>>) cbk
                -> cbk.bitField(buildSignKey(uid, localDate).getBytes(), BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(lengthOfMonth)).valueAt(0)));
        if (bitFieldList != null && bitFieldList.size() > 0) {
            long valueDec = bitFieldList.get(0) != null ? bitFieldList.get(0) : 0;
            //使用i--,从最低位开始处理
            for (int i = lengthOfMonth; i > 0; i--) {
                LocalDate tempDayOfMonth = LocalDate.now().withDayOfMonth(i);
                //valueDec先右移1位再左移1位得到一个新值,这个新值最低位的二进制值为0,再与valueDec做比较,如果相等valueDec的最低为是0,否则是1
                if (valueDec >> 1 << 1 != valueDec) {
                    signMap.put(tempDayOfMonth.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")), "1");
                } else {
                    signMap.put(tempDayOfMonth.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")), "0");
                }
                //每次处理完右移1位
                valueDec >>= 1;
            }
        }
        return signMap;
    }

2.6 工具方法

/**
     * 拼接rediskey
     * @param uid  用户id
     * @param date  日期对象
     * @return
     */
    private static String buildSignKey(int uid, LocalDate date) {
        return String.format("sign:%s:%s", uid, formatDate(date));
    }

    /**
     * 日期格式化
     * @param date
     * @return
     */
    private static String formatDate(LocalDate date) {
        return formatDate(date, "yyyyMM");
    }