SpringBoot2.x中使用Redis的bitmap結(jié)構(gòu)(工具類)

  1. 一億個用戶遍膜,有的用戶頻繁登錄爵赵,也有不經(jīng)常登錄的等恐。
  2. 如何記錄用戶的登錄信息洲劣?
  3. 如何查詢活躍用戶?[如一周內(nèi) 登錄三次的]

Redis中文教程
Redis語法大全

我們可以使用Redis的bitmap(位圖)來存儲數(shù)據(jù)课蔬。

1. 什么叫做Redis的bitmap

即:操作String數(shù)據(jù)結(jié)構(gòu)的key所存儲的字符串指定偏移量上的囱稽,返回原位置的值

1.1 優(yōu)點(diǎn):

節(jié)省空間:通過一個bit位來表示某個元素對應(yīng)的值或者狀態(tài),其中key就是對應(yīng)元素的值二跋。實(shí)際上8個bit可以組成一個Byte粗悯,所以是及其節(jié)省空間的。

效率高:setbitgetbit的時間復(fù)雜度都是O(1)同欠,其他位運(yùn)算效率也高样傍。

1.2 缺點(diǎn):

本質(zhì)上只有01的區(qū)別,所以用做業(yè)務(wù)數(shù)據(jù)記錄铺遂,就不需要在意value的值衫哥。

1.3 使用場景

  1. 可作為簡單的布爾過濾器來判斷用戶是否執(zhí)行過某些操作;
  2. 可以計(jì)算用戶日活襟锐、月活撤逢、留存率的統(tǒng)計(jì);
  3. 可以統(tǒng)計(jì)用戶在線狀態(tài)和人數(shù);

2. Redis的bitmap命令

2.1 setbit命令

設(shè)置或修改key上的偏移量(offset)的位(value)的值蚊荣。

  • 語法:setbit key offset value
  • 返回值:指定偏移量(offset)原來存儲的值初狰。
    bitmap的setkey指令

2.2 getbit命令

查詢key所存儲的字符串值,獲取偏移量上的互例。

  • 語法:getbit key offset
  • 返回值:返回指定key上的偏移量奢入,若key不存在,那么返回0媳叨。
bitmap的getbit指令

2.3 bitcount命令

計(jì)算給定key的字符串值中腥光,被設(shè)置為1的位bit的數(shù)量

  • 語法:bitcount key [start] [end]
  • 返回值:1比特位的數(shù)量

注意:setbit是設(shè)置或者清除bit位置。這個是統(tǒng)計(jì)key出現(xiàn)1的次數(shù)糊秆。
(小胖友情提示:)需要注意的是:[start][end](單位)實(shí)際是byte武福,這是什么意思呢?進(jìn)入redis實(shí)際上是乘以8痘番。

bitcount指令的使用

2.4 bitop命令

對一個或多個保存二進(jìn)制的字符串key進(jìn)行元操作捉片,并將結(jié)果保存到destkey上。

  • 語法:operation可以是and汞舱、or伍纫、notxor的一種兵拢。
  • bitop and destkey key [key...],對一個或多個key邏輯并逾礁,結(jié)果保存到destkey说铃。
  • bitop or destkey key [key...],對一個或多個key邏輯或嘹履,結(jié)果保存到destkey腻扇。
  • bitop xor destkey key [key...],對一個或多個key邏輯異或砾嫉,結(jié)果保存到destkey幼苛。
  • bitop xor destkey key,對一個或多個key邏輯非焕刮,結(jié)果保存到destkey舶沿。

除了NOT之外,其他操作多可以接受一個或多個key作為輸入配并。

BITOP的時間復(fù)雜度是O(N)括荡,當(dāng)處理大型矩陣或者大量數(shù)據(jù)統(tǒng)計(jì)時,最好將任務(wù)指派到附屬節(jié)點(diǎn)(slave)進(jìn)行溉旋,避免阻塞主節(jié)點(diǎn)畸冲。

3. SpringBoot中使用

@Component
public class SpringUtils implements ApplicationContextAware {
    private static ApplicationContext applicationContext;

    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        if (SpringUtils.applicationContext == null) {
            SpringUtils.applicationContext = applicationContext;
        }
    }

    public static Object getBean(String name) {
        return getApplicationContext().getBean(name);
    }

    public static <T> T getBean(Class<T> c) {
        return getApplicationContext().getBean(c);
    }

    public static <T> T getBean(String name, Class<T> c) {
        return getApplicationContext().getBean(name, c);
    }


}

工具類:

mport com.google.common.hash.Funnels;
import com.google.common.hash.Hashing;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;

import java.nio.charset.Charset;

/**
 * 工具類-提供靜態(tài)方法
 */
public  class RedisTemplateUtil {

    private static  StringRedisTemplate stringRedisTemplate = SpringUtils.getBean(StringRedisTemplate.class);


    /*********************************************************************************
     *
     * 對bitmap的操作
     *
     ********************************************************************************/

    /**
     * 將指定param的值設(shè)置為1,{@param param}會經(jīng)過hash計(jì)算進(jìn)行存儲。
     *
     * @param key   bitmap結(jié)構(gòu)的key
     * @param param 要設(shè)置偏移的key邑闲,該key會經(jīng)過hash運(yùn)算算行。
     * @param value true:即該位設(shè)置為1,否則設(shè)置為0
     * @return 返回設(shè)置該value之前的值苫耸。
     */
    public static Boolean setBit(String key, String param, boolean value) {
        return stringRedisTemplate.opsForValue().setBit(key, hash(param), value);
    }

    /**
     * 將指定param的值設(shè)置為0州邢,{@param param}會經(jīng)過hash計(jì)算進(jìn)行存儲。
     *
     * @param key   bitmap結(jié)構(gòu)的key
     * @param param 要移除偏移的key鲸阔,該key會經(jīng)過hash運(yùn)算偷霉。
     * @return 若偏移位上的值為1咏雌,那么返回true捕透。
     */
    public static Boolean getBit(String key, String param) {
        return stringRedisTemplate.opsForValue().getBit(key, hash(param));
    }


    /**
     * 將指定offset偏移量的值設(shè)置為1鬼佣;
     *
     * @param key    bitmap結(jié)構(gòu)的key
     * @param offset 指定的偏移量枣接。
     * @param value  true:即該位設(shè)置為1纳决,否則設(shè)置為0
     * @return 返回設(shè)置該value之前的值盹舞。
     */
    public static Boolean setBit(String key, Long offset, boolean value) {
        return stringRedisTemplate.opsForValue().setBit(key, offset, value);
    }

    /**
     * 將指定offset偏移量的值設(shè)置為0娄昆;
     *
     * @param key    bitmap結(jié)構(gòu)的key
     * @param offset 指定的偏移量泽西。
     * @return 若偏移位上的值為1晃痴,那么返回true残吩。
     */
    public static Boolean getBit(String key, long offset) {
        return stringRedisTemplate.opsForValue().getBit(key, offset);
    }

    /**
     * 統(tǒng)計(jì)對應(yīng)的bitmap上value為1的數(shù)量
     *
     * @param key bitmap的key
     * @return value等于1的數(shù)量
     */
    public static Long bitCount(String key) {
        return stringRedisTemplate.execute((RedisCallback<Long>) con -> con.bitCount(key.getBytes()));
    }

    /**
     * 統(tǒng)計(jì)指定范圍中value為1的數(shù)量
     *
     * @param key   bitMap中的key
     * @param start 該參數(shù)的單位是byte(1byte=8bit),{@code setBit(key,7,true);}進(jìn)行存儲時倘核,單位是bit泣侮。那么只需要統(tǒng)計(jì)[0,1]便可以統(tǒng)計(jì)到上述set的值。
     * @param end   該參數(shù)的單位是byte紧唱。
     * @return 在指定范圍[start*8,end*8]內(nèi)所有value=1的數(shù)量
     */
    public static Long bitCount(String key, int start, int end) {
        return stringRedisTemplate.execute((RedisCallback<Long>) con -> con.bitCount(key.getBytes(), start, end));
    }


    /**
     * 對一個或多個保存二進(jìn)制的字符串key進(jìn)行元操作活尊,并將結(jié)果保存到saveKey上。
     * <p>
     * bitop and saveKey key [key...]漏益,對一個或多個key邏輯并蛹锰,結(jié)果保存到saveKey。
     * bitop or saveKey key [key...]绰疤,對一個或多個key邏輯或铜犬,結(jié)果保存到saveKey。
     * bitop xor saveKey key [key...]轻庆,對一個或多個key邏輯異或癣猾,結(jié)果保存到saveKey。
     * bitop xor saveKey key余爆,對一個或多個key邏輯非煎谍,結(jié)果保存到saveKey。
     * <p>
     *
     * @param op      元操作類型龙屉;
     * @param saveKey 元操作后將結(jié)果保存到saveKey所在的結(jié)構(gòu)中呐粘。
     * @param desKey  需要進(jìn)行元操作的類型满俗。
     * @return 1:返回元操作值。
     */
    public static Long bitOp(RedisStringCommands.BitOperation op, String saveKey, String... desKey) {
        byte[][] bytes = new byte[desKey.length][];
        for (int i = 0; i < desKey.length; i++) {
            bytes[i] = desKey[i].getBytes();
        }
        return stringRedisTemplate.execute((RedisCallback<Long>) con -> con.bitOp(op, saveKey.getBytes(), bytes));
    }

    /**
     * 對一個或多個保存二進(jìn)制的字符串key進(jìn)行元操作作岖,并將結(jié)果保存到saveKey上唆垃,并返回統(tǒng)計(jì)之后的結(jié)果。
     *
     * @param op      元操作類型痘儡;
     * @param saveKey 元操作后將結(jié)果保存到saveKey所在的結(jié)構(gòu)中辕万。
     * @param desKey  需要進(jìn)行元操作的類型。
     * @return 返回saveKey結(jié)構(gòu)上value=1的所有數(shù)量值沉删。
     */
    public static Long bitOpResult(RedisStringCommands.BitOperation op, String saveKey, String... desKey) {
        bitOp(op, saveKey, desKey);
        return bitCount(saveKey);
    }


    /**
     * guava依賴獲取hash值渐尿。
     */
    private static long hash(String key) {
        Charset charset = Charset.forName("UTF-8");
        return Math.abs(Hashing.murmur3_128().hashObject(key, Funnels.stringFunnel(charset)).asInt());
    }

}

4. bitmap導(dǎo)致大key

4.1 原因

網(wǎng)上的文章,均是簡單的介紹bitmap的用法矾瑰,但是都存在一個很大的風(fēng)險的砖茸,導(dǎo)致bitmap占用大內(nèi)存。

例如判斷活躍用戶量殴穴,使用bitmap實(shí)現(xiàn):網(wǎng)上介紹很簡單凉夯,bitset 將偏移量設(shè)置為1,但是id如何轉(zhuǎn)換為偏移量并沒有一篇文章進(jìn)行介紹采幌。

bitmap推薦的偏移量是從1一直累加的劲够,但是計(jì)算出的hash值為10位(10億級別),那么占用的內(nèi)存大小為239MB休傍,若是計(jì)算出的hash值為7位(百萬級別)占用的內(nèi)存大小為124KB征绎。

所以計(jì)算偏移量的時候不能無腦的進(jìn)行hash得到,而是要根據(jù)系統(tǒng)情況(百萬級別的日活磨取、十萬級別日活)人柿,進(jìn)行取余計(jì)算,得到合適的偏移量寝衫。

4.2 解決方案

對數(shù)據(jù)進(jìn)行分組在分片:

  1. 不僅可以避免bitmap導(dǎo)致大key顷扩。
  2. 避免出現(xiàn)范圍用戶太多導(dǎo)致查詢時出現(xiàn)熱key拐邪。
public class TestBitMap {


    public static void main(String[] args) {

        System.out.println(ONE_BITMAP_SIZE);
        System.out.println(1024*1024);
        long r1=223456679;
        BitMapKey u1 = computeUserGroup(r1, ONE_BITMAP_SIZE, SHARD_COUNT);
        System.out.println(u1);

        long r2=1234566777;
        BitMapKey u2 = computeUserGroup(r2, ONE_BITMAP_SIZE, SHARD_COUNT);
        System.out.println(u2);


        String redisKey = u2.generateKeyWithPrefix("ttt");
        System.out.println(redisKey);
    }

    // 單個bitmap占用1M內(nèi)存
    // 如果useId < 100億慰毅, 則會分到7000個分組里
    private static final int ONE_BITMAP_SIZE = 1 << 20;
    // 同一個分組里的的useId劃分到20個bitmap里
    // 避免出現(xiàn)范圍用戶太多導(dǎo)致查詢時出現(xiàn)熱key
    private static final int SHARD_COUNT = 20;

    // 計(jì)算用戶的 raw, shard扎阶, 和對應(yīng)的offset
    public static BitMapKey computeUserGroup(long userId, int oneBitMapSize, int shardCount) {
        //獲取組
        long groupIndex = userId / oneBitMapSize;
        //獲取分片位置
        int shardIndex = Math.abs((int) (hash(userId+"") % shardCount));
        //獲刃谖浮(組-分片)下的offset位置
        int bitIndex = (int) (userId - groupIndex * oneBitMapSize);
        //獲取到對象
        return new BitMapKey((int) groupIndex, shardIndex, bitIndex);
    }

    @Data
    public static class BitMapKey {
        /**
         * 組
         */
        private final int groupIndex;
        /**
         * 組中分片
         */
        private final int shardIndex;
        /**
         *
         */
        private final int bitIndex;

        public BitMapKey(int groupIndex, int shardIndex, int bitIndex) {
            this.groupIndex = groupIndex;
            this.shardIndex = shardIndex;
            this.bitIndex = bitIndex;
        }

        public int getBitIndex() {
            return bitIndex;
        }

        public String generateKeyWithPrefix(String prefix) {
            return String.join(":", prefix, groupIndex + "", shardIndex + "");
        }
    }

    /**
     * guava依賴獲取hash值。
     */
    private static long hash(String key) {
        Charset charset = Charset.forName("UTF-8");
        return Math.abs(Hashing.murmur3_128().hashObject(key, Funnels.stringFunnel(charset)).asInt());
    }

}

依賴類:

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>31.0.1-jre</version>
</dependency>

5. 計(jì)算日活东臀、月活着饥、留存率的具體方法

具體實(shí)施:使用redis的bitmap

  1. 設(shè)置一個key專門用來記錄用戶日活的,可以使用時間來翻滾比如1號的key為active01.
  2. 使用每個用戶的唯一標(biāo)識映射一個偏移量惰赋,比如使用id宰掉,這里可以把id換算成一個數(shù)字或直接使用id的二進(jìn)制值作為該用戶在當(dāng)天是否活躍偏移量
  3. 用戶登錄則把該用戶偏移量上的位值設(shè)置為1
  4. 每天按日期生成一個位圖(bitmap)
  5. 計(jì)算日活則使用bitcount即可獲得一個key的位值為1的量
  6. 計(jì)算月活(一個月內(nèi)登陸的用戶去重總數(shù))即可把30天的所有bitmap做or計(jì)算呵哨,然后再計(jì)算bitcount
  7. 計(jì)算留存率(次日留存=昨天今天連續(xù)登錄的人數(shù)/昨天登錄的人數(shù)) 即昨天的bitmap與今天的bitmap做and計(jì)算就是連續(xù)登錄的再做bitcount就得到連續(xù)登錄人數(shù),再bitcount得到昨天登錄人數(shù)轨奄,就可以通過公式計(jì)算出次日留存孟害。

文章參考:
Redis:Bitmap的setbit,getbit,bitcount,bitop等使用與應(yīng)用場景

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市挪拟,隨后出現(xiàn)的幾起案子挨务,更是在濱河造成了極大的恐慌,老刑警劉巖玉组,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件谎柄,死亡現(xiàn)場離奇詭異,居然都是意外死亡惯雳,警方通過查閱死者的電腦和手機(jī)朝巫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來吨凑,“玉大人捍歪,你說我怎么就攤上這事⊥叶郏” “怎么了糙臼?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長恩商。 經(jīng)常有香客問我变逃,道長,這世上最難降的妖魔是什么怠堪? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任揽乱,我火速辦了婚禮,結(jié)果婚禮上粟矿,老公的妹妹穿的比我還像新娘凰棉。我一直安慰自己,他們只是感情好陌粹,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布撒犀。 她就那樣靜靜地躺著,像睡著了一般掏秩。 火紅的嫁衣襯著肌膚如雪或舞。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天蒙幻,我揣著相機(jī)與錄音映凳,去河邊找鬼。 笑死邮破,一個胖子當(dāng)著我的面吹牛诈豌,可吹牛的內(nèi)容都是我干的仆救。 我是一名探鬼主播,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼矫渔,長吁一口氣:“原來是場噩夢啊……” “哼派桩!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起蚌斩,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤铆惑,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后送膳,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體员魏,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年叠聋,在試婚紗的時候發(fā)現(xiàn)自己被綠了撕阎。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡碌补,死狀恐怖虏束,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情厦章,我是刑警寧澤镇匀,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站袜啃,受9級特大地震影響汗侵,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜群发,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一晰韵、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧熟妓,春花似錦雪猪、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至告材,卻和暖如春坤次,著一層夾襖步出監(jiān)牢的瞬間古劲,已是汗流浹背斥赋。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留产艾,地道東北人疤剑。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓滑绒,卻偏偏與公主長得像,于是被迫代替她去往敵國和親隘膘。 傳聞我的和親對象是個殘疾皇子疑故,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評論 2 345

推薦閱讀更多精彩內(nèi)容