概要
主要用於節約內存適用的場景,例如用戶簽到,用戶登錄 狀態,位圖的常見應用是用來存儲狀態值。
在我們平時開發過程中,會有一些 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)); } }