目录
1、Bitmap是什么
2、Bitmap 基本命令
3、Bitmap的优点和限制
4、Bitmap使用场景
4.1、引入依赖、配置
4.2、活跃用户
4.3、查询指定日期 活跃的用户数
4.4、扩展 周活跃用户数
4.5、用户/员工签到
总结
可以把BitMap想象成一个数组,树组的下标即是 偏移量,数组只能存储 0 1。
bitmap = 位图,就是 byte 数组,用二进制表示,这个数组只能存储0或者1 。bitmap 就是用最小的单位bit来存储 0/1 从而表示某个元素对应的值或者状态。
setbit key offset value:对key 所存储的字符串值,设置或清除指定偏移量上的位(bit)
getbit key offset : 对key所存储的字符串值,获取指定偏移量上的位(bit)
bitcount key [offset_start, offset_end] : 获取位图指定范围中位值为1的个数如果不指定start与end,则取所有。
bitop op destkey [key1,key2...] : 做多个bitmap的and(交集)、or(并集)、not(非)、xor(异或)操作并将结果保存在destKey中
优点
限制
bit 映射被限制在512MB之内,所以最大是 2^32次方 ,因为 1M = 1024kb , 1kb = 1024 B , 1B = 8bit。
所以1M = 1024*1024 * 8 = 2^23 那么512MB = 2^32 次方 ,所以建议 key的 offset 控制一下。
根据上述了解到的理论知识可知,bitmap可以记录一些状态,通过0 1 区分状态的场景。下面就通过小案例展示 如果通过 RedisTemplate 操作 Bitmap。
1.RedisTemplate 使用opsForValue 操作 setbit和getbit
2.RedisTemplate 没有提供直接操作 bitcount的方法,通过 redisTemplate.execute
来执行bitcount方法
org.springframework.boot
spring-boot-starter-data-redis
org.springframework.boot
spring-boot-starter-web
@Configuration public class RedisConfig {
@Bean public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
StringRedisTemplate template = new StringRedisTemplate(factory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setKeySerializer(stringRedisSerializer);
template.setHashKeySerializer(stringRedisSerializer); template.setValueSerializer(stringRedisSerializer);
template.setHashValueSerializer(stringRedisSerializer);
template.afterPropertiesSet();
return template;
} }
我们假设某一天登录过 就算 活跃 ,那么可以登录后设置 状态为 1 ; key = 2021-06-26
@GetMapping("/login") public void login(String username, String password) {
//1.模拟从数据库查询 用户信息
UserEntity user = findUserByUsername(username);
if (user != null) {
//记录今日活跃
recordActivity(user.getId());
}
//其他业务逻辑
}
private Boolean recordActivity(long id) {
//获取今日日期
LocalDate date = LocalDate.now();
String dateText = date.format(DateTimeFormatter.ISO_DATE);
return redisTemplate.opsForValue().setBit(dateText, id, true);
}
RedisTemplate 没有提供直接操作 bitcount的方法,通过redisTemplate.execute
来执行bitcount方法
/** * 根据日期 查询指定日期的活跃用户数 * * @param dateText */
@GetMapping("/day") public Long dayActivity(String dateText) {
return (Long) redisTemplate.execute((RedisCallback) cn -> cn.bitCount(dateText.getBytes()));
//其他业务逻辑 }
通过上面的基础,我们可以配合 bitop 操作 来统计 一周的 活跃用户数 然后通过 bitcount 计算出来,命令如下所示:
bitop or weekActivityKey 2021-04-26 2021-04-27 2021-04-28 2021-04-29 2021-04-30 2021-05-01 2021-05-02
bitcount weekActivityKey ## 即可得到 近一周的 活跃用户数的统计结果
@GetMapping("/week") public Long weekActivity() { List weekDateList = getWeekDateList(); String result = ""; //读取result 的 结果 return (Long) redisTemplate.execute((RedisCallback) redisConnection -> { RedisStringCommands redisStringCommands = redisConnection.stringCommands(); List collect = weekDateList.stream().map(String::getBytes).collect(Collectors.toList()); //collect.toArray(new byte[][]{new byte[collect.size()]} //先通过 or 操作 计算结果到 result redisStringCommands.bitOp(BitOperation.OR, result.getBytes(), collect.toArray(new byte[][]{new byte[collect.size()]})); return redisConnection.bitCount(result.getBytes()); }); } //查询 最近一周日期list private List getWeekDateList() { List weekList = new ArrayList<>(); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); Calendar c = Calendar.getInstance(); // 今天是一周中的第几天 int dayOfWeek = c.get(Calendar.DAY_OF_WEEK); if (c.getFirstDayOfWeek() == Calendar.SUNDAY) { c.add(Calendar.DAY_OF_MONTH, 1); } // 计算一周开始的日期 c.add(Calendar.DAY_OF_MONTH, -dayOfWeek); for (int i = 1; i <= 7; i++) { c.add(Calendar.DAY_OF_MONTH, 1); weekList.add(sdf.format(c.getTime())); } return weekList; }
签到也是可以通过 0 1 状态来记录。假设公司对100个员工进行签到行为统计,可以为当月每一天分配一个bitmap,这个bitmap保存100个bit位,来记录签到行为。
/** * SETBIT命令 * 员工打卡 * 时间复杂度:O(1) */ public void sign(String key, int employeeNumber){ redisTemplate.opsForValue().setBit(key, employeeNumber - 1, true); } /** * GETBIT命令 * 查看员工打卡情况 * 时间复杂度:O(1) */ public boolean isSigned(String key,int employeeNumber){ return redisTemplate.opsForValue().getBit(key, employeeNumber - 1); }
可以查看某一天的打卡总人数,这样就能根据打卡人数来判断当天的迟到人数比例。
/** * BITCOUNT命令 * 查看某一天的打卡人数 * 时间复杂度:O(N) */ public Long signedCount(String key){ return (Long) redisTemplate.execute((RedisCallback) conn -> conn.bitCount(key.getBytes())); }
如果想看当月没有迟到过的员工呢?就需要用到交集了,对当月每天的bitmap做交集,值为1的员工就是没有迟到过的。bitmap的聚合运算命令 bitop支持AND(与)、OR(或), XOR(异或) and NOT(非)运算,除了NOT后面跟一个bitmap外,其他3种聚合运算后面都可以跟多个bitmap,命令如下:
BITOP AND destkey srckey1 srckey2 srckey3 ... srckeyN
BITOP OR destkey srckey1 srckey2 srckey3 ... srckeyN
BITOP XOR destkey srckey1 srckey2 srckey3 ... srckeyN
BITOP NOT destkey srckey
为了让demo简单一些,给出只查看2天内没有迟到的员工。
/** * 命令:BITOP * 复杂度:O(N) * 整个月全勤的员工数量,这里用2天代表整个月 * @param key1 第一天 * @param key2 第二天 */ public Long signedAllMonth(String key1, String key2){ String andMap = "signedAllMonth11"; redisTemplate.execute((RedisCallback) conn -> conn.bitOp(RedisStringCommands.BitOperation.AND, andMap.getBytes(), key1.getBytes(), key2.getBytes())); return (Long) redisTemplate.execute((RedisCallback) conn -> conn.bitCount(andMap.getBytes())); }
下面给出测试代码,模拟只有50个员工全勤。
@Test public void testSignedAllMonth(){ for (int i = 1; i <= 100; i++){ bitMapService.sign("signed:20201101", i); } for (int i = 1; i <= 100; i += 2){ bitMapService.sign("signed:20201102", i); } Long count = bitMapService.signedAllMonth("signed:20201101", "signed:20201102"); System.out.println("=========="+count); }
bitmap广泛地运用在二值计算的场景,对于一个二值状态只用一个bit位就可以,非常节约内存。比如我们对一个10亿的用户进行日活计算,占用的空间只有120M:
10亿/8/1024/1024=120M