面試官嘲笑我,這你都不會?

01 背景

你們好,我是阿沐!你的收穫即是個人喜歡,你的點贊即是對個人承認。php

多年前剛畢業出來工做的時候,那個時候剛畢業對緩存的使用基本上能夠說不多涉及,在大學作課件設計或者小型項目也都是用不到緩存,再者說了我大學是作嵌入式寫彙編語言和c語言的。html

當時出實習去找工做並不順利,面試官問了知道redis和memcached區別嘛?額,我當時雖然也作了一些功課,就是惡補redis基礎以及應用,可是並非很熟悉;就支支吾吾地回答個只知其一;不知其二。而後又問我,假如公司如今作一個app,app有簽到功能,你該怎麼作?mysql

「這很難嘛?」,直接對着面試官說用mysql存儲就能夠啦。git

「目前咱們有20多萬的活躍用戶,你肯定只用mysql存儲嘛?你就不怕把咱們數據庫搞炸了?」,面試官很費解的看着我,內心好像在對我說,真是個菜鳥。這麼簡單就不會?github

正好最近有小夥伴問了這關於簽到方面的知識點,這裏就來講一說!面試

02 位圖是什麼梗?

官網說:位圖並非一個真實的數據類型,而是定義在字符串類型上的面向位的操做集合。位圖的最小單位是比特(bit),每個bit的值只能是0或者1。redis中字符串限制最大爲512M,因此位圖中最大可容納2^32(42億)個不一樣的位。redis

能夠將位圖看作是一個bit數組,數組的下標就是偏移量sql

它的優勢:內存開銷小,效率高且操做簡單。數據庫

位圖展現結構圖

03 咱們用位圖能作什麼?

  • 統計用戶每日簽到(最最最經常使用的)
  • 統計日活/越活活躍用戶(擴展:精確數據:用hivespark統計;非精確數據,用HyperLogLog)
  • 用戶在線狀態實時統計(1億用戶大概:須要12MB的存儲空間)
  • 數據雙寫去重
  • 視頻、文章等等的已讀或未讀狀態

04 位圖有哪些指令可使用?

一、查找select指令操做:後端

getbit指令:getbit key offset 獲取指定偏移量上的位(bit);時間複雜度O(1)。

注意:

當key不存在或者offset比字符串值的長度大時,則返回0。

bitcount指令:bitcount key [start] [end]獲取指定範圍內比特位的數量;時間複雜度O(n)。

注意:

當key不存在時會被當成是空字符串來處理,因此返回值爲0。

bitpos指令:bittops key bit [start] [end] 獲取位圖中第一個值爲bit的二進制位的位置;時間複雜度: O(n),其中n爲位圖包含的二進制位數量。
複製代碼

二、添加insert指令操做:

setbit指令:setbit key offset value 設置key所儲存的字符串值,或清除指定偏移量上的位(bit);時間複雜度:O(1)。

注意:

一、位的設置或清除取決於value參數,0或者1。

二、當key不存在時,自動生成一個新的字符串值。

三、位數組會自動伸展擴充,offet偏移量設置超出現現有的內容範圍,爲確保value值在指定偏移量上,會經過擴容,空白位置用0填充補上,

四、offet參數值必須大於或者等於0,小於2^32(字符串最大值是512M)
複製代碼

下面是我整理哈希類型命令的時間複雜度,你們能夠參考此表:

指令 時間複雜度
getbit key offset O(1)
bitcount key [start] [end] O(n),n是位數量的大小
setbit key offset value O(1)
bittops key bit [start] [end] O(n),n是二進制位數量

05 位圖實踐系列

咱們要實現的功能是最最最經常使用的簽到,實現功能以下:

  • 一、簽到打卡
  • 二、檢測某一天是否打卡(由於大部分app只會存在當日是否簽到按鈕)
  • 三、獲取用戶某月打卡記錄列表
  • 四、統計用戶某月打卡總次數
  • 五、獲取用戶某月連續打卡次數
  • 六、用戶補籤

咱們首先建立一個關係型的用戶打卡信息數據表,存儲用戶的打卡信息,這裏強調一點:

看過網絡上不少人只用redis才存儲用戶打卡信息,並不實際落地處理,最奇怪的是別人問,假如redis掛了或者怎樣,運營或者產品想要獲取數據分析,咱們該怎麼辦?

有一位博主仁兄回答是:「redis高可用、redis持久化、後臺寫一個查詢緩存接口,這些你不會嘛?」,下面評價:博主是認真回答的嘛?來逗咱們玩呢?我也是笑了笑.....

說一說建表記錄的緣由:

一、在大數據時代,任何有價值的信息都要收集起來,跟用戶活躍度DAU相關的都是比較重要的

二、跟用戶相關聯的,產品營運一定會須要這些數據來分析用戶行爲,認爲簽到送禮帶來的收益

三、不能過分的去依賴緩存,一旦緩存出問題或者崩盤,數據丟失都是一個大問題,用戶的反饋投訴極具增長

四、用戶存在質疑時,能夠快速經過落地數據進行排查問題

五、緩存出現問題時,能夠經過數據庫的記錄進行數據回源,保證數據一致

CREATE TABLE `mumu_sign_202105` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵自增ID',
  `user_id` varchar(255) NOT NULL DEFAULT '' COMMENT '用戶暱稱',
  `sign_date` date NOT NULL DEFAULT '0000-00-00' COMMENT '簽到時間',
  `create_at` int(10) NOT NULL DEFAULT '0' COMMENT '建立時間',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uniq_uid_date` (`user_id`,`sign_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用戶信息基礎表';
複製代碼

你們看到這個表名是否是很奇怪爲啥帶上_202105呢,主要是這裏用了按月分表的原理,由於用戶量大,儘量的保證一張表的數據在500w一下,固然有的性能比較好1000以上優化以後速度也是槓槓的。

不過建議仍是按月分表,每月度結束以後,數據能夠同步給hive、es、sphinx均可以,咱們只須要保證半年內的表數據,其他能夠用完刪除便可。

因此每當咱們設計表或者作一項功能,必定要考慮預估量以及爲何要這樣作,能帶來的效益是什麼?這樣你會變的愈來愈優秀,思惟邏輯愈來愈縝密。

insert into `mumu_user` (`user_id`, `sign_date`,`create_at`) VALUES
('10001','2021-04-22', unix_timestamp()),
('10001','2021-04-24', unix_timestamp()),
('10001','2021-04-25', unix_timestamp()),
('10001','2021-04-26', unix_timestamp()),
('10001','2021-04-30', unix_timestamp());
複製代碼

那麼下面咱們開始代碼邏輯梳理:

① 簽到打卡系列
/**
 * @desc 簽到
 * @param string $date
 * @return int
 */
public function signIn($date = '')
{
    //獲取當月用戶簽到的緩存key
    $key = $this->getKey($date);
    
    // this->getCurrentDay 獲取當日是本月的第幾天而且減去1就是設置位圖的下標
    return $this->redis->setBit($key, $this->getCurrentDay($date), 1);
}

require_once  './Bitmap.php'; //引入位圖類

$user_id = 1001; //傳入用戶ID

$date = "2021-04-24"; //傳入指定的簽到日期

$bitmap = new Bitmap($user_id); //實例化位圖類

echo $bitmap->signIn($date); //輸出1

-- 終端操做
localhost:6379> SETBIT user:sign:1001:202104 23 1
(integer) 0
複製代碼

是否是看起來超級簡單,無非就是使用setbit指令來給用戶存儲簽到狀態爲1。記住這裏寫入緩存以前必定要先插入數據表保證數據庫落地成功,聰明的你可能在表中看到了設置的惟一鍵,目的是保證一個用戶天天的只有一次簽到記錄。

② 檢測某一天是否已打卡
/**
 * @desc 判斷用戶在某一天是否簽到
 * @param string $date
 * @return int
 */
public function judgeUserSign($date = '')
{
    $key = $this->getKey($date);

    return $this->redis->getBit($key, $this->getCurrentDay($date));
}

require_once  './Bitmap.php';

$user_id = 1001;

$date = "2021-04-24";

$bitmap = new Bitmap($user_id);

echo $bitmap->judgeUserSign($date);// 輸出值爲1

-- 終端操做
localhost:6379> GETBIT user:sign:1001:202104 23 //這裏23是指位圖下標,由於是從0開始,因此存儲時減一操做了 變成了23
(integer) 1
localhost:6379> GETBIT user:sign:1001:202104 24 //這裏實際查詢的是4月25號是否簽到
(integer) 0
複製代碼
③ 獲取用戶某月打卡記錄列表
/**
 * @desc 獲取用戶本月簽到的記錄列表
 * @param string $date
 * @return mixed
 */
public function getUserAllSign($date = '')
{
    // 獲取本月或者指定月的簽到緩存key
    $key = $this->getKey($date);

    // 很遺憾 本地reddi並無支持這個函數
    $result = $this->redis->bitField($key); // 正常這裏應該返回的是數組 我這裏使用不了 至關於模擬
    
    // 存儲本月的簽到結果集
    $list = [];

    // 獲取指定月的月數
    $days = $this->getMonthDays($date);

    // 從低位到高位遍歷,0表示未簽到;1表示已簽到
    for ($i = $days; $i > 0; $i--) {
        // 本月已經循環完直接退出
        if ($i < 0) break;
        // 定義當前的日期是多少
        $local_date = date('Y-m') . '-' . $i;
        
        // 右移再左移,若是不等於本身說明最低位是 1,表示已簽到 
        $flag = ($result >> 1 << 1) != $result ? true : false;
        
         // 若是已簽到,添加標記爲1,不然爲0
        $list[$local_date] = $flag ? 1 : 0;
        
         // 而後右移一位從新賦值計算
        $result >>= 1;
    }

    return $list;
}

require_once  './Bitmap.php';

$user_id = 1001;

$date = "2021-04-24";

$bitmap = new Bitmap($user_id);

$result = $bitmap->getUserAllSign($date);

var_dump($result);

//執行結果集
array(30) {
  ["2021-04-30"]=>
  int(0)
  ["2021-04-29"]=>
  int(0)
  ["2021-04-28"]=>
  int(0)
  ["2021-04-27"]=>
  int(0)
  ["2021-04-26"]=>
  int(0)
  ["2021-04-25"]=>
  int(0)
  ["2021-04-24"]=>
  int(1)
  ["2021-04-23"]=>
  int(0)
  ["2021-04-22"]=>
  int(0)
  ["2021-04-21"]=>
  int(0)
  ["2021-04-20"]=>
  int(0)
  ["2021-04-19"]=>
  int(0)
  ["2021-04-18"]=>
  int(0)
  ["2021-04-17"]=>
  int(0)
  ["2021-04-16"]=>
  int(0)
  ["2021-04-15"]=>
  int(0)
  ["2021-04-14"]=>
  int(0)
  ["2021-04-13"]=>
  int(0)
  ["2021-04-12"]=>
  int(0)
  ["2021-04-11"]=>
  int(0)
  ["2021-04-10"]=>
  int(0)
  ["2021-04-9"]=>
  int(0)
  ["2021-04-8"]=>
  int(0)
  ["2021-04-7"]=>
  int(0)
  ["2021-04-6"]=>
  int(0)
  ["2021-04-5"]=>
  int(0)
  ["2021-04-4"]=>
  int(0)
  ["2021-04-3"]=>
  int(0)
  ["2021-04-2"]=>
  int(0)
  ["2021-04-1"]=>
  int(0)
}
//咱們再看下終端執行的結果
localhost:6379> BITFIELD user:sign:1001:202104 get u30 0
1) (integer) 64
複製代碼

注意2021-04-24這日期是咱們上面設置簽到了,返回的64這個是二進制數據:0100 0000 這樣是否是就很清晰了,表示24號簽到了

你們是否是有點疑問,爲啥沒有支持bitField指令,redis在3.2版本以後就新增了這個強大的指令bitfield。r若是沒有這個指令的出現,估計上面的代碼就要緩存經過管道命令批量獲取幾十天的數據了。可是有了它就徹底不是一個概念,它一條命令就能夠完成全部值獲取。普及下bitfield命令:

官方文檔:
BITFIELD key [GET type offset] [SET type offset value] [INCRBY type offset increment] [OVERFLOW WRAP|SAT|FAIL] 意思是:中括號的意思是指支持它的子命令;get、set、incrby

時間複雜度:O(1)

舉個例子解析:
bitfield key get u8 0 
一、key指咱們須要操做的緩存key
二、get是bitfield的子命令 用來select
三、u8表示無符號數+30位整形位數(i8表示有符號數)
四、0表示返回指定的位偏移量
複製代碼

科普一下:所謂的無符號數是指非負數,沒有符號位置,獲取的位數組所有都是值;無符號數是指一個負數,獲取到的值的第一位是符號位,剩下的纔是可用的值。你們想了解的話,能夠看看計算機組成原理在bibi就能夠看的。

④ 獲取用戶當月打卡總數
/**
 * @Desc 獲取用戶當月打卡總數
 * @param string $date
 * @return int
 */
public function getSumSignCount($date = '')
{
    $key = $this->getKey($date);

    return $this->redis->bitCount($key);
}
$bitmap = new Bitmap($user_id);

$result = $bitmap->getSumSignCount($date);

var_dump($result); //結果輸出1  由於咱們4月份就打卡了一天

//終端執行
localhost:6379> BITCOUNT user:sign:1001:202104
(integer) 1
複製代碼
⑤ 獲取用戶連續簽到次數
/**
 * @desc 獲取用戶連續簽到的次數
 * @param string $date
 * @return int
 */
public function getContinuousSignCount($date = '')
{
    $key = $this->getKey($date);

    // 獲取今每天數
    $days = $this->getCurrentDay($date);

    //// 獲取用戶從當前日期開始到 1 號的全部簽到狀態  不過很遺憾 本地reddi並無支持這個函數
    $result = $this->redis->bitField($key, 'u' . $days, 0); // 正常這裏應該返回的是數組

    // 連續簽到計數器總數
    $signCount = 0;

    $value = isset($result[0]) ? $result[0] : 0;

    // 經過位移計算連續簽到次數
    for ($i = $days; $i > 0; $i--) // i 表示位移操做次數
    {
        if ($i < 0) break; //超出則終止循環

        // 先右移再左移,若是等於本身說明最低位是 0,表示未簽到
        if ($value >> 1 << 1 == $value) { //存在用戶當天還未簽到,因此要排除掉
            // 低位 0 且非當天說明連續簽到中斷了
            if ($i != $days) break;
        } else {
            // 若是不等於本身說明最低位是1,表示已經簽到
            $signCount++;
        }

        // 右移一位並從新賦值,至關於把最低位丟棄一位而後從新計算
        $value >>= 1;
    }
    return $signCount;
}

$bitmap = new Bitmap($user_id);

$result = $bitmap->getContinuousSignCount($date);

var_dump($result); //執行結果 只有2次連續簽到 後面我設置了 20號 24號簽到了
//終端執行
localhost:6379> BITFIELD user:sign:1001:202104 get u25 0
1) (integer) 22
複製代碼

這個連續簽到次數主要是考驗你們對二進制的位運算的熟練程度,知道如何進行位運算,這樣就能更好地知道簽到如何使用。

科普下左移右移:

a < < a << b (左移) 將 a 中的位向左移動 a 中的位向左移動 b 次(每一次移動都表示「乘以 2」);左移時右側以零填充,符號位被移走意味着正負號不被保留。

a > > a >> b (右移) 將 a 中的位向右移動 a 中的位向右移動 b 次(每一次移動都表示「除以 2」);右移時左側以符號位填充,意味着正負號被保留。

⑥ 用戶補籤
/**
 * @desc 用戶補籤
 * @param string $date
 * @return bool|int
 */
public function rebuildSign($date = '')
{
    $key = $this->getKey($date);

    // 先檢測當前用戶這一天是否已經簽到
    if ($this->judgeUserSign($date)) return false;

    return $this->signIn($date);
}
//這個就很簡單了,你們能夠本身操做一下
複製代碼

位圖實戰代碼倉庫地址:github.com/woshiamu/am…

最後總結

本文主要是經過實際的應用場景纔講解redis的位圖使用。如何應用,如何實踐,經過一個個的代碼案例執行你就能更加了解位圖究竟是怎麼回事。

位圖是一個佔用內存且能存儲大量數據的一個字符串。給你們一個小小的建議,在看文章或者看書籍時,必定要看完以後動手實踐,由於實踐纔是檢驗真理的惟一標準;若是還在使用set hash simember來作簽到功能,能夠嘗試改換而後對比性能,也提升下咱們的技術水準以及接口訪問速度。

看完文章小夥伴們對位圖的使用是否有進一步的瞭解?若是阿沐的文章感受有幫助或者有不足之處,請在評論下面留言。

最後,歡迎關注個人我的公衆號「我是阿沐」,會不按期的更新後端知識點和學習筆記。也歡迎直接公衆號私信或者郵箱聯繫我,咱們能夠一塊兒學習,一塊兒進步。

好了,我是阿沐,一個不想30歲就被淘汰的打工人 ⛽️ ⛽️ ⛽️ 。

相關文章
相關標籤/搜索