Redis中BitMap技術(shù)簡(jiǎn)介及應(yīng)用

Redis中BitMap技術(shù)簡(jiǎn)介及應(yīng)用

BitMap簡(jiǎn)介

BitMap是一串連續(xù)的二進(jìn)制數(shù)字(0和1),類似于位數(shù)組,每一位所在的位置為偏移量(offset),類似于數(shù)組索引,BitMap就是通過最小的單位bit來進(jìn)行0|1的設(shè)置,時(shí)間復(fù)雜度位O(1),表示某個(gè)元素的值或者狀態(tài)。由于bit是計(jì)算機(jī)中最小的單位,使用它進(jìn)行儲(chǔ)存將非常節(jié)省空間。特別適合一些數(shù)據(jù)量大的場(chǎng)景。例如,統(tǒng)計(jì)每日活躍用戶、統(tǒng)計(jì)每月打卡數(shù)等統(tǒng)計(jì)場(chǎng)景。1天記錄1000W用戶的活躍統(tǒng)計(jì)數(shù)據(jù),只需要10000000/8/1024/1024 ≈1.2M。

Redis中的BitMap

Redis從2.2.0版本開始新增了setbit,getbit,bitcount,bitop等幾個(gè)BitMap相關(guān)命令,雖然是新命令,但是并沒有增加新的數(shù)據(jù)類型,它還是屬于String類型。Redis中的BitMap最大占用內(nèi)存大小限制在512M之內(nèi),即2^32。

相關(guān)命令操作

setbit

設(shè)置某個(gè)key的指定偏移量的value值為0或者1,key不存在時(shí)自動(dòng)生成一個(gè)新的字符串值,字符串會(huì)進(jìn)行伸展,該偏移量前面的位值默認(rèn)為0,偏移量offset參數(shù)必須大于等于0,小于2^32。

時(shí)間復(fù)雜度:O(1)

返回值:指定偏移量存儲(chǔ)的值

示例:

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指定偏移量上的值,當(dāng)key不存在時(shí),返回0。

時(shí)間復(fù)雜度:O(1)

返回值:指定偏移量上存儲(chǔ)的值

示例:

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

統(tǒng)計(jì)給定key中,被設(shè)置為1的比特位的數(shù)量,可以通過start和end參數(shù)設(shè)置范圍。

注意!,setbit和getbit是對(duì)bit位進(jìn)行操作,bitcount的參數(shù)start和end是對(duì)字節(jié)byte計(jì)數(shù),1 byte = 8bit。

時(shí)間復(fù)雜度:O(n)

返回值:key中被設(shè)置為1的數(shù)量

示例:

127.0.0.1:6379[3]> bitcount month     // 空的key,位為1的數(shù)量為0
(integer) 0
127.0.0.1:6379[3]> setbit month 4 1  
(integer) 0
127.0.0.1:6379[3]> bitcount month     // 默認(rèn)統(tǒng)計(jì)整個(gè)key位為1的數(shù)量
(integer) 1
127.0.0.1:6379[3]> bitcount month 0 0   // 查詢month中第一個(gè)字節(jié)位為1的數(shù)量,即0 1 2 3 4 5 6 7位。
(integer) 1
127.0.0.1:6379[3]> bitcount month 1 1   // 查詢第二個(gè)字節(jié)
(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]是一個(gè)閉區(qū)間,所以這里查詢的是第1、2個(gè)字節(jié)
(integer) 2

bitop

對(duì)一個(gè)或多個(gè)key進(jìn)行位操作,并將結(jié)果保存到destkey上。操作方式可以是AND、OR、NOT、XOR這四種,除了NOT操作之外,其他操作可接收多個(gè)key。

處理不同長(zhǎng)度的字符串時(shí),較短的那個(gè)字符串所缺少的部分會(huì)被看作0,空的key也被看做全是0的字符串序列。

時(shí)間復(fù)雜度:O(n)

返回值:保存到destkey的字符串的長(zhǎng)度

示例:

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    // 對(duì)month1和month2做或運(yùn)算,結(jié)果:00011000
(integer) 1
127.0.0.1:6379[3]> bitcount month         // month中位為1的數(shù)量就為2
(integer) 2

WEB常見應(yīng)用

用戶行為統(tǒng)計(jì)

  • 是否點(diǎn)擊過某個(gè)按鈕
  • 是否領(lǐng)取過優(yōu)惠券
  • 點(diǎn)贊、喜歡等
import * as Redis from "ioredis";
const redis = new Redis({});
// 記錄用戶行為,是否領(lǐng)取過優(yōu)惠券
const key = "got_coupon";
const uid = 100;
redis.setbit(key, uid, 1)
// 查詢用戶是否領(lǐng)取過
const is_got = redis.getbit(key, uid)
// 統(tǒng)計(jì)優(yōu)惠券已發(fā)放數(shù)量
const sended_count = redis.bigcount(key)

活躍用戶統(tǒng)計(jì)

import * as Redis from "ioredis";
const redis = new Redis({});

// 用戶(uid:100)登錄計(jì)數(shù)
const uid = 100;
const key = "userLogin:2019-08-01";
redis.setbit(key, uid, 1);

// 計(jì)算今天活躍用戶數(shù)
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");

// 統(tǒng)計(jì)最近三天用戶活躍數(shù)
const key3 = "userLogin:2019-08-03";
redis.bitop("OR", "three_day_active", key, key2, key3);
const threedays_nums = redis.bitcount("three_day_active");

用戶簽到

簽到需求:

  1. 用戶使用簽到功能,用戶的簽到狀態(tài)
  2. 用戶的周、月簽到記錄、次數(shù)
  3. 當(dāng)天有多少用戶簽到
import redis
from datetime import date, timedelta
import calendar

# redis 連接
r = redis.Redis(
    host="192.168.0.200",
    port=6379,
    db=3
)

# 檢查參數(shù)裝飾器
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:
        # 用戶今日簽到狀態(tài)
        return r.getbit(self._get_key(date.today()), user_id)

    @check_input
    def week_sign_status(self, user_id: int) -> list:
        # 求出這個(gè)周的簽到狀況
        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]
        # 比如周三的時(shí)候我們只查3次getbit,然后剩下補(bǔ)0
        data.extend([0] * (7 - len(data)))
        return data

    @check_input
    def month_sing_status(self, user_id: int) -> list:
        # 求出這個(gè)月的某個(gè)用戶簽到狀況
        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]
        # 獲取當(dāng)月天數(shù),還沒到的天數(shù)補(bǔ)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:
        # 求出這個(gè)周的簽到次數(shù)
        return sum(self.week_sign_status(user_id))

    @check_input
    def month_sign_num(self, user_id: int) -> int:
        # 求出這個(gè)月的簽到次數(shù)
        return sum(self.month_sing_status(user_id))

    @check_input
    def today_sign_all_num(self) -> int:
        # 求出當(dāng)天有多少用戶簽到
        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

之前的應(yīng)用都是統(tǒng)計(jì)總數(shù),但如果業(yè)務(wù)需要,有時(shí)也可能需要獲取用戶ID,來做下一步操作。

// 獲取活躍用戶的id,可進(jìn)行下一步操作,比如發(fā)送優(yōu)惠信息
import redis
import time

r = redis.Redis(host="192.168.0.200", port=6379, db=3)
# byte字節(jié)
tmp = r.get("login")
# bit位
total_bits = tmp * 8
start = time.time()
for i in range(len(total_bits)):
    # 所屬字節(jié)
    offset_arr = i // 8
    # 偏移量
    offset_bit = i % 8
    # 與128(10000000)進(jìn)行與運(yùn)算,bit存在,則表示該位為1,此時(shí)i就是用戶id
    bit = (tmp[offset_arr] << offset_bit) & 0b10000000
    if bit:
        print(f'user {i} is set')
# 統(tǒng)計(jì)時(shí)間,1000W數(shù)據(jù),只需要4s;
print(f'end: {time.time() - start}')
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容