Redis Bitmap命令和使用场景
- Bitmap
- 1. 优缺点
- 优点
- 缺点
- 2. 应用场景
- 3. 命令
- `BITCOUNT`
- `BITFIELD`
- `BITFIELD_RO`
- `BITOP`
- `BITPOS`
- `GETBIT`
- `SETBIT`
- 4. 案例场景
- 4.1 员工每月签到
- 服务代码
- 测试代码
- 测试结果
- 4.2 吃鸡小战队的活动人数
- 服务代码
- 测试代码
- 测试结果
Bitmap
Redis 的 Bitmap(位图)是一种特殊的字符串数据类型,它利用字符串类型键(key)来存储一系列连续的二进制位(bits),
每个位可以独立地表示一个布尔值(0 或 1),因此也只有两种状态。其底层实现仍是基于 String 类型。
1. 优缺点
优点
- 极高空间效率:bitmap 是真的节省数据存储空间。粗略的算一下,一亿位的 Bitmap 大概才占 12MB 的内存,相比其他数据结构,能极大地节省存储空间;
- 快速查询:位操作通常比其他数据结构查询速度更快。无论是设置位值还是获取位值,时间复杂度都为 O (1),能够快速响应查询请求;
- 易于操作:支持单个位操作、位统计、位逻辑运算等,运算效率高,不需要进行比较和移位;
缺点
- 它仅适用于表示两种状态,即 0 和 1。对于需要表示更多状态的情况,Bitmap 就不适用了;
- 只有当数据比较密集时才有优势,如果我们只设置(20,30,888888888)三个偏移量的位值,则需要创建一个 99999999 长度的 BitMap ,
但是实际上只存了3个数据,这时候就有很大的空间浪费,碰到这种问题的话,可以通过引入另一个 Roaring BitMap 来解决.
2. 应用场景
主要分成两种场景
- 用户维度的某种属性的状态。
- 例如店铺员工每个月签到状态(签到的天数、连续签到最长的天数等等)
- 某个业务维度的用户状态
- 某个活动中用户的在线状态(统计活跃用户)
- 问卷答题,key=>业务id:每种类型的问卷id;value:参与答卷的用户ID
3. 命令
index | command | description | url |
---|---|---|---|
0 | BITCOUNT |
计算被设置为 1 的比特位的数量,默认是按照BYTE计算 | https://redis.io/docs/latest/commands/bitcount/ |
1 | BITFIELD |
可在一次调用中同时对多个位范围进行操作 | https://redis.io/docs/latest/commands/bitfield/ |
2 | BITFIELD_RO |
BITFIELD 的只读版本 |
https://redis.io/docs/latest/commands/bitfield_ro/ |
3 | BITOP |
对多个位图进行按位运算,并将结果存储在目标键中。运算可以是 AND、OR、XOR 或 NOT。 | https://redis.io/docs/latest/commands/bitop/ |
4 | BITPOS |
返回位图中第一个值为 bit 的二进制位的位置 | https://redis.io/docs/latest/commands/bitpos/ |
5 | GETBIT |
获取指定偏移量上的位(bit) | https://redis.io/docs/latest/commands/getbit/ |
6 | SETBIT |
设置或清除指定偏移量上的位(bit) | https://redis.io/docs/latest/commands/setbit/ |
BITCOUNT
command | BITCOUNT |
---|---|
syntax | BITCOUNT key [start end [BYTE | BIT]] |
description | Count the number of set bits (population counting) in a string. |
time complexity | O(N) |
available in | |
url | https://redis.io/docs/latest/commands/bitcount |
默认是按照BYTE计算,哎。。。。
127.0.0.1:6379> del tom_lixi
(integer) 1
127.0.0.1:6379> setbit tom_lixi 0 1
(integer) 0
127.0.0.1:6379> setbit tom_lixi 2 1
(integer) 0
127.0.0.1:6379>
127.0.0.1:6379> bitcount tom_lixi 0 1
(integer) 2
127.0.0.1:6379> bitcount tom_lixi 0 1 BIT
(integer) 1
127.0.0.1:6379>
BITFIELD
command | BITFIELD |
---|---|
syntax | BITFIELD key [GET encoding offset | [OVERFLOW <WRAP | SAT | FAIL>] <SET encoding offset value | INCRBY encoding offset increment> [GET encoding offset | [OVERFLOW <WRAP | SAT | FAIL>] <SET encoding offset value | INCRBY encoding offset increment> …]] |
description | The command treats a Redis string as an array of bits, and is capable of addressing specific integer fields of varying bit widths and arbitrary non (necessary) aligned offset. In practical terms using this command you can set, for example, a signed 5 bits integer at bit offset 1234 to a specific value, retrieve a 31 bit unsigned integer from offset 4567. Similarly the command handles increments and decrements of the specified integers, providing guaranteed and well specified overflow and underflow behavior that the user can configure. |
time complexity | O(1) for each subcommand specified |
available in | |
url | https://redis.io/docs/latest/commands/bitfield |
- 支持的命令列表。
GET 返回指定的位字段。
SET 设置指定的位域并返回其旧值。
INCRBY 增加或减少(如果给出负增量)指定的位域并返回新值。 - 指定偏移量
有两种方法可以在位域命令中指定偏移量。如果指定了没有任何前缀的数字,则它仅用作字符串中基于零的位偏移量。
但是,如果偏移量以字符为前缀#,则指定的偏移量将乘以整数编码的宽度,例如:BITFIELD mystring SET i8 #0 100 SET i8 #1 200
将在偏移量 0 处设置第一个 i8 整数,在偏移量 8 处设置第二个整数。这样,如果您想要的是一个给定大小的整数普通数组,那么您不必在客户端内部自己进行计算。 - 溢出控制
则默认使用WRAP。使用OVERFLOW命令,用户可以通过指定以下行为之一来微调增量或减量溢出(或下溢)的行为:
- WRAP:环绕,有符号整数和无符号整数均如此。对于无符号整数,环绕就像执行对整数可以包含的最大值取模的运算(C 标准行为)。
而对于有符号整数,环绕意味着溢出会重新开始向最负的值,而下溢会重新开始向最正的值,因此,例如,如果i8将整数设置为值 127,则将其增加 1 将产生-128。 - SAT:使用饱和算法,即在下溢时将值设置为最小整数值,在上溢时将值设置为最大整数值。例如,i8从值 120 开始以 10 为增量递增整数,将得到值 127,
并且进一步递增将始终将值保持在 127。下溢时也会发生同样的情况,但值被阻止在最小负值处。 - FAIL:在此模式下,不会对检测到的溢出或下溢执行任何操作。相应的返回值设置为 NULL,以向调用者发出信号通知情况。
请注意,每个OVERFLOW语句仅影响INCRBY子SET 命令列表中其后的命令,直到下一个OVERFLOW 语句。
下面是返回 NULL 的示例OVERFLOW FAIL。
- 数据类型
- i是有符号
- u是无符号
- 数字标示bit的位数
> BITFIELD mykey OVERFLOW FAIL incrby u2 102 1
1) (nil)
127.0.0.1:6379> bitfield lixi set i8 0 97
1) (integer) 0
127.0.0.1:6379> get lixi
"a"
127.0.0.1:6379> bitfield lixi INCRBY i8 0 1
1) (integer) 98
127.0.0.1:6379> get lixi
"b"
127.0.0.1:6379>
127.0.0.1:6379>
127.0.0.1:6379> getbit lixi 2
(integer) 1
127.0.0.1:6379> getbit lixi 5
(integer) 0
127.0.0.1:6379>
注意:bitfield的解释数据顺序是bit低位是数据的高位。
b => 0110 0010
BITFIELD_RO
command | BITFIELD_RO |
---|---|
syntax | BITFIELD_RO key [GET encoding offset [GET encoding offset …]] |
description | Read-only variant of the BITFIELD command. |
time complexity | O(1) for each subcommand specified |
available in | |
url | https://redis.io/docs/latest/commands/bitfield_ro |
BITOP
command | BITOP |
---|---|
syntax | BITOP <AND | OR | XOR | NOT> destkey key [key …] |
description | Perform a bitwise operation between multiple keys (containing string values) and |
time complexity | O(N) |
available in | |
url | https://redis.io/docs/latest/commands/bitop |
127.0.0.1:6379> del tom
(integer) 1
127.0.0.1:6379> setbit tom 1 1
(integer) 0
127.0.0.1:6379> del lixi
(integer) 1
127.0.0.1:6379> setbit lixi 0 1
(integer) 0
127.0.0.1:6379>
127.0.0.1:6379> bitop OR tom_lixi tom lixi
(integer) 1
127.0.0.1:6379> bitcount tom_lixi
(integer) 2
127.0.0.1:6379>
BITPOS
command | BITPOS |
---|---|
syntax | BITPOS key bit [start [end [BYTE | BIT]]] |
description | Return the position of the first bit set to 1 or 0 in a string. |
time complexity | O(N) |
available in | |
url | https://redis.io/docs/latest/commands/bitpos |
127.0.0.1:6379> del lixi
(integer) 1
127.0.0.1:6379> setbit lixi 0 1
(integer) 0
127.0.0.1:6379> setbit lixi 8 1
(integer) 0
127.0.0.1:6379> bitpos lixi 0 0 -1
(integer) 1
127.0.0.1:6379> bitpos lixi 1 0 -1
(integer) 0
127.0.0.1:6379> bitpos lixi 1 1 -1
(integer) 8
127.0.0.1:6379>
GETBIT
command | GETBIT |
---|---|
syntax | GETBIT key offset |
description | Returns the bit value at offset in the string value stored at key. |
time complexity | O(1) |
available in | |
url | https://redis.io/docs/latest/commands/getbit |
127.0.0.1:6379> getbit lixi 8
(integer) 1
127.0.0.1:6379> getbit lixi 7
(integer) 0
127.0.0.1:6379>
SETBIT
command | SETBIT |
---|---|
syntax | SETBIT key offset value |
description | Sets or clears the bit at offset in the string value stored at key. |
time complexity | O(1) |
available in | |
url | https://redis.io/docs/latest/commands/setbit |
127.0.0.1:6379> setbit lixi 0 1
(integer) 0
127.0.0.1:6379> setbit lixi 8 1
(integer) 0
127.0.0.1:6379>
4. 案例场景
4.1 员工每月签到
场景:可以计算每个员工进行签到、退签、计算每个月签到天数、连续签到最长的天数
服务代码
package com.tom.service;
/**
* @author
*/
public interface BitmapService {
void sign(long uid, String mouth, int day);
void unsign(long uid, String mouth, int day);
int count(long uid, String mouth);
int maxContinueDays(long uid, String mouth);
}
package com.tom.service.impl;
import com.tom.service.BitmapService;
import redis.clients.jedis.UnifiedJedis;
import java.util.List;
import java.util.Objects;
/**
* @author
*/
public class BitmapServiceImpl implements BitmapService {
private static final String SIGN_PREFIX = "sign.";
private final UnifiedJedis unifiedJedis;
public BitmapServiceImpl(UnifiedJedis unifiedJedis) {
this.unifiedJedis = unifiedJedis;
}
private String getKey(String mouth, long shard) {
return SIGN_PREFIX + shard + "." + mouth;
}
@Override
public void sign(long uid, String mouth, int day) {
setbit(uid, mouth, day, true);
}
private void setbit(long uid, String mouth, int day, boolean value) {
String key = getBitmapKey(uid, mouth);
boolean setbit = unifiedJedis.setbit(key, day, value);
long expire = unifiedJedis.expire(key, 32 * 3600 * 24); // 这里写死了32天,但是为了节省内存可以计算到下个月的第一天,用expireAt
System.out.printf("%s %s %s%n", day, setbit, expire);
}
@Override
public void unsign(long uid, String mouth, int day) {
setbit(uid, mouth, day, false);
}
private String getBitmapKey(long uid, String mouth) {
return getKey(mouth, uid);
}
@Override
public int count(long uid, String mouth) {
String bitmapKey = getBitmapKey(uid, mouth);
long bitcount = unifiedJedis.bitcount(bitmapKey);
return (int) bitcount;
}
@Override
public int maxContinueDays(long uid, String mouth) {
String bitmapKey = getBitmapKey(uid, mouth);
List<Long> longs = unifiedJedis.bitfieldReadonly(bitmapKey, "GET", "u32", "0");
Long aLong = longs.get(0);
if (Objects.isNull(aLong)) {
return 0;
}
return getLongMaxContinue(aLong);
}
public static int getLongMaxContinue(Long aLong) {
String binaryString = Long.toBinaryString(aLong);
int resMax = 0;
for (int i = 0; i < binaryString.length(); ++i) {
int curMax = 0;
while (i < binaryString.length() && binaryString.charAt(i) == '1') {
++i;
++curMax;
}
if (curMax > resMax) {
resMax = curMax;
}
}
return resMax;
}
public static void main(String[] args) {
long l = 123456789L;
int longMaxContinue = BitmapServiceImpl.getLongMaxContinue(l);
System.out.println(longMaxContinue);
}
}
测试代码
package com.tom;
import com.tom.constants.RedisConstants;
import com.tom.service.BitmapService;
import com.tom.service.impl.BitmapServiceImpl;
import org.junit.Assert;
import redis.clients.jedis.UnifiedJedis;
/**
* @author
*/
public class TestRedisBitMap {
public static void main(String[] args) {
UnifiedJedis jedis = new UnifiedJedis(RedisConstants.URL);
long uid = 1234L;
String month = "2024-10";
BitmapService bitmapService = new BitmapServiceImpl(jedis);
bitmapService.sign(uid, month, 1);
bitmapService.sign(uid, month, 3);
bitmapService.sign(uid, month, 4);
bitmapService.sign(uid, month, 6);
int count = bitmapService.count(uid, month);
Assert.assertEquals(4, count);
System.out.println(count);
int i = bitmapService.maxContinueDays(uid, month);
Assert.assertEquals(2, i);
System.out.println(i);
jedis.close();
}
}
测试结果
1 false 1
3 false 1
4 false 1
6 false 1
4
2
127.0.0.1:6379> get "sign.1234.2024-10"
"Z"
127.0.0.1:6379>
4.2 吃鸡小战队的活动人数
场景:可以计算参与吃鸡小战队的人数、人员;上线、下线,在线的人员个数
服务代码
package com.tom.service;
import java.util.List;
/**
* @author
*/
public interface ActivityService {
void participate(String activityName, long uid);
void unParticipate(String activityName, long uid);
List<Long> getAllUids(String activityName);
int count(String activityName);
void online(String activityName, long uid);
void offline(String activityName, long uid);
int onlineCount(String activityName);
}
package com.tom.service.impl;
import com.tom.service.ActivityService;
import redis.clients.jedis.UnifiedJedis;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @author
*/
public class ActivityServiceImpl implements ActivityService {
private static final String ALL_UID_PREFIX = "aup.";
private static final String ONLINE_UID_PREFIX = "oup.";
private final UnifiedJedis unifiedJedis;
public ActivityServiceImpl(UnifiedJedis unifiedJedis) {
this.unifiedJedis = unifiedJedis;
}
private String allUidKey(String activityName) {
return ALL_UID_PREFIX + activityName;
}
private String onlineUidKey(String activityName) {
return ONLINE_UID_PREFIX + activityName;
}
@Override
public void participate(String activityName, long uid) {
String key = allUidKey(activityName);
byte[] bytes = longToBytes(uid);
unifiedJedis.sadd(key.getBytes(), bytes);
}
@Override
public void unParticipate(String activityName, long uid) {
String key = allUidKey(activityName);
byte[] bytes = longToBytes(uid);
unifiedJedis.srem(key.getBytes(), bytes);
}
@Override
public List<Long> getAllUids(String activityName) {
String key = allUidKey(activityName);
Set<byte[]> smembers = unifiedJedis.smembers