Redis高級(jí)數(shù)據(jù)結(jié)構(gòu)實(shí)戰(zhàn)(一)BitMap用戶連續(xù)簽到

功能概述

  • 用戶連續(xù)登錄天數(shù)
  • 用戶累計(jì)登錄天數(shù)

1. 為什么選用 bitmap(位圖)

占用內(nèi)存更小,性能更高。這里偏實(shí)戰(zhàn),原理的東西就不細(xì)講了。

2. 實(shí)戰(zhàn)

2.1 基礎(chǔ)指令

記錄一個(gè)用戶某天登錄,只需要指令

redis:0> setbit key 6 1
"0"

bitmap 是一個(gè)bit數(shù)組,數(shù)據(jù)結(jié)構(gòu)大概是長(zhǎng)這樣子的:

key 0 0 0 0 0 0 1 0 0

數(shù)字6是這個(gè)數(shù)組的偏移量(index,下標(biāo)從0開(kāi)始),表示第7天簽到了

redis:0> getbit key 6
"1"

查看累計(jì)登錄天數(shù):

redis:0> bitcount key
"1"

因?yàn)?bitfield 指令無(wú)符號(hào)獲取的偏移量最大是63,所以一個(gè)key只存一個(gè)月份的數(shù)據(jù),這樣key的結(jié)構(gòu)可以是這樣:

user:sign:userId:date

bitfield 指令其實(shí)就是獲取這個(gè)key的數(shù)組下標(biāo)的一個(gè)list

bitfield user:sign:5:202105 get u14 0

u 表示無(wú)符號(hào) ,14 表示今天是14號(hào),0 表示索引,即從第一天開(kāi)始

2.2 偽代碼

里面每一步的注釋都寫(xiě)的非常明白,關(guān)鍵點(diǎn)在最后一個(gè)方法的移位操作

    // 簽到
    public void doSign(Integer userId, String dateStr) {
        // 獲取日期
        Date date = getDate(dateStr);
        // 獲取日期對(duì)應(yīng)的天數(shù),即多少號(hào)
        int offset = DateUtil.dayOfMonth(date) - 1;
        // 構(gòu)建 key user:sign:id:yyyyMM
        String signKey = buildKey(userId, date);
        // 查看是否簽到
        Boolean isSign = redisTemplate.opsForValue().getBit(signKey, offset);
        AssertUtil.isTrue(isSign, "當(dāng)前日期已簽到");
        // 簽到
        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"));
    }
    /**
     * 統(tǒng)計(jì)連續(xù)簽到的次數(shù)
     *
     * @param dinerId
     * @param date
     * @return
     */
    private int getContinuousSignCount(Integer dinerId, Date date) {
        // 當(dāng)前日期是幾號(hào)
        int dayOfMonth = DateUtil.dayOfMonth(date);
        // 構(gòu)建 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;
    }
    
        /**
     * 遞歸獲取連續(xù)簽到天數(shù)
     * @param dinerId
     * @param lastMonth
     * @return
     */
    private int getLastMonthSignCount(Integer dinerId, Date lastMonth) {

        // 獲取當(dāng)月最后一天
        Date lastDay = DateUtil.endOfMonth(lastMonth);
        int dayOfMonth = DateUtil.dayOfMonth(lastDay);
        // 構(gòu)建 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 表示無(wú)符號(hào) ,14 表示今天是14號(hào),0 表示索引,即從第一天開(kāi)始
        BitFieldSubCommands bitFieldSubCommands = BitFieldSubCommands
                .create()
                .get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth))
                .valueAt(0);

        List<Long> 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);

        // 移位操作:先右移再左移,結(jié)果未變則表示未簽到,結(jié)果變了則表示簽到了
        for (int i = dayOfMonth; i > 0; i--) { // i 表示位移的次數(shù)
            if (count >> 1 << 1 == count) {
                // 如果低位是0 且低位所在不是當(dāng)天,說(shuō)明連續(xù)簽到中斷
                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<Long>) con -> con.bitCount(key.getBytes()));
    }

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

2.3 移位

待續(xù)...

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容