Redis-Bitmap介绍及使用

目录

1、Bitmap是什么

2、Bitmap 基本命令

3、Bitmap的优点和限制

4、Bitmap使用场景

4.1、引入依赖、配置

4.2、活跃用户

4.3、查询指定日期 活跃的用户数

4.4、扩展 周活跃用户数

4.5、用户/员工签到

总结


1、Bitmap是什么

可以把BitMap想象成一个数组,树组的下标即是 偏移量,数组只能存储 0 1。

bitmap = 位图,就是 byte 数组,用二进制表示,这个数组只能存储0或者1 。bitmap 就是用最小的单位bit来存储 0/1 从而表示某个元素对应的值或者状态。

2、Bitmap 基本命令

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中

3、Bitmap的优点和限制

优点

  1. 基于最小单位bit存储数据,非常节省空间,大概的空间占用计算公式是:($offset/8/1024/1024)MB,比如我有5000W用户,那么一天的数据大约为50000000/8/1024/1024=6MB
  2. 设置数据的时间复杂度O(1) 获取数据的时间复杂度 也是 O(1),所以操作很快
  3. 方便扩容

限制

bit 映射被限制在512MB之内,所以最大是 2^32次方 ,因为 1M = 1024kb  ,  1kb = 1024 B  , 1B = 8bit。

所以1M = 1024*1024 * 8   = 2^23     那么512MB = 2^32 次方 ,所以建议 key的 offset 控制一下。

4、Bitmap使用场景

根据上述了解到的理论知识可知,bitmap可以记录一些状态,通过0 1 区分状态的场景。下面就通过小案例展示 如果通过 RedisTemplate 操作 Bitmap。

1.RedisTemplate 使用opsForValue 操作 setbit和getbit

2.RedisTemplate 没有提供直接操作 bitcount的方法,通过 redisTemplate.execute

来执行bitcount方法

4.1、引入依赖、配置

    
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;   
} }

4.2、活跃用户

我们假设某一天登录过 就算 活跃 ,那么可以登录后设置 状态为 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); 
}

4.3、查询指定日期 活跃的用户数

RedisTemplate 没有提供直接操作 bitcount的方法,通过redisTemplate.execute

来执行bitcount方法

/** * 根据日期 查询指定日期的活跃用户数 * * @param dateText */
 @GetMapping("/day") public Long dayActivity(String dateText) {   
 return (Long) redisTemplate.execute((RedisCallback) cn -> cn.bitCount(dateText.getBytes()));    
//其他业务逻辑 }

4.4、扩展 周活跃用户数

通过上面的基础,我们可以配合 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; }

4.5、用户/员工签到

签到也是可以通过 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

你可能感兴趣的:(Redis由浅入深,redis,java)