美文网首页
Redis中BitMap技术简介及应用

Redis中BitMap技术简介及应用

作者: 747大雄 | 来源:发表于2019-10-09 13:50 被阅读0次

    Redis中BitMap技术简介及应用

    BitMap简介

    BitMap是一串连续的二进制数字(0和1),类似于位数组,每一位所在的位置为偏移量(offset),类似于数组索引,BitMap就是通过最小的单位bit来进行0|1的设置,时间复杂度位O(1),表示某个元素的值或者状态。由于bit是计算机中最小的单位,使用它进行储存将非常节省空间。特别适合一些数据量大的场景。例如,统计每日活跃用户、统计每月打卡数等统计场景。1天记录1000W用户的活跃统计数据,只需要10000000/8/1024/1024 ≈1.2M。

    Redis中的BitMap

    Redis从2.2.0版本开始新增了setbit,getbit,bitcount,bitop等几个BitMap相关命令,虽然是新命令,但是并没有增加新的数据类型,它还是属于String类型。Redis中的BitMap最大占用内存大小限制在512M之内,即2^32。

    相关命令操作

    setbit

    设置某个key的指定偏移量的value值为0或者1,key不存在时自动生成一个新的字符串值,字符串会进行伸展,该偏移量前面的位值默认为0,偏移量offset参数必须大于等于0,小于2^32。

    时间复杂度:O(1)

    返回值:指定偏移量存储的值

    示例:

    127.0.0.1:6379[3]> setbit login 2 1
    (integer) 0
    127.0.0.1:6379[3]> setbit login 2 1
    (integer) 1
    127.0.0.1:6379[3]> getbit login 2
    (integer) 1
    127.0.0.1:6379[3]> getbit login 1
    (integer) 0
    

    getbit

    获取key指定偏移量上的值,当key不存在时,返回0。

    时间复杂度:O(1)

    返回值:指定偏移量上存储的值

    示例:

    127.0.0.1:6379[3]> exists order
    (integer) 0
    127.0.0.1:6379[3]> getbit order 10
    (integer) 0
    127.0.0.1:6379[3]> setbit order 10 1
    (integer) 0
    127.0.0.1:6379[3]> getbit order 10
    (integer) 1
    127.0.0.1:6379[3]>
    

    bitcount

    统计给定key中,被设置为1的比特位的数量,可以通过start和end参数设置范围。

    注意!,setbit和getbit是对bit位进行操作,bitcount的参数start和end是对字节byte计数,1 byte = 8bit。

    时间复杂度:O(n)

    返回值:key中被设置为1的数量

    示例:

    127.0.0.1:6379[3]> bitcount month     // 空的key,位为1的数量为0
    (integer) 0
    127.0.0.1:6379[3]> setbit month 4 1  
    (integer) 0
    127.0.0.1:6379[3]> bitcount month     // 默认统计整个key位为1的数量
    (integer) 1
    127.0.0.1:6379[3]> bitcount month 0 0   // 查询month中第一个字节位为1的数量,即0 1 2 3 4 5 6 7位。
    (integer) 1
    127.0.0.1:6379[3]> bitcount month 1 1   // 查询第二个字节
    (integer) 0
    127.0.0.1:6379[3]> setbit month 8 1
    (integer) 0
    127.0.0.1:6379[3]> bitcount month 0 1    // [start, end]是一个闭区间,所以这里查询的是第1、2个字节
    (integer) 2
    

    bitop

    对一个或多个key进行位操作,并将结果保存到destkey上。操作方式可以是AND、OR、NOT、XOR这四种,除了NOT操作之外,其他操作可接收多个key。

    处理不同长度的字符串时,较短的那个字符串所缺少的部分会被看作0,空的key也被看做全是0的字符串序列。

    时间复杂度:O(n)

    返回值:保存到destkey的字符串的长度

    示例:

    127.0.0.1:6379[3]> setbit month1 3 1   // month1:00010000
    (integer) 0
    127.0.0.1:6379[3]> setbit month2 4 1   // month2:00001000
    (integer) 0
    127.0.0.1:6379[3]> bitop OR month month1 month2    // 对month1和month2做或运算,结果:00011000
    (integer) 1
    127.0.0.1:6379[3]> bitcount month         // month中位为1的数量就为2
    (integer) 2
    

    WEB常见应用

    用户行为统计

    • 是否点击过某个按钮
    • 是否领取过优惠券
    • 点赞、喜欢等
    import * as Redis from "ioredis";
    const redis = new Redis({});
    // 记录用户行为,是否领取过优惠券
    const key = "got_coupon";
    const uid = 100;
    redis.setbit(key, uid, 1)
    // 查询用户是否领取过
    const is_got = redis.getbit(key, uid)
    // 统计优惠券已发放数量
    const sended_count = redis.bigcount(key)
    

    活跃用户统计

    import * as Redis from "ioredis";
    const redis = new Redis({});
    
    // 用户(uid:100)登录计数
    const uid = 100;
    const key = "userLogin:2019-08-01";
    redis.setbit(key, uid, 1);
    
    // 计算今天活跃用户数
    const active_nums = redis.bitcount(key);
    
    // 昨天今天均活跃的用户
    const key2 = "userLogin:2019-08-02";
    redis.bitop("AND", "both_active", key, key2);
    const both_nums = redis.bitcount("both_active");
    
    // 统计最近三天用户活跃数
    const key3 = "userLogin:2019-08-03";
    redis.bitop("OR", "three_day_active", key, key2, key3);
    const threedays_nums = redis.bitcount("three_day_active");
    

    用户签到

    签到需求:

    1. 用户使用签到功能,用户的签到状态
    2. 用户的周、月签到记录、次数
    3. 当天有多少用户签到
    import redis
    from datetime import date, timedelta
    import calendar
    
    # redis 连接
    r = redis.Redis(
        host="192.168.0.200",
        port=6379,
        db=3
    )
    
    # 检查参数装饰器
    def check_input(func):
        def wrapper(*args, **kwargs):
            if not isinstance(args[1], int):
                raise ValueError(f"User_id must be int, and your input is {type(args[1])}")
            return func(*args, **kwargs)
    
        return wrapper
    
    class RedisCheckIn:
        _private_key = "_check_in_"
    
        def __init__(self):
            pass
    
        @check_input
        def sign(self, user_id: int) -> int:
            # 用户签到
            return r.setbit(self._get_key(date.today()), user_id, 1)
    
        @check_input
        def sign_status(self, user_id: int) -> int:
            # 用户今日签到状态
            return r.getbit(self._get_key(date.today()), user_id)
    
        @check_input
        def week_sign_status(self, user_id: int) -> list:
            # 求出这个周的签到状况
            now = date.today()  # 2020-06-05
            # 周一是1 周日是7
            weekday = now.isoweekday()  # 5
            # 使用管道批量化操作
            with r.pipeline(transaction=False) as p:
                for d in range(weekday):
                    check_day = now - timedelta(days=d)
                    p.getbit(self._get_key(check_day), user_id)
                # 倒序,之前是倒着查询的
                data = p.execute()[::-1]
            # 比如周三的时候我们只查3次getbit,然后剩下补0
            data.extend([0] * (7 - len(data)))
            return data
    
        @check_input
        def month_sing_status(self, user_id: int) -> list:
            # 求出这个月的某个用户签到状况
            now = date.today()
            day = now.day
            with r.pipeline(transaction=False) as p:
                for d in range(day):
                    check_day = now - timedelta(days=d)
                    p.getbit(self._get_key(check_day), user_id)
                data = p.execute()[::-1]
            # 获取当月天数,还没到的天数补0
            month_range = calendar.monthrange(now.year, now.month)
            data.extend([0] * (month_range[1] - len(data)))
            return data
    
        @check_input
        def week_sign_num(self, user_id: int) -> int:
            # 求出这个周的签到次数
            return sum(self.week_sign_status(user_id))
    
        @check_input
        def month_sign_num(self, user_id: int) -> int:
            # 求出这个月的签到次数
            return sum(self.month_sing_status(user_id))
    
        @check_input
        def today_sign_all_num(self) -> int:
            # 求出当天有多少用户签到
            return r.bitcount(self._get_key(date.today()))
    
        @staticmethod
        def _get_key(check_date):
            return f"check_in_{check_date}"
    
    if __name__ == '__main__':
        redis_sign_in = RedisCheckIn()
        redis_sign_in.sign(100)  # 签到
        print(redis_sign_in.sign_status(100))   # 1表示已签到
        print(redis_sign_in.sign_status(101))   # 0表示未签到
        print(redis_sign_in.week_sign_status(100))   # userId为100的用户这周签到情况:[0, 0, 0, 0, 1, 0, 0]
        print(redis_sign_in.week_sign_num(100))   # 这周总共签到1次
    

    获取用户ID

    之前的应用都是统计总数,但如果业务需要,有时也可能需要获取用户ID,来做下一步操作。

    // 获取活跃用户的id,可进行下一步操作,比如发送优惠信息
    import redis
    import time
    
    r = redis.Redis(host="192.168.0.200", port=6379, db=3)
    # byte字节
    tmp = r.get("login")
    # bit位
    total_bits = tmp * 8
    start = time.time()
    for i in range(len(total_bits)):
        # 所属字节
        offset_arr = i // 8
        # 偏移量
        offset_bit = i % 8
        # 与128(10000000)进行与运算,bit存在,则表示该位为1,此时i就是用户id
        bit = (tmp[offset_arr] << offset_bit) & 0b10000000
        if bit:
            print(f'user {i} is set')
    # 统计时间,1000W数据,只需要4s;
    print(f'end: {time.time() - start}')
    

    相关文章

      网友评论

          本文标题:Redis中BitMap技术简介及应用

          本文链接:https://www.haomeiwen.com/subject/ljfgpctx.html