通過一個bit位來表示某個元素對應的值或者狀態,其中的key就是對應元素本身。8個bit可以組成一個Byte,所以bitmap本身會極大的節省儲存空間。
語法:setbit key offset value
描述:
對key所儲存的字符串值,設置或清除指定偏移量上的位(bit)。
位的設置或清除取決於 `value` 參數,可以是 `0` 也可以是 `1` 。
當 `key` 不存在時,自動生成一個新的字符串值。
字符串會進行伸展(grown)以確保它可以將 `value` 保存在指定的偏移量上。當字符串值進行伸展時,空白位置以 `0` 填充。
注意:
`offset` 參數必須大於或等於 `0` ,小於 2^32 (bit 映射被限制在 512 MB 之內)。
因為 Redis 字符串的大小被限制在 512 兆(megabytes)以內, 所以用戶能夠使用的最大偏移量為 2^29-1(536870911) , 如果你需要使用比這更大的空間, 請使用多個 `key。`
當生成一個很長的字符串時, Redis 需要分配內存空間, 該操作有時候可能會造成服務器阻塞(block)。 在2010年出產的Macbook Pro上, 設置偏移量為 536870911(512MB 內存分配)將耗費約 300 毫秒, 設置偏移量為 134217728(128MB 內存分配)將耗費約 80 毫秒, 設置偏移量 33554432(32MB 內存分配)將耗費約 30 毫秒, 設置偏移量為 8388608(8MB 內存分配)將耗費約 8 毫秒。
語法:bitcount key [start] [end] 返回值:被設置為 1 的位的數量 描述: 計算給定字符串中,被設置為 1 的比特位的數量 一般情況下,給定的整個字符串都會被進行計數,通過指定額外的 start 或 end 參數,可以讓計數只在特定的字節上進行。注意不是bit位,是字節。 例如:假如key1的value是00001100 11001000 11110000 <1> bitcount key1 0 0 這個是獲取key1中第0個字節組中bit為1的count,也就是00001100 中查詢,返回2 <2> bitcount key1 0 1 這個是獲取key1中第0-1個字節組中bit為1的count,也就是00001100 11001000中查詢,返回5 <3> bitcount key1 1 2 這個是獲取key1中第1-2個字節組中bit為1的count,也就是11001000 11110000中查詢,返回7 start 和 end 參數的設置和 GETRANGE key start end 命令類似,都可以使用負數值: 比如 -1表示最后一個bit, -2 表示倒數第二個bit,以此類推。 不存在的 key 被當成是空字符串來處理,因此對一個不存在的 key 進行 BITCOUNT 操作,結果為 0 。
使用場景:用戶簽到
考慮到每月初需要重置連續簽到次數,按用戶每月存一條簽到數據(也可以每年存一條數據)。Key的格式為u:sign:uid:yyyyMM
,Value則采用長度為4個字節(32位)的位圖(最大月份只有31天)。位圖的每一位代表一天的簽到,1表示已簽,0表示未簽。
例如u:sign:1000:201902
表示ID=1000的用戶在2019年2月的簽到記錄。
# 用戶2月17號簽到 SETBIT u:sign:1000:201902 16 1 # 偏移量是從0開始,所以要把17減1 # 檢查2月17號是否簽到 GETBIT u:sign:1000:201902 16 # 偏移量是從0開始,所以要把17減1 # 統計2月份的簽到次數 BITCOUNT u:sign:1000:201902 # 獲取2月份前28天的簽到數據 BITFIELD u:sign:1000:201902 get u28 0 # 獲取2月份首次簽到的日期 BITPOS u:sign:1000:201902 1 # 返回的首次簽到的偏移量,加上1即為當月的某一天
代碼
@Slf4j @Service public class SignService { @Autowired private RedisTemplate redisTemplate; /** * 用戶簽到 * * @param uid * @param localDate * @return */ public Boolean doSign(int uid, LocalDate localDate) { int offset = localDate.getDayOfMonth() - 1; String signKey = buildSignKey(uid, localDate); return redisTemplate.opsForValue().setBit(signKey, offset, true); } /** * 檢查用戶是否簽到 * * @param uid * @param date * @return */ public Boolean checkSign(int uid, LocalDate date) { int offset = date.getDayOfMonth() - 1; String signKey = buildSignKey(uid, date); return redisTemplate.opsForValue().getBit(signKey, offset); } /** * 獲取簽到次數 * * @param uid * @param date * @return */ public long getSignCount(int uid, LocalDate date) { String signKey = buildSignKey(uid, date); return (long) redisTemplate.execute((RedisCallback<Long>) conn -> conn.bitCount(signKey.getBytes())); } /** * 獲得當月首次簽到日期 * * @param uid * @param date * @return */ public LocalDate getFirstSignDate(int uid, LocalDate date) { String signKey = buildSignKey(uid, date); long pos = (long) redisTemplate.execute((RedisCallback<Long>) conn -> conn.bitPos(signKey.getBytes(), true)); return pos < 0 ? null : date.withDayOfMonth((int) (pos + 1)); } /** * 獲取連續簽到次數 * 0是高位 * * @return */ public long getContinuousSignCount(int uid, LocalDate date) { int signCount = 0; String signKey = buildSignKey(uid, date); int dayOfMonth = date.getDayOfMonth(); List<Long> list = (List<Long>) redisTemplate.execute((RedisCallback<List<Long>>) conn -> conn.bitField(signKey.getBytes(), BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth)).valueAt(0))); if (list != null && list.size() > 0) { long v = list.get(0) == null ? 0 : list.get(0); for (int i = 0; i < date.getDayOfMonth(); i++) { /** * 取低位連續不為0的個數即為連續簽到次數,需考慮當天尚未簽到的情況 */ if (v >> 1 << 1 == v) { //低位為0且非當天說明連續簽到中斷了 if (i > 0) { break; } } else { signCount += 1; } v >>= 1; } } return signCount; } public Map<String, Boolean> getSignInfo(int uid, LocalDate date) { Map<String, Boolean> signMap = new HashMap<>(date.getDayOfMonth()); int lengthOfMonth = date.lengthOfMonth(); String signKey = buildSignKey(uid, date); List<Long> list = (List<Long>) redisTemplate.execute((RedisCallback<List<Long>>) conn -> conn.bitField(signKey.getBytes(), BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(lengthOfMonth)).valueAt(0))); if (list != null && list.size() > 0) { long v = list.get(0) == null ? 0 : list.get(0); for (int i = date.lengthOfMonth(); i > 0; i--) { LocalDate d = date.withDayOfMonth(i); signMap.put(formatDate(d, "yyyy-MM-dd"), v >> 1 << 1 != v); v >>= 1; } } return signMap; } /** * 構建簽到key * * @param uid * @param date * @return */ private String buildSignKey(int uid, LocalDate date) { String monthSuffix = formatDate(date, "yyyyMM"); return String.format("u:sign:%d:%s", uid, monthSuffix); } /** * 日期格式化 * * @param date * @param pattern * @return */ public String formatDate(LocalDate date, String pattern) { return date.format(DateTimeFormatter.ofPattern(pattern)); } }
場景二:統計活躍用戶
使用時間作為cacheKey,然后用戶ID為offset,如果當日活躍過就設置為1,那么我該如果計算某幾天/月/年的活躍用戶呢(暫且約定,統計時間內只有有一天在線就稱為活躍),有請下一個redis的命令
命令 BITOP operation destkey key [key ...]
說明:對一個或多個保存二進制位的字符串 key 進行位元操作,並將結果保存到 destkey 上。
說明:BITOP 命令支持 AND 、 OR 、 NOT 、 XOR 這四種操作中的任意一種參數
假設當前站點有5000W用戶,那么一天的數據大約為50000000/8/1024/1024=6MB
使用場景三:用戶在線狀態
對方給我提供了一個查詢當前用戶是否在線的接口。不了解對方是怎么做的,自己考慮了一下,使用bitmap是一個節約空間效率又高的一種方法,只需要一個key,然后用戶ID為offset,如果在線就設置為1,不在線就設置為0,和上面的場景一樣,5000W用戶只需要6MB的空間。