面试官嘲笑我,这你都不会?

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岁就被淘汰的打工人 ⛽️ ⛽️ ⛽️ 。

相关文章
相关标签/搜索