Redis位圖-節衣縮食

概要

主要用於節約內存適用的場景,例如用戶簽到,用戶登錄 狀態,位圖的常見應用是用來存儲狀態值

在我們平時開發過程中,會有一些 bool 型數據需要存取,比如用戶一年的簽到記錄, 簽了是 1,沒簽是 0,要記錄 365 天。如果使用普通的 key/value,每個用戶要記錄 365個,當用戶上億的時候,需要的存儲空間是驚人的。

爲了解決這個問題,Redis 提供了位圖數據結構-byte數組這樣每天的簽到記錄只佔據一個位,365 天就是 365 個位,46 個字節 (一個稍長一點的字符串) 就可以完全容納下,這就大大 節約了存儲空間。

 

 

統計和查找

基本使用:「零存整取」,同樣我們還也可以「零存零取」,「整存零 取」。「零存」就是使用 setbit 對位值進行逐個設置,「整存」就是使用字符串一次性填充 所有位數組,覆蓋掉舊值。


127.0.0.1:6379> setbit w 1 1 
(integer) 0
127.0.0.1:6379> setbit w 2 1 
(integer) 0
127.0.0.1:6379> setbit w 4 1 
(integer) 0
127.0.0.1:6379> getbit w 1 
(integer) 1
127.0.0.1:6379> getbit w 2 
(integer) 1 
127.0.0.1:6379> getbit w 4
(integer) 1 
127.0.0.1:6379> getbit w 5 
(integer) 0

 

Redis 提供了位圖統計指令 bitcount 和位圖查找指令 bitpos,bitcount 用來統計指定位 置範圍內 1 的個數,bitpos 用來查找指定範圍內出現的第一個 0 或 1。

比如我們可以通過 bitcount 統計用戶一共簽到了多少天,通過 bitpos 指令查找用戶從 哪一天開始第一次簽到。如果指定了範圍參數[start, end],就可以統計在某個時間範圍內用戶 簽到了多少天,用戶自某天以後的哪天開始簽到。

遺憾的是, start 和 end 參數是字節索引,也就是說指定的位範圍必須是 8 的倍數, 而不能任意指定。這很奇怪,我表示不是很能理解 Antirez 爲什麼要這樣設計。因爲這個設 計,我們無法直接計算某個月內用戶簽到了多少天,而必須要將這個月所覆蓋的字節內容全 部取出來 (getrange 可以取出字符串的子串) 然後在內存裏進行統計,這個非常繁瑣。

接下來我們簡單試用一下 bitcount 指令和 bitpos 指令:

127.0.0.1:6379> set w hello
OK
127.0.0.1:6379> bitcount w 
(integer) 21
127.0.0.1:6379> bitcount w 0 0  #  1 
(integer) 3
127.0.0.1:6379> bitcount w 0 1  #  1 
(integer) 7
127.0.0.1:6379> bitpos w 0 #  0  
(integer) 0
127.0.0.1:6379> bitpos w 1 #  1  
(integer) 1
127.0.0.1:6379> bitpos w 1 1 1  #  1 
(integer) 9
127.0.0.1:6379> bitpos w 1 2 2  #  1 
(integer) 17

demo-統計用戶登錄狀態

package com.zpl.redis;

import redis.clients.jedis.Jedis;

import java.time.LocalDate;
import java.time.temporal.ChronoUnit;

public class UserLoginStatusService {
    private static final String host="127.0.0.1";

    private static final int port=6379;

    private static final Jedis jedis=new Jedis(host,port);

    //日期的初始值(也可以理解爲用戶的註冊時間),
    //下文需要使用日期的偏移量作爲redis位圖的offset,
    //因此需要將要保存登錄狀態的日期減去該初始日期。
    //這裏使用了Java 8的新日期API
    private static final LocalDate beginDate=LocalDate.of(2018,1,1);

    static {
        jedis.connect();
    }

    /**
     * @Description: 零存  存儲用戶登錄狀態  
     * @Param: [userId, date:第幾位 日期 offset, isLogin] 
     * @return: void 
     * @Author: 九江彭于晏
     * @Date: 2020/11/3 
     */
    public void setLoginStatus(String userId, LocalDate date,boolean isLogin){
        long offset = getDateDuration(beginDate, date);
        jedis.setbit(userId,offset,isLogin);
    }

    public Boolean getLoginStatus(String userId,LocalDate date){
        long offset = getDateDuration(beginDate, date);
        return jedis.getbit(userId,offset);
    }

    private long getDateDuration(LocalDate start ,LocalDate end){
        return start.until(end, ChronoUnit.DAYS);
    }

    public static void main(String[] args) {
        UserLoginStatusService userLoginStatusService=new UserLoginStatusService();
        String userId="user_2";
        LocalDate today = LocalDate.now();
        userLoginStatusService.setLoginStatus(userId,today,true);
        boolean todayLoginStatus = userLoginStatusService.getLoginStatus(userId, today);
        System.out.println(String.format("The loginStatus of %s in %s is %s",userId,today,todayLoginStatus));
        LocalDate yesterday = LocalDate.now().minusDays(1);
        boolean yesterdayLoginStatus = userLoginStatusService.getLoginStatus(userId, yesterday);
        System.out.println(String.format("The loginStatus of %s in %s is %s",userId,yesterday,yesterdayLoginStatus));
        //統計登錄天數 
        System.out.println(getAllCount(userId));
    }
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章