Redis 的 Bitmap(位图)的使用场景
Redis 的 Bitmap(位图)是一种特殊的数据结构,它本质上是一个字符串,但可以将它看作一个由二进制位组成的数组。每个位只能是 0 或 1,因此 Bitmap 非常适合用于存储和处理大量的布尔状态信息,而且非常节省空间。
基本操作:
SETBIT key offset value
: 设置指定偏移量(offset)上的位的值(0 或 1)。GETBIT key offset
: 获取指定偏移量上的位的值。BITCOUNT key [start end]
: 计算指定范围内值为 1 的位的数量。BITOP operation destkey key [key ...]
: 对多个 Bitmap 进行位运算(AND、OR、XOR、NOT),并将结果存储到新的 Bitmap 中。BITPOS key bit [start] [end]
: 查找指定范围内第一个值为 0 或 1 的位的位置。
应用场景:
-
用户签到:
- 使用 Bitmap 存储用户的签到状态。
- 每个用户对应一个 Bitmap,Bitmap 的每一位代表一天。
- 例如,用户 ID 为 1000 的用户,2023 年 10 月 26 日签到,可以将
user_sign:1000
的第 299 位(从 0 开始计算,2023 年的第 299 天)设置为 1。
// 签到 stringRedisTemplate.opsForValue().setBit("user_sign:1000", 299, true); // 查询用户是否签到 boolean signedIn = stringRedisTemplate.opsForValue().getBit("user_sign:1000", 299); // 统计用户当月签到次数 long count = stringRedisTemplate.execute((RedisCallback<Long>) connection -> connection.bitCount("user_sign:1000".getBytes(), 273, 303)); // 计算 10 月份的签到次数 // 统计用户连续签到天数 (需要遍历) // ...
- 可以按月分key, 比如
user_sign:1000:202310
, 这样可以减少key的大小.
-
用户在线状态:
- 使用 Bitmap 存储用户的在线状态。
- 每个用户对应 Bitmap 的一位。
- 用户上线时,将对应的位设置为 1;用户下线时,将对应的位设置为 0。
- 可以配合过期时间使用,例如设置一个较短的过期时间(如 5 分钟),如果用户在过期时间内没有活动,就认为用户离线。
// 用户上线 stringRedisTemplate.opsForValue().setBit("online_users", 1000, true); stringRedisTemplate.expire("online_users", 5, TimeUnit.MINUTES); // 用户下线 stringRedisTemplate.opsForValue().setBit("online_users", 1000, false); // 查询用户是否在线 boolean online = stringRedisTemplate.opsForValue().getBit("online_users", 1000); // 统计在线用户数 long count = stringRedisTemplate.execute((RedisCallback<Long>) connection -> connection.bitCount("online_users".getBytes()));
-
布隆过滤器 (Bloom Filter):
- 布隆过滤器是一种概率型数据结构,用于判断一个元素是否可能存在于一个集合中。
- 可以使用 Bitmap 实现布隆过滤器。
- 将元素通过多个哈希函数映射到 Bitmap 的多个位上,并将这些位设置为 1。
- 判断元素是否存在时,检查对应的位是否都为 1。如果任何一位为 0,则元素一定不存在;如果所有位都为 1,则元素可能存在(存在误判率)。
-
统计活跃用户:
- 可以使用 Bitmap 存储每日的活跃用户。
- 每天使用一个 Bitmap,用户 ID 作为偏移量。
- 用户访问时,将对应的位设置为 1。
- 统计一段时间内的活跃用户数,可以使用
BITOP
命令对多个 Bitmap 进行 OR 运算,然后使用BITCOUNT
统计结果 Bitmap 中 1 的数量。
// 用户访问 stringRedisTemplate.opsForValue().setBit("active_users:2023-10-26", 1000, true); stringRedisTemplate.opsForValue().setBit("active_users:2023-10-27", 1000, true); stringRedisTemplate.opsForValue().setBit("active_users:2023-10-27", 1001, true); // 统计 2023-10-26 到 2023-10-27 的活跃用户数 stringRedisTemplate.execute((RedisCallback<Long>) connection -> { connection.bitOp(RedisStringCommands.BitOperation.OR, "active_users:2023-10-26_27".getBytes(), "active_users:2023-10-26".getBytes(), "active_users:2023-10-27".getBytes()); return connection.bitCount("active_users:2023-10-26_27".getBytes()); }); // 可以使用 Pipeline 批量操作,提高效率 // ...
-
统计用户行为标签:
- 可以使用 Bitmap 存储用户的行为标签。
- 每个标签对应 Bitmap 的一位。
- 例如,可以定义以下标签:
- 0: 喜欢体育
- 1: 喜欢音乐
- 2: 喜欢电影
- …
- 用户具有哪些标签,就将对应的位设置为 1。
- 可以方便地进行标签的交集、并集、差集运算,以实现用户群体的筛选。
// 用户 1000 喜欢体育和音乐 stringRedisTemplate.opsForValue().setBit("user_tags:1000", 0, true); stringRedisTemplate.opsForValue().setBit("user_tags:1000", 1, true); // 查询喜欢体育的用户 // 可以使用 BITOP 和 BITCOUNT 统计喜欢体育的用户数 // ...
-
IP 黑白名单
- 可以将IP地址转换为整数,然后使用Bitmap存储黑白名单。
- 例如,可以将IPv4地址转换为32位整数,IPv6地址转换为128位整数。
-
统计独立访客(UV)
- 可以使用一个大的 Bitmap,每一位代表一个用户。用户访问时,将对应的位设置为1.
BITCOUNT
可以直接统计UV.- 这种方式在用户量非常大时,内存占用也会很大。可以使用 HyperLogLog 作为替代方案 (有一定误差)。
优点:
- 节省空间: Bitmap 使用位存储数据,非常节省空间。例如,存储 1 亿个用户的签到状态,只需要约 12 MB 的内存 (100000000 / 8 / 1024 / 1024)。
- 高效的位运算: Redis 提供了高效的位运算命令(
BITOP
),可以快速进行交集、并集、差集等操作。 - 快速的查找:
GETBIT
和BITPOS
命令可以快速查找指定位的值或位置。
缺点:
- 稀疏位图可能浪费空间: 如果 Bitmap 中 1 的分布非常稀疏,可能会浪费空间。例如,如果只有少数用户签到,但用户 ID 很大,Bitmap 可能会占用大量内存,但其中大部分位都是 0。
- 不适合存储非bool类型的数据
注意事项:
- 合理设计键名: 键名应该具有可读性和区分度,例如
user_sign:{user_id}:{year_month}
。 - 选择合适的偏移量: 偏移量应该根据业务需求进行选择,例如用户 ID、日期等。
- 考虑数据过期: 对于不需要长期保存的数据,可以设置过期时间,以释放内存。
- 注意内存使用: 虽然 Bitmap 很节省空间,但如果存储的数据量非常大,仍然需要注意内存使用情况。可以考虑分片存储或使用 HyperLogLog 等替代方案。
- 注意大Key问题: 如果一个 Bitmap 存储了大量的数据, 可能会导致 Redis 阻塞. 需要根据实际情况进行拆分. 比如按月、按日拆分。
总结:
Redis Bitmap 是一种非常高效的数据结构,适用于存储和处理大量的布尔状态信息。 常见的应用场景包括用户签到、用户在线状态、布隆过滤器、统计活跃用户、用户行为标签等。 在使用 Bitmap 时,需要注意合理设计键名、选择合适的偏移量、考虑数据过期、注意内存使用,以及大key问题。