Redis中BitMap技術簡介及應用
BitMap簡介
BitMap是一串連續(xù)的二進制數(shù)字(0和1),類似于位數(shù)組,每一位所在的位置為偏移量(offset),類似于數(shù)組索引忧吟,BitMap就是通過最小的單位bit來進行0|1的設置,時間復雜度位O(1)斩披,表示某個元素的值或者狀態(tài)溜族。由于bit是計算機中最小的單位,使用它進行儲存將非常節(jié)省空間垦沉。特別適合一些數(shù)據(jù)量大的場景煌抒。例如,統(tǒng)計每日活躍用戶厕倍、統(tǒng)計每月打卡數(shù)等統(tǒng)計場景寡壮。1天記錄1000W用戶的活躍統(tǒng)計數(shù)據(jù),只需要10000000/8/1024/1024 ≈1.2M讹弯。
Redis中的BitMap
Redis從2.2.0版本開始新增了setbit况既,getbit,bitcount组民,bitop等幾個BitMap相關命令棒仍,雖然是新命令,但是并沒有增加新的數(shù)據(jù)類型臭胜,它還是屬于String類型莫其。Redis中的BitMap最大占用內存大小限制在512M之內,即2^32庇楞。
相關命令操作
setbit
設置某個key的指定偏移量的value值為0或者1榜配,key不存在時自動生成一個新的字符串值,字符串會進行伸展吕晌,該偏移量前面的位值默認為0蛋褥,偏移量offset參數(shù)必須大于等于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
統(tǒng)計給定key中淫茵,被設置為1的比特位的數(shù)量,可以通過start和end參數(shù)設置范圍蹬跃。
注意匙瘪!,setbit和getbit是對bit位進行操作,bitcount的參數(shù)start和end是對字節(jié)byte計數(shù)丹喻,1 byte = 8bit薄货。
時間復雜度:O(n)
返回值:key中被設置為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 // 默認統(tǒng)計整個key位為1的數(shù)量
(integer) 1
127.0.0.1:6379[3]> bitcount month 0 0 // 查詢month中第一個字節(jié)位為1的數(shù)量碍论,即0 1 2 3 4 5 6 7位谅猾。
(integer) 1
127.0.0.1:6379[3]> bitcount month 1 1 // 查詢第二個字節(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]是一個閉區(qū)間,所以這里查詢的是第1鳍悠、2個字節(jié)
(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的數(shù)量就為2
(integer) 2
WEB常見應用
用戶行為統(tǒng)計
- 是否點擊過某個按鈕
- 是否領取過優(yōu)惠券
- 點贊瞎抛、喜歡等
import * as Redis from "ioredis";
const redis = new Redis({});
// 記錄用戶行為,是否領取過優(yōu)惠券
const key = "got_coupon";
const uid = 100;
redis.setbit(key, uid, 1)
// 查詢用戶是否領取過
const is_got = redis.getbit(key, uid)
// 統(tǒng)計優(yōu)惠券已發(fā)放數(shù)量
const sended_count = redis.bigcount(key)
活躍用戶統(tǒng)計
import * as Redis from "ioredis";
const redis = new Redis({});
// 用戶(uid:100)登錄計數(shù)
const uid = 100;
const key = "userLogin:2019-08-01";
redis.setbit(key, uid, 1);
// 計算今天活躍用戶數(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)計最近三天用戶活躍數(shù)
const key3 = "userLogin:2019-08-03";
redis.bitop("OR", "three_day_active", key, key2, key3);
const threedays_nums = redis.bitcount("three_day_active");
用戶簽到
簽到需求:
- 用戶使用簽到功能却紧,用戶的簽到狀態(tài)
- 用戶的周桐臊、月簽到記錄、次數(shù)
- 當天有多少用戶簽到
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:
# 求出這個周的簽到狀況
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]
# 獲取當月天數(shù),還沒到的天數(shù)補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:
# 求出這個周的簽到次數(shù)
return sum(self.week_sign_status(user_id))
@check_input
def month_sign_num(self, user_id: int) -> int:
# 求出這個月的簽到次數(shù)
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
之前的應用都是統(tǒng)計總數(shù)巫俺,但如果業(yè)務需要认烁,有時也可能需要獲取用戶ID,來做下一步操作介汹。
// 獲取活躍用戶的id却嗡,可進行下一步操作,比如發(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)進行與運算嘹承,bit存在窗价,則表示該位為1,此時i就是用戶id
bit = (tmp[offset_arr] << offset_bit) & 0b10000000
if bit:
print(f'user {i} is set')
# 統(tǒng)計時間叹卷,1000W數(shù)據(jù)撼港,只需要4s坪它;
print(f'end: {time.time() - start}')