Bitmap(位图)是一种紧凑、高效的数据结构,通过二进制位保存大量布尔状态,常用于: 用户签到;在线状态;权限标记;内容是否已读;活跃用户统计。
可以把 Bitmap 理解为一个很长的位数组,其中每个位置只能保存 0 或 1:
0 表示否、未发生或不存在;1 表示是、已发生或存在。例如,依次执行 9 次位设置操作:
redisTemplate.opsForValue().setBit("key", i, true);位图中的数据会逐步变为:
设置 offset 0 后:100000000
设置 offset 1 后:110000000
...
设置 offset 8 后:111111111Bitmap 并不是传统的键值映射。它使用一个 Redis key 保存整个位数组,由 offset 表示具体位置。
Redis 提供了常用的位操作命令:
SETBIT key offset value
GETBIT key offset
BITCOUNT key其中,BITCOUNT 可以统计值为 1 的位数。
可以为每条微博创建一个 Bitmap:
key:weibo:view:{weiboId}
offset:userId
value:是否看过,1 表示看过例如,用户 1001 看过微博 9527:
SETBIT weibo:view:9527 1001 1无论同一个用户重复访问多少次,同一个位置都只会保持为 1,因此:
BITCOUNT weibo:view:9527返回的是独立阅读用户数(UV),而不是总浏览次数(PV)。如果业务还需要统计 PV,应另外使用 INCR 等方式计数。
当 key 不存在时,SETBIT 会自动创建该 key,并将此前未设置的位置视为 0。
Redis Set 同样可以实现去重统计:
SADD weibo:view:9527 1001
SCARD weibo:view:9527
SISMEMBER weibo:view:9527 1001Set 和 Bitmap 的主要区别如下:
| 对比项 | Bitmap | Set |
|---|---|---|
| 保存方式 | 每个用户占一个 bit | 保存完整成员值 |
| 去重统计 | BITCOUNT | SCARD |
| 判断是否存在 | GETBIT | SISMEMBER |
| 内存占用 | 用户 ID 稠密时非常低 | 通常高于 Bitmap |
| 支持字符串 ID | 不能直接支持 | 支持 |
| 精确性 | 直接使用唯一整数 ID 时精确 | 精确 |
| 对稀疏 ID 的适应性 | 较差 | 较好 |
当用户拥有连续、稠密且非负的数字 ID 时,Bitmap 通常非常节省内存。理论上,覆盖 100 万个用户只需要约:
1,000,000 bit ÷ 8 ≈ 125,000 byte ≈ 122 KiB实际 Redis 内存占用还会包含 key、字符串对象和内存分配等额外开销。
但 Bitmap 的空间由最大 offset决定,而不是由实际设置为 1 的位数决定。例如:
SETBIT example 1000000000 1即使只设置了一个位置,也需要扩展到约 10 亿 bit,即大约 119 MiB 的原始位空间。因此,不应直接使用跨度极大的稀疏 ID 作为 offset。
以下示例以 10 万条数据为基准,分别写入:
set10w_uuid;set10w_incr;bitmap10w_hash;bitmap10w_hash_size;bitmap10w_incr。ValueOperations<String, Object> valueOps = redisTemplate.opsForValue();
for (int i = 0; i < 100_000; i++) {
String uuid = UUID.randomUUID().toString();
byte[] bytes = uuid.getBytes(StandardCharsets.UTF_8);
redisTemplate.opsForSet().add("set10w_uuid", uuid);
redisTemplate.opsForSet().add("set10w_incr", String.valueOf(i));
long hash = Integer.toUnsignedLong(
Murmur3.hash_x86_32(bytes, bytes.length, 0)
);
valueOps.setBit("bitmap10w_hash", hash, true);
valueOps.setBit("bitmap10w_hash_size", hash % 100_000, true);
valueOps.setBit("bitmap10w_incr", i, true);
}可以使用 Redis 命令查看各 key 的内存占用:
redis-cli MEMORY USAGE set10w_uuid
redis-cli MEMORY USAGE set10w_incr
redis-cli MEMORY USAGE bitmap10w_hash
redis-cli MEMORY USAGE bitmap10w_hash_size
redis-cli MEMORY USAGE bitmap10w_incr实验通常会体现以下规律:
原始测试得到过如下结果:
set10w_uuid = 100000,耗时约 7 ms
set10w_incr = 100000,耗时约 3 ms
bitmap10w_incr = 100000,耗时约 35 ms
bitmap10w_hash = 99998,耗时约 141 ms这些数字只代表当时的测试环境,不能直接用于普遍的性能结论。网络往返、序列化、Redis 版本、数据分布、是否批量执行以及机器负载都会影响结果。
另外,bitmap10w_hash 的计数小于 10 万,说明不同 UUID 的哈希值发生了碰撞。由此也能看出:**把用户 ID 哈希后映射到单个 bit,不再是严格准确的 UV 统计方案。
Comments (0)
Loading comments...