Redis高级数据结构实战(一)BitMap用户连续签到

功能概述

  • 用户连续登录天数
  • 用户累计登录天数

1. 为什么选用 bitmap(位图)

占用内存更小,性能更高。这里偏实战,原理的东西就不细讲了。

2. 实战

2.1 基础指令

记录一个用户某天登录,只需要指令

redis:0> setbit key 6 1
"0"

bitmap 是一个bit数组,数据结构大概是长这样子的:

key 0 0 0 0 0 0 1 0 0

数字6是这个数组的偏移量(index,下标从0开始),表示第7天签到了

redis:0> getbit key 6
"1"

查看累计登录天数:

redis:0> bitcount key
"1"

因为 bitfield 指令无符号获取的偏移量最大是63,所以一个key只存一个月份的数据,这样key的结构可以是这样:

user:sign:userId:date

bitfield 指令其实就是获取这个key的数组下标的一个list

bitfield user:sign:5:202105 get u14 0

u 表示无符号 ,14 表示今天是14号,0 表示索引,即从第一天开始

2.2 伪代码

里面每一步的注释都写的非常明白,关键点在最后一个方法的移位操作

    // 签到
    public void doSign(Integer userId, String dateStr) {
        // 获取日期
        Date date = getDate(dateStr);
        // 获取日期对应的天数,即多少号
        int offset = DateUtil.dayOfMonth(date) - 1;
        // 构建 key user:sign:id:yyyyMM
        String signKey = buildKey(userId, date);
        // 查看是否签到
        Boolean isSign = redisTemplate.opsForValue().getBit(signKey, offset);
        AssertUtil.isTrue(isSign, "当前日期已签到");
        // 签到
        redisTemplate.opsForValue().setBit(signKey, offset, true);

    }
    
     private String buildKey(Integer dinerId, Date date) {
        return String.format("user:sign:%d:%s", dinerId,
                DateUtil.format(date, "yyyyMM"));
    }
    /**
     * 统计连续签到的次数
     *
     * @param dinerId
     * @param date
     * @return
     */
    private int getContinuousSignCount(Integer dinerId, Date date) {
        // 当前日期是几号
        int dayOfMonth = DateUtil.dayOfMonth(date);
        // 构建 key
        String key = buildKey(dinerId, date);

        int signCount = getSignCountFromRedis(key, dayOfMonth);
        if (dayOfMonth == signCount) {
            Date lastMonth = DateUtil.offsetMonth(date, -1);
            signCount += getLastMonthSignCount(dinerId, lastMonth);
        }

        return signCount;
    }
    
        /**
     * 递归获取连续签到天数
     * @param dinerId
     * @param lastMonth
     * @return
     */
    private int getLastMonthSignCount(Integer dinerId, Date lastMonth) {

        // 获取当月最后一天
        Date lastDay = DateUtil.endOfMonth(lastMonth);
        int dayOfMonth = DateUtil.dayOfMonth(lastDay);
        // 构建 key
        String key = buildKey(dinerId, lastMonth);

        int signCountFromRedis = getSignCountFromRedis(key, dayOfMonth);
        if (signCountFromRedis == dayOfMonth) {
            Date lastMonth1 = DateUtil.offsetMonth(lastMonth, -1);
            signCountFromRedis += getLastMonthSignCount(dinerId, lastMonth1);
        }


        return signCountFromRedis;
    }
    
        public int getSignCountFromRedis(String key, int dayOfMonth) {

        // bitfield user:sign:5:202105 u14 0 ,u 表示无符号 ,14 表示今天是14号,0 表示索引,即从第一天开始
        BitFieldSubCommands bitFieldSubCommands = BitFieldSubCommands
                .create()
                .get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth))
                .valueAt(0);

        List list = redisTemplate.opsForValue().bitField(key, bitFieldSubCommands);
        if (list == null || list.isEmpty()) {
            return 0;
        }
        int signCount = 0;
        long count = list.get(0) == null ? 0 : list.get(0);

        // 移位操作:先右移再左移,结果未变则表示未签到,结果变了则表示签到了
        for (int i = dayOfMonth; i > 0; i--) { // i 表示位移的次数
            if (count >> 1 << 1 == count) {
                // 如果低位是0 且低位所在不是当天,说明连续签到中断
                if (i != dayOfMonth) break;
            } else {
                signCount++;
            }
            // 把最后一位丢弃
            count >>= 1;
        }
        return signCount;
    }
    public void setBit(String key, Integer offset) {
        redisTemplate.opsForValue().setBit(key, offset, true);
    }

    public Boolean getBit(String key, Integer offset) {
        return redisTemplate.opsForValue().getBit(key, offset);
    }

    public Long bitCount(String key) {
        return (Long) redisTemplate.execute((RedisCallback) con -> con.bitCount(key.getBytes()));
    }

    public List bitField(String key, BitFieldSubCommands bitFieldSubCommands) {
        return redisTemplate.opsForValue().bitField(key, bitFieldSubCommands);
    }

2.3 移位

待续...

你可能感兴趣的:(Redis高级数据结构实战(一)BitMap用户连续签到)