Redis `Bitmaps` 位图:用户在线状态与活跃度统计

各位观众,各位朋友,程序员们,大家好!今天我们来聊聊 Redis 的 Bitmaps,这玩意儿听起来好像很高大上,其实用起来相当接地气。咱们今天就把它扒个精光,看看它怎么帮我们搞定用户在线状态和活跃度统计这种常见需求。

开场白:为啥要用 Bitmaps?

在互联网的世界里,用户就是上帝。但是上帝太多了,动不动就几百万、几千万,甚至上亿。要实时追踪每个用户的在线状态,或者统计他们的活跃度,那可不是件容易事。

最简单粗暴的方法,莫过于给每个用户建个 boolean 类型的字段,比如 is_online。用户在线就设为 true,离线就设为 false。看起来挺合理,但问题是:数量级一大,内存消耗就爆炸了!

假设我们有 1 亿用户,每个 boolean 类型占 1 个字节,那光是存储在线状态就要 100MB。这还没算上其他数据呢!而且,要统计有多少用户在线,还得遍历一遍所有用户,效率低到令人发指。

这时候,Bitmaps 就该闪亮登场了。Bitmaps 是一种基于位的存储结构,说白了,就是把每个用户的状态映射到一个 bit 上。一个 bit 只有两种状态:0 和 1,分别表示用户离线和在线。

这样做的好处是:极大地节省内存!还是 1 亿用户,用 Bitmaps 只需要 100000000 / 8 = 12.5MB。足足省了 8 倍!而且,Redis 提供了丰富的位操作命令,可以高效地进行各种统计。

Bitmaps 的基本原理:拨开云雾见青天

Bitmaps 的本质就是一个 bit 数组,每个 bit 都有一个对应的索引。在 Redis 中,这个索引就是用户的 ID。

假设我们有 10 个用户,ID 分别是 1 到 10。我们可以用一个 10 bits 的 Bitmap 来表示他们的在线状态。

用户 ID 1 2 3 4 5 6 7 8 9 10
Bitmap 0 1 0 0 1 1 0 1 0 0

在这个例子中,用户 2、5、6 和 8 在线,其他用户离线。

Redis Bitmaps 的常用命令:十八般武艺样样精通

Redis 提供了几个专门用于操作 Bitmaps 的命令:

  • SETBIT key offset value: 设置指定 key 中,偏移量为 offset 的 bit 的值为 value (0 或 1)。
  • GETBIT key offset: 获取指定 key 中,偏移量为 offset 的 bit 的值。
  • BITCOUNT key [start end]: 统计指定 key 中,值为 1 的 bit 的数量。可以指定 startend 参数来限制统计范围。
  • BITOP operation destkey key [key ...]: 对一个或多个 key 执行位运算,并将结果存储到 destkey 中。operation 可以是 ANDORXORNOT
  • BITPOS key value [start [end]]: 查找指定 key 中,第一个值为 value 的 bit 的位置。可以指定 startend 参数来限制查找范围。
  • BITFIELD key [GET type offset] [SET type offset value] [INCRBY type offset increment] [OVERFLOW WRAP|SAT|FAIL]: 更复杂的位域操作,可以对指定范围的 bit 进行读取、设置和自增操作。

实战演练:用户在线状态管理

假设我们要用 Redis Bitmaps 来管理用户的在线状态。我们可以这样做:

  1. 设置用户上线:
SETBIT online_status 12345 1  # 用户 ID 12345 上线
  1. 设置用户下线:
SETBIT online_status 12345 0  # 用户 ID 12345 下线
  1. 检查用户是否在线:
GETBIT online_status 12345  # 返回 1 表示在线,0 表示离线
  1. 统计在线用户数量:
BITCOUNT online_status  # 返回在线用户总数

代码示例 (Python):

import redis

# 连接 Redis
r = redis.Redis(host='localhost', port=6379, db=0)

def set_user_online(user_id):
  """设置用户上线"""
  r.setbit('online_status', user_id, 1)

def set_user_offline(user_id):
  """设置用户下线"""
  r.setbit('online_status', user_id, 0)

def is_user_online(user_id):
  """检查用户是否在线"""
  return r.getbit('online_status', user_id) == 1

def get_online_user_count():
  """获取在线用户数量"""
  return r.bitcount('online_status')

# 示例
user_id = 12345
set_user_online(user_id)
print(f"用户 {user_id} 是否在线: {is_user_online(user_id)}")  # 输出: True
print(f"在线用户数量: {get_online_user_count()}")

set_user_offline(user_id)
print(f"用户 {user_id} 是否在线: {is_user_online(user_id)}")  # 输出: False
print(f"在线用户数量: {get_online_user_count()}")

实战演练:用户活跃度统计

除了在线状态,Bitmaps 还可以用来统计用户的活跃度。比如,我们可以统计每天活跃的用户数量。

  1. 每天创建一个 Bitmap: 每个 key 代表一天,比如 active_users:2023-10-27

  2. 用户每天第一次访问时,设置对应的 bit 为 1:

SETBIT active_users:2023-10-27 12345 1  # 用户 ID 12345 今天活跃
  1. 统计每天的活跃用户数量:
BITCOUNT active_users:2023-10-27  # 返回今天的活跃用户数量
  1. 统计某段时间内的活跃用户数量: 可以使用 BITOP 命令进行位运算。比如,要统计 2023-10-26 和 2023-10-27 两天都活跃的用户数量:
BITOP AND active_users:2023-10-26_27 active_users:2023-10-26 active_users:2023-10-27
BITCOUNT active_users:2023-10-26_27  # 返回两天都活跃的用户数量

代码示例 (Python):

import redis
import datetime

# 连接 Redis
r = redis.Redis(host='localhost', port=6379, db=0)

def mark_user_active(user_id, date=None):
  """标记用户在指定日期活跃"""
  if date is None:
    date = datetime.date.today().strftime('%Y-%m-%d')
  key = f'active_users:{date}'
  r.setbit(key, user_id, 1)

def get_daily_active_user_count(date):
  """获取指定日期的活跃用户数量"""
  key = f'active_users:{date}'
  return r.bitcount(key)

def get_period_active_user_count(start_date, end_date):
  """获取指定时间段内的活跃用户数量 (至少有一天活跃就算活跃)"""
  keys = [f'active_users:{date.strftime("%Y-%m-%d")}' for date in (start_date + datetime.timedelta(n) for n in range((end_date - start_date).days + 1))]
  dest_key = 'period_active_users'
  r.bitop('OR', dest_key, *keys)
  return r.bitcount(dest_key)

def get_period_all_active_user_count(start_date, end_date):
    """ 获取指定时间段内每天都活跃的用户数量 """
    keys = [f'active_users:{date.strftime("%Y-%m-%d")}' for date in (start_date + datetime.timedelta(n) for n in range((end_date - start_date).days + 1))]
    dest_key = 'period_all_active_users'
    r.bitop('AND', dest_key, *keys)
    return r.bitcount(dest_key)
# 示例
user_id = 12345
today = datetime.date.today()
yesterday = today - datetime.timedelta(days=1)

mark_user_active(user_id, today.strftime('%Y-%m-%d'))
print(f"今天活跃用户数量: {get_daily_active_user_count(today.strftime('%Y-%m-%d'))}")
print(f"昨天活跃用户数量: {get_daily_active_user_count(yesterday.strftime('%Y-%m-%d'))}")  # 假设昨天没人活跃,输出 0

print(f"从 {yesterday} 到 {today} 活跃用户数量: {get_period_active_user_count(yesterday, today)}")

print(f"从 {yesterday} 到 {today} 每天都活跃的用户数量: {get_period_all_active_user_count(yesterday, today)}")

高级用法:结合 Bloom Filter

如果用户 ID 非常大,而且比较分散,直接用用户 ID 作为 Bitmap 的 offset 可能会导致 Bitmap 非常稀疏,浪费空间。 这时候,可以考虑结合 Bloom Filter。

Bloom Filter 是一种概率型数据结构,用于判断一个元素是否存在于一个集合中。它的特点是:

  • 如果 Bloom Filter 说某个元素不存在,那它肯定不存在。
  • 如果 Bloom Filter 说某个元素存在,那它可能存在,也可能不存在(存在一定的误判率)。

我们可以先用 Bloom Filter 判断用户 ID 是否可能存在,如果 Bloom Filter 说不存在,那我们就不用去操作 Bitmap 了。这样可以避免操作一些无效的 offset,节省空间。

需要注意的点:魔鬼都在细节里

  • Key 的设计: Bitmap 的 key 要设计得合理,方便管理和查询。比如,可以按照日期、业务类型等进行划分。
  • Offset 的选择: Offset 的选择直接影响 Bitmap 的大小。要尽量选择连续的、较小的 ID 作为 offset。如果用户 ID 很大,可以考虑使用 Hash 函数进行映射。
  • 内存占用: 虽然 Bitmaps 可以节省内存,但也要注意控制 Bitmap 的大小,避免占用过多内存。
  • 并发问题: 在并发环境下,要考虑 SETBIT 操作的原子性。Redis 是单线程的,所以 SETBIT 操作本身是原子性的。但是,如果涉及到多个操作,比如先 GETBIT 再 SETBIT,就需要使用 Lua 脚本或者事务来保证原子性。

总结:Bitmaps 的优势与局限

优势:

  • 极高的存储效率: 用 bit 来存储状态,大大节省内存空间。
  • 高效的位操作: Redis 提供了丰富的位操作命令,可以高效地进行各种统计。
  • 简单易用: API 简单明了,容易上手。

局限:

  • 只适用于状态简单的场景: Bitmaps 只能存储 0 和 1 两种状态,不适合存储复杂的数据。
  • 对 Offset 的要求: Offset 必须是整数,而且要尽量连续。
  • 可能存在稀疏问题: 如果用户 ID 分布不均匀,可能会导致 Bitmap 非常稀疏,浪费空间。

表格总结:

特性 描述 优点 缺点 适用场景
存储结构 基于 bit 数组,每个 bit 代表一个状态。 极高的存储效率,节省内存空间。 只能存储 0 和 1 两种状态,不适合存储复杂的数据。 用户在线状态、活跃用户统计、用户签到等需要记录二元状态的场景。
操作命令 SETBIT, GETBIT, BITCOUNT, BITOP, BITPOS, BITFIELD 等。 提供了丰富的位操作命令,可以高效地进行各种统计,例如统计 1 的数量,进行位运算等。 对 Offset 有要求,需要是整数,且尽量连续。 位操作频繁的场景,例如统计一段时间内都活跃的用户。
内存占用 每个用户对应一个 bit,1 亿用户只需要 12.5MB 内存。 显著降低内存占用,尤其在大规模用户场景下。 如果用户 ID 分布不均匀,可能导致 Bitmap 非常稀疏,浪费空间。 大规模用户场景,需要高效地存储和查询用户状态。
适用场景 用户在线状态、活跃用户统计、用户签到、权限控制等。 简单易用,性能高效,节省资源。 不适合存储复杂的数据,例如用户信息、评论内容等。 需要快速判断用户是否满足特定条件,例如用户是否拥有某个权限。
结合 Bloom Filter 在用户 ID 非常大且分散的情况下,先用 Bloom Filter 过滤,再操作 Bitmap。 可以避免操作一些无效的 Offset,节省空间。 Bloom Filter 存在一定的误判率。 需要处理大量用户 ID,且大部分用户 ID 实际上并不存在的情况下。

尾声:总结与展望

总而言之,Redis Bitmaps 是一种非常实用的数据结构,可以高效地解决用户在线状态和活跃度统计等问题。当然,它也有一些局限性,需要根据具体的场景进行选择。

希望通过今天的分享,大家对 Redis Bitmaps 有了更深入的了解。下次再遇到类似的需求,就可以胸有成竹地使用 Bitmaps 来解决问题了。

谢谢大家!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注