Redis中BitMap技术简介及应用

2019-10-09  本文已影响0人  747大雄

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}')
上一篇下一篇

猜你喜欢

热点阅读