Redis系列二:位图实战,实现打卡签到

来都来了 2020-11-19 18:55:59 ⋅ 1478 阅读

前言

  1. 如果要统计一篇文章的阅读量,可以直接使用 Redis 的 incr 指令来完成
  2. 如果要求阅读量必须按用户去重,那就可以使用 set 来记录阅读了这篇文章的所有用户 id,获取 set 集合的长度就是去重阅读量。但是如果爆款文章阅读量太大,set 会浪费太多存储空间。
  3. 这时候我们就要使用 Redis 提供的 HyperLogLog 数据结构 来代替 set,它只会占用最多 12k 的存储空间就可以完成海量的去重统计。但是它牺牲了准确度,它是模糊计数,误差率约为 0.81%

按照官网的说法,Redis位图Bitmaps不是实际的数据类型,而是在字符串类型上定义的一组 面向位的操作。在Redis中**字符串限制最大为512MB**, 所以位图中最大可以设置**2^32个不同的位(42.9亿个)**。图位的最小单位是比特(bit),每个bit的值只能是0或1。位图适合存bool数据,当某个业务只有两种结果的时候,位图是不二之选 位图的存储大小计算: (maxOffset / 8 / 1024 / 1024)MB。其中maxOffset为位图的最大位数

基本操作

// bit 基本操作
async function baseBitFun() {

    // hello => 二进制  【01101000 01100101 01101100 01101100 01101111
    const [bitKey, bitValue] = ['bitKey', 'hello']
    client.set(bitKey, bitValue, redis.print);
    client.get(bitKey, redis.print);
    // 1. 根据偏移量获取bit上的值 0=》0;1-》1
    client.getbit(bitKey, 1, redis.print);
    // 2. bitcount 获取全部的 1 的总数
    client.bitcount(bitKey, redis.print);
    // 3. setbit 设置指定偏移量的值,0 || 1
    // !offset 参数必须0到 2^32 (bit 映射被限制在 512 MB 之内)。
    // !注意,这里的star和end不是指bit的下标,而是字节(byte)的下标。比如start为1,则实际对应的bit下标为8(1byte = 8 bit)
    client.setbit(bitKey, 0, '1');
    client.bitcount(bitKey, redis.print);
    await sleep(0.2);
    console.log('----获取位置----');
    // 4. 获取第一次出现0或1的位置,获取某个偏移量之后第一次出现0或1的位置
    client.bitpos(bitKey, 0, redis.print)
    client.bitpos(bitKey, 1, redis.print)
    // 1=>8 2=>16 == [8, 16]
    client.bitpos(bitKey, 1, 2, redis.print)
    await sleep(0.2);
    console.log('----BITFIELD----');
    // 设置value=hello
    client.setbit(bitKey, 0, '0');
    client.get(bitKey, redis.print);
    // get i4 0 从0开始取4位即0110,有符号/无符号转十进制为6, 1*2^2+1*2^1 = 6, 结果一致
    const get1Arg = ['get', 'i4', 0];
    // !get i4 4 从4开始取4位即 1000 , 有符号转十进制为 -8, 1000 =>  
    const get2Arg = ['get', 'i4', 4];
    // 5. bitfield
    client.bitfield(bitKey, get1Arg, redis.print);
    client.bitfield(bitKey, get2Arg, redis.print);
    // 6. incrby
    // 2位开始连续4位无符号自增
    const incrby1Arg = ['incrby', 'u4', 2, 1];
    client.bitfield(bitKey, incrby1Arg, redis.print);
    /** 
     * 它用来对指定范围的位进行自增操作。既然提到自增,就有可能出现溢出。如果增加了正数,会出现上溢,如果增加的是负数,就会出现下溢出。
     * 【Redis 默认的处理是折返】。如果出现了溢出,就将溢出的符号位丢掉。如果是 8 位无符号数 255,加 1 后就会溢出,会全部变零。如果是 8 位有符号数 127,加 1 后就会溢出变成 -128
    */
    await sleep(0.2);
    console.log('----BITOP----');

};
20 58 14 19 13 62 98 50 99 54 18 55 48 91 11 21 42 97 3 30 70 49 1 100 7 64 82 75 68 15 26 69 73 41 51 27 84 56 33 16 6 10 94 32 34 90 40 5 78 37 45 57 88 96 8 67 76 4 2 60 89 83 23 39 46 95 77 47 29 63 52 43 25 79 87 22 36 81 44 65 72 59 31 38 61 80 53 24 0 85 35 92 66 74 93 28 17 9 86 12

返回值

/** 
Reply: OK
Reply: hello
Reply: 1
Reply: 21
Reply: 22
----获取位置----
Reply: 3
Reply: 0
Reply: 17
----BITFIELD----
Reply: hello
Reply: 6
Reply: -8
Reply: 11
----BITOP----

*/

BITFIELD

字母数值二进制(高位<-低位)h1040110 1000e1010110 0101l1080110 1100l1080110 1100o1110110 1111

127.0.0.1:6379> set w hello
OK

127.0.0.1:6379> bitfield w get u4 0    #从w的第0个位开始取4个位(0110),结果为无符号数(u)
1) (integer) 6
127.0.0.1:6379> bitfield w get u3 2    #从w的第2个位开始取3个位(101),结果为无符号数(u)
1) (integer) 5
127.0.0.1:6379> bitfield w get i4 0   #从w的第0个位开始取4个位(0110),结果为有符号数(i)
1) (integer) 6
127.0.0.1:6379> bitfield w get i3 2   #从w的第2个位开始取3个位(101),结果为有符号数(i)
1) (integer) -3

127.0.0.1:6379> bitfield w set u8 8 97  #从第9个位开始,将接下来8个位用无符号数97 ( 字母a) 替换
1) (integer) 101
127.0.0.1:6379> get w

BITOP operation destkey key [key …]

基本操作其实还是用终端比较好,直接贴命令

127.0.0.1:6379> set a a                  # 二进制  01100001    
OK    
127.0.0.1:6379> set c c                  # 二进制  01100011    
OK    
127.0.0.1:6379> bitop and destkey a c    # 与操作  01100001 -> a    
(integer) 1    
127.0.0.1:6379> get destkey    "a"
127.0.0.1:6379set a a                 # 二进制  01100001    
OK    127.0.0.1:6379set b b           # 二进制  01100010    
OK    127.0.0.1:6379bitop or destkey a b    # 或操作  01100011 -c    
(integer) 1
127.0.0.1:6379get destkey    
"c"
127.0.0.1:6379set a a                 # 二进制  01100001    
OK    
127.0.0.1:6379set z Z                 # 二进制  01011010 (大写的Z)    
OK    
127.0.0.1:6379bitop xor destkey a z   # 异或    00111011 -> ; 分号    
(integer) 1    
127.0.0.1:6379get destkey
";"

实战前奏

如果对位运算不熟悉的同学,可以先复习一下。

链接放这里了:

理解有符号数和无符号数
位运算世界畅游指南

位图实战

 

目标

  1. 打卡
  2. 判断某天是否打卡
  3. 统计某月打卡总次数
  4. 获取某用户在某月的打卡信息
  5. 连续打卡的起止时间
  6. 最长连续天数
  7. 统计指定区间的打卡次数

总的调用

(async function () {
    const bitmap = new BitMap();
    // 初始化数据
    await bitmap.initData(['2020-10']);
    // 展示10月份全部签到数据
    await bitmap.getAllData(['2020-10']);

    const [uid1, uid2] = [1, 2]

    // 用户X 签到
    await bitmap.userSign(uid2, '2020-11-18');
    // 用户X在XX日期 是否签到
    await bitmap.judgeUserSign(uid2, '2020-11-18');
    // 用户X在XX月 总的签到次数
    await bitmap.getUserSignCount(uid2, '2020-10');
    // 用户X在XX月 第一次签到的日期
    await bitmap.getFirstSignDate(uid2, '2020-11');
    // 用户XX在XX月 签到的情况
    await bitmap.getSignInfo(uid2, '2020-10');
    // 某个区间内,连续签到的人数总和
    await bitmap.signAllWeek();

    await common.sleep(1);
    process.exit(1);
})()
66 94 10 31 57 26 76 53 91 27 37 32 49 17 6 44 25 33 13 16 77 90 46 80 85 64 51 42 9 54 47 38 36 14 96 97 71 3 12 63 41 35 55 39 58 99 89 81 45 69 4 84 22 60 65 5 72 29 78 21 23 95 7 59 11 40 68 79 62 34 98 82 18 83 56 67 20 93 24 70 52 28 92 87 1 8 61 75 19 15 2 100 74 88 43 0 50 48 73 30

返回值

  2位图 git:(main)  ts-node sign.ts

用户1在 2020-10 签到数据: 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
用户2在 2020-10 签到数据: 0,0,1,1,1,1,0,0,0,0,1,0,1,0,0,0,1,0,1,0,0,0,1,1,0,1,1,0,1,1,1
用户3在 2020-10 签到数据: 1,0,1,0,0,1,0,0,1,0,0,1,1,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,0,1
用户4在 2020-10 签到数据: 1,0,0,1,0,1,1,0,1,0,0,0,0,1,0,1,0,0,1,1,1,1,1,1,0,1,1,0,1,1,0
用户5在 2020-10 签到数据: 1,0,1,1,0,1,1,1,0,1,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,1,1,1,0,1
用户6在 2020-10 签到数据: 0,0,0,1,0,0,1,0,0,0,1,1,1,1,0,1,1,0,1,1,0,0,0,1,1,0,0,1,1,1,0
用户2在 2020-11-18签到为1
用户1在 2020-11 签到数据: 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
用户2在 2020-11 签到数据: 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0
用户3在 2020-11 签到数据: 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
用户4在 2020-11 签到数据: 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
用户5在 2020-11 签到数据: 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
用户6在 2020-11 签到数据: 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
用户2在 2020-11-18签到状态为1
用户2在 2020-10 月份 签到总次数为15
用户2在 2020-11 月份 首次签到 日期为2020-11-18
------用户2在2020-10签到情况-------
当月签到连续情况为:
 [ { signCount: 4, days: [ 3, 4, 5, 6 ] },
  { signCount: 3, days: [ 29, 30, 31 ] },
  { signCount: 2, days: [ 26, 27 ] },
  { signCount: 2, days: [ 23, 24 ] },
  { signCount: 1, days: [ 19 ] },
  { signCount: 1, days: [ 17 ] },
  { signCount: 1, days: [ 13 ] },
  { signCount: 1, days: [ 11 ] } ]
最长的连续签到次数:4
最长的连续签到次数日期为:3,4,5,6
----------本月某七天-----------
本月第4到10天 中所有的签到次数: 1

具体实现

实现部分涉及:

  1. 位操作
  2. 多种实现方法
/**
     * 获取某些月份总的签到数据
     * @param totalMonth 如: ['2020-10']
     */
    public async getAllData(totalMonth: string[]) {
        for (let uid = 1; uid <= this.allUser; uid++) {
            const total = [];
            // 获取上月份的起止时间
            for (const month of totalMonth) {
                // month对应的天数
                const { days } = UtilDate.daysInMonth(month);
                // allUser 用户ID作为key中的标示
                // 【偏移量+1】就是某月对应的几号
                let offset = 0;
                while (offset < days) {
                    const bit = await this.client.getbit(this.genKey({ date: month, uid }), offset);
                    total.push(bit);
                    offset++;
                }
                const result = `用户${uid}在 ${month}月 签到数据: ${total}`;
                console.log(result);
            }
        }
    }
    /**
     * 用户在某天 签到
     * @param uid 用户ID
     * @param date YYYY-MM—DD
     */
    public async userSign(uid: number, date: string) {
        const offset = UtilDate.dayOfNumInMonth(date);
        const status = SIGN.YES;
        await this.client.setbit(this.genKey({ date, uid }), offset - 1, status);
        console.log(`用户${uid}在 ${date}签到为${status}`);
    }
    /**
     * 判断用户在某天是否签到
     * @param uid 用户ID
     * @param date YYYY-MM—DD
     */
    public async judgeUserSign(uid: number, date: string) {
        const offset = UtilDate.dayOfNumInMonth(date);
        const status = await this.client.getbit(this.genKey({ date, uid }), offset - 1);
        await this.getAllData(['2020-11']);
        console.log(`用户${uid}在 ${date}签到状态为${status}`);
    }
    /**
     * 用户X在XX月总的签到次数
     * @param uid 用户ID
     * @param date YYYY-MM—DD
     */
    public async getUserSignCount(uid: number, date: string) {
        const count = await this.client.bitcount(this.genKey({ date, uid }));
        console.log(`用户${uid}在 ${date} 月份 签到总次数为${count}`);
    }
35 81 93 47 87 30 64 40 78 97 36 21 18 14 96 90 43 68 89 58 45 84 23 5 22 92 57 32 38 10 69 4 31 3 86 72 41 62 55 8 79 60 75 66 80 15 13 99 73 33 63 37 94 51 49 34 50 85 71 1 59 82 12 27 53 48 65 39 16 100 24 17 42 61 20 83 70 7 74 29 54 44 56 88 98 0 2 9 26 76 91 11 77 28 25 52 6 95 19 67
/**
     * 获取当月签到情况
     * 1. 当月最长的签到天数
     * 2. 
     * @param uid 
     * @param date 
     */
    public async getSignInfo(uid: number, date: string) {
        const { days, dayList } = UtilDate.daysInMonth(date);
        const key = this.genKey({ date, uid });
        // days 该月总天数
        const bitValue = await this.genBitIntervalValue({ key, start: 0, length: days });
        if (bitValue === -1) {
            console.log('相关信息不存在')
            return
        }

        let signCount = 0;
        const signInfo = [];
        let signValue = bitValue;
        // 从后往前算
        for (let index = dayList.length; index > 0; index--) {
            // 位运算
            // 先左后右,如果和原数据相等,则标示最低位是0,即,没有签到
            // 从该月最后一天往前算。
            if (signValue >> 1 << 1 === signValue) {
                if (signCount > 0) {
                    // 记录连续的长度&位置
                    signInfo.push({ signCount, index });
                    // 重置连续次数
                    signCount = 0;
                }
            } else {
                signCount++;
            }
            signValue >>= 1;
        }

        // 记录最后的一次连续【高位】
        if (signCount > 0) {
            signInfo.push({ signCount, index: 0 });
        }

        // 统计连续的天数、连续的日期
        const result = [];


        for (const item of signInfo) {
            const { signCount, index } = item;
            const days = [];
            let i = 1;
            let _index = index + 1;
            while (i <= signCount) {
                days.push(_index++);
                i++;
            }
            const arg = {
                signCount,
                days,
            }
            result.push(arg);
        }
        // 排序函数 逆序排列
        const compare = (p: any) => (m: any, n: any) => -(m[p] - n[p]);
        result.sort(compare('signCount'));
        console.log(`------用户${uid}在${date}签到情况-------`)
        console.log("当月签到连续情况为:", '\n', result);
        console.log(`最长的连续签到次数:${result[0].signCount}`);
        console.log(`最长的连续签到次数日期为:${result[0].days}`);
    }
15 52 25 34 1 16 36 48 38 35 12 20 86 13 94 91 74 23 59 17 57 83 90 11 10 28 69 56 93 55 50 26 44 33 54 97 51 80 95 14 30 2 70 63 79 6 60 67 39 78 19 66 73 99 3 84 41 65 92 18 24 61 46 75 47 88 40 64 37 32 49 62 22 68 42 89 21 85 100 87 8 98 7 31 71 29 58 53 9 4 96 81 43 27 5 77 0 45 72 82

具体的实例代码:https://github.com/simuty/Integration/blob/main/Redis/


全部评论: 0

    我有话说:

    Redis系列四 锁

      本文目标 1. 熟悉乐观锁ABA概念 2. 理解掌握redis事务以及watch回滚; 3. 实战redis锁 乐观锁 乐观锁是一种不会阻塞其他线程并发的机制,它不会使用数据库的

    Redis系列六 Lua

      本文目标 学习lua基本语法 能够采用redis+lua lua 基本语法 Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用

    Redis系列七 Debug Lua

      调试redis+lua 学了lua的基本语法,了解了redis+lua的配套用法,但是却不知道怎么断点调试。学就学全面点, 官网中有dubug相关说明。地址:Redis Lua

    Redis系列四 GEO附近的人

    GEO算法 GeoHash是一种地址编码方法。将维的空间经纬度数据编码成一个字符串; 地球上的经度范围:[-180, 180],纬度范围:[-90,90]。如果以本初子午线、赤道为界,地球可以

    Redis系列一 基本用法&应用场景

        说明 redis的最基本使用方法以及使用场景。 字符串 // stringasync function stringFun() { const [key

    Redis系列八 抢红包

      本文概述 掌握红包的两种常见生成算法 掌握lua+redis 实现原子性抢红包 项目中还有mysql相关内容 了解jmeter的基本用法 遗留问题 redis同步DB时机问题

    Redis 5.0.11、6.0.11、6.2 发布,修复 32 系统上的整数溢出

    Redis 同时发布了 5.0.11、6.0.11 和 6.2 版本。对于使用 32 Redis 的用户来说,此次更新解决了一个重要的安全问题,即 32 系统上的整数溢出((CVE-2021

    Redis系列九 推荐系统-布隆过滤器

      布隆过滤器 概念 布隆过滤器是一种空间利用率较高的概率型数据结构,用来测试一个元素是否在集合中。但是存在一定可能,导致结果误判。当布隆过滤器说某个值存在时,这个值可能不存在;当它说不存在时,那就肯定不存在. 特性: 与哈希表不同,...

    为什么单线程的Redis能够达到百万级的QPS?

    作者:在江湖中coding链接:https://juejin.im/post/5e6097846fb9a07c9f3fe744 性能测试报告 查看了下阿里云 Redis 的性能测试报告如下,能够

    Kafka系列

    目的是:提供负载均衡,实现系统的高伸缩性【Sca...

    iOS实战篇:iOS 界面顿原因

    界面顿的原因在 VSync[1] 信号到来后,系统图形服务会通过 CADisplayLink 等机制通知 App,App 主线程开始在 CPU 中计算显示内容......

    BAT大牛Redis客户端与服务端交互原理

    Redis实例运行在单独的进程中,应用系统Redis客户端)通过Redis协议和Redis Server

    「免费」千万级电商高并发与秒杀实战

    IT老齐 16年Java研发与架构设计经验、前京东金融架构师、中国财政部数据平台架构师、专注送给小白的实战课、只为高薪而生、重,说人话,讲干货,不扯淡!

    Redisson 3.13.6 发布,官方推荐的 Redis 客户端

    Redisson 3.13.6 已发布,这是一个 Java 编写的 Redis 客户端,具备驻内存数据网格(In-Memory Data Grid)功能,并获得了 Redis 的官方推荐

    Dgraph 1.2.8 发布,事务性分布式图形数据库

    Dgraph 1.2.8 发布了。Dgraph 是一个可扩展的,分布式的,低延迟的数据库,目标是提供 Google 生产水平的规模和吞吐量,在超过 TB 的结构数据里,为用户提供足够低延迟的实时

    抖音品质建设 - iOS启动优化《实战篇》

    ,本文是实战篇。 原理篇:抖音品质建设-iO...

    「转载」Redis 到底是怎么实现“附近的人”这个功能的?

    Redis,结合其有序队列zset以及geohash编码,实现了空间搜索功能,且拥有极高的运行效率。

    商城系统 DBShop V3.0 Beta 发布

    全新重构,首次亮相。 系统简介 DBShop企业级商城系统,使用PHP语言基于Laminas(Zendframework 3) + Doctrine 2 组合框架开发完成。可定制、多终端、多场景、多