3.千萬日活的簽到系統(tǒng)如何設(shè)計(jì)?

假設(shè)有個(gè)百萬簽到系統(tǒng),記錄用戶簽到記錄,簽了記錄1,沒簽記錄 0,如果我們用redis的string存儲(chǔ),一年就要存1000000*365個(gè)key,會(huì)占用大量的內(nèi)存。

為了解決這個(gè)問題,Redis 提供了位圖數(shù)據(jù)結(jié)構(gòu),這樣每天的簽到記錄只占據(jù)一個(gè)位,365 天就是 365 個(gè)位,46 個(gè)字節(jié) (一個(gè)稍長一點(diǎn)的字符串) 就可以完全容納下,這就大大節(jié)約了存儲(chǔ)空間。

bitmap 存儲(chǔ)的是連續(xù)的二進(jìn)制數(shù)字(0 和 1),通過 bitmap, 只需要一個(gè) bit 位來表示某個(gè)元素對(duì)應(yīng)的值或者狀態(tài),key 就是對(duì)應(yīng)元素本身 。我們知道 8 個(gè) bit 可以組成一個(gè) byte,所以 bitmap 本身會(huì)極大的節(jié)省儲(chǔ)存空間。

1.常用命令

byte[] bytes = "hzy".getBytes();
for (byte b : bytes) {
 System.out.println(Integer.toBinaryString(b));
}

執(zhí)行結(jié)果
1101000
1111010
1111001

我們使用位圖設(shè)置一個(gè)字符串"hzy",基本命令是"setbit key [offset] [value]",我們先得到ascll碼對(duì)應(yīng)的二進(jìn)制數(shù)據(jù):01101000 01111010 01111001

圖片
## 零存整取
127.0.0.1:6379> setbit test 1 1
(integer) 0
127.0.0.1:6379> setbit test 2 1
(integer) 0
127.0.0.1:6379> setbit test 4 1
(integer) 0
127.0.0.1:6379> get test
"h"

127.0.0.1:6379> setbit test 9 1
(integer) 0
127.0.0.1:6379> setbit test 10 1
(integer) 0
127.0.0.1:6379> setbit test 11 1
(integer) 0
127.0.0.1:6379> setbit test 12 1
(integer) 0
127.0.0.1:6379> setbit test 14 1
(integer) 0
127.0.0.1:6379> get test
"hz"

127.0.0.1:6379> setbit test 17 1
(integer) 0
127.0.0.1:6379> setbit test 18 1
(integer) 0
127.0.0.1:6379> setbit test 19 1
(integer) 0
127.0.0.1:6379> setbit test 20 1
(integer) 0
127.0.0.1:6379> setbit test 23 1
(integer) 0
127.0.0.1:6379> get test
"hzy"

## 零存零取
127.0.0.1:6379> getbit test 1
(integer) 1

## 整存零取
127.0.0.1:6379> set test2 h
OK
127.0.0.1:6379> getbit test2 1
(integer) 1

2.bitcount&bitpos

我們可以通過 bitcount 統(tǒng)計(jì)用戶一共簽到了多少天。通過 bitpos 指令查找用戶從哪一天開始第一次簽到,如果指定了范圍參數(shù)[start, end],就可以統(tǒng)計(jì)在某個(gè)時(shí)間范圍內(nèi)用戶簽到了多少天。用戶自某天以后的哪天開始簽到。

但是需要注意的是 start 和 end 參數(shù)是字節(jié)索引,也就是說指定的位范圍必須是 8 的倍數(shù),而不能任意指定。因?yàn)檫@個(gè)設(shè)計(jì),我們無法直接計(jì)算某個(gè)月內(nèi)用戶簽到了多少天,而必須要將這個(gè)月所覆蓋的字節(jié)內(nèi)容全部取出來 (getrange 可以取出字符串的子串) 然后在內(nèi)存里進(jìn)行統(tǒng)計(jì),這個(gè)非常繁瑣。

bitcount命令使用,bitcount key [start] [end]

127.0.0.1:6379> set name hzy
OK
127.0.0.1:6379> bitcount name
(integer) 13
127.0.0.1:6379> bitcount name 0 0  # 第一個(gè)字符中1的個(gè)數(shù)
(integer) 3
127.0.0.1:6379> bitcount name 0 1  # 前兩個(gè)字符中1的個(gè)數(shù)
(integer) 8

bitpos命令使用,bitpos key bit [start] [end]

hzy對(duì)應(yīng)的ascll碼對(duì)應(yīng)的二進(jìn)制數(shù)據(jù):01101000 01111010 01111001

127.0.0.1:6379> bitpos name 0 #第一個(gè)0的下標(biāo)
(integer) 0
127.0.0.1:6379> bitpos name 1 #第一個(gè)1的下標(biāo)
(integer) 1
127.0.0.1:6379> bitpos name 1 2 10 #從第三個(gè)字符起,第一個(gè)1的下標(biāo)(也就是說hz不算,只有y參與)
(integer) 17

3.bitfield

我們?cè)O(shè)置 (setbit) 和獲取 (getbit) 指定位的值都是單個(gè)位的,如果要一次操作多個(gè)位,就必須使用管道來處理。不過 Redis 的 3.2 版本提供了bitfield指令。

bitfield key [GET type offset] [SET type offset value] [INCRBY type offset increment] [OVERFLOW WRAP|SAT|FAIL]

## 模擬用戶2021年7月15日簽到,偏移量從0開始
127.0.0.1:6379> setbit userid:sign:202107 14 1
(integer) 0
## 模擬用戶2021年7月16日簽到,偏移量從0開始
127.0.0.1:6379> setbit userid:sign:202107 15 1
(integer) 0
## 模擬用戶2021年7月31日簽到,偏移量從0開始
127.0.0.1:6379> setbit userid:sign:202107 30 1
(integer) 0
## 獲取用戶2021年7月的簽到數(shù)據(jù)
127.0.0.1:6379> bitfield userid:sign:202107 get u31 0
1) (integer) 98305
// 這是偽代碼,key是我寫死的,真實(shí)場(chǎng)景下,userid是用戶的ID
public static void main(String[] args) {
 LocalDate date = LocalDate.now();
 Map<String, Boolean> signMap = new TreeMap<>();
 List<Long> list = jedis.bitfield("userid:sign:202107", "GET", String.format("u%d", date.lengthOfMonth()), "0");
 if (list != null && list.size() > 0) {
  // 由低位到高位,為0表示未簽,為1表示已簽
  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;
  }
 }
 System.out.println(signMap.toString());
}
private static String formatDate(LocalDate date, String pattern) {
 return date.format(DateTimeFormatter.ofPattern(pattern));
}
// 執(zhí)行結(jié)果
{2021-07-01=false, 2021-07-02=false, 2021-07-03=false, 
2021-07-04=false, 2021-07-05=false, 2021-07-06=false, 
2021-07-07=false, 2021-07-08=false, 2021-07-09=false, 
2021-07-10=false, 2021-07-11=false, 2021-07-12=false, 
2021-07-13=false, 2021-07-14=false, 2021-07-15=true, 
2021-07-16=true, 2021-07-17=false, 2021-07-18=false, 
2021-07-19=false, 2021-07-20=false, 2021-07-21=false, 
2021-07-22=false, 2021-07-23=false, 2021-07-24=false, 
2021-07-25=false, 2021-07-26=false, 2021-07-27=false, 
2021-07-28=false, 2021-07-29=false, 2021-07-30=false, 
2021-07-31=true}

注意:i是有符號(hào)整數(shù) u是無符號(hào)整數(shù),例如u8是一個(gè)8位的無符號(hào)整數(shù),有符號(hào)最多可以獲取64位,無符號(hào)最多可以獲取63位

我們使用bitfield key set子指令,把最后一個(gè)字符串y,改為大寫的Y,Y的ASCII碼是89

127.0.0.1:6379> set name hzy 
OK 
127.0.0.1:6379> bitfield name set u8 16 89 
1) (integer) 121 
127.0.0.1:6379> get name 
"hzY"
最后編輯于
?著作權(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),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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