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");
用户签到
签到需求:
- 用户使用签到功能,用户的签到状态
- 用户的周、月签到记录、次数
- 当天有多少用户签到
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}')