整理Bitmap在项目中的实战

1、认识Bitmap

bitmap(又称位图)是一种实现对位的操作的“数据结构”,它属于Redis的String数据类型,Redis中一个字符串类型的值最多能存储512MB 的内容,同时每个字符串由多个字节组成,每个字节又由8个Bit 位组成,所以 bitmap 存储上限为2的32次幂。

bitmap本质是普通的字符串,即就是bytes数组,可以通过get/set命令来操作数组。 bitmap相对普通的字符串来说它,不仅效率高而且节省空间。

常见的bitmap的命令整理如下:

命令 含义
SETBIT 向指定位置 (offset) 存入一个0或1
GETBIT 获取指定位置 (offset) 的bit值
BITCOUNT 统计BitMap中值为1的bit位的数量
BITFIELD 操作(查询、修改、自增) BitMap中bit数组中的指定位置 (offset) 的值
BITFIELD_RO 获取BitMap中bit数组,并以十进制形式返回
BITOP‍ 将多个BitMap的结果做位运算 (与 、或、异或)
BITPOS 查找bit数组中指定范围内第一个0或1出现的位置

由于bit位只能0和1两种数据,所以bitmap适用于一些特定的场景,如数据统计、数据标记等。

2、实际的应用——数据标记场景

bitmap在实际的项目中的数据标记方面应用有如京东每日签到送京豆电影、开屏广告是否被点击播放过、连续签到打卡等等。下面以签到场景

签到的实现流程图:

核心的实现代码:

#用户签到 
public Boolean QianDao() { 
  //获取登录用户 
  Long userId = UserHolder.getuser( ) .getId( ); 
  //获取日期 
  LocalDateTime now = LocalDateTime.now(); 
  //拼接redis的key 
  String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM")); 
  String key = "USER_SIGN_" + userId + keySuffix; 
  //获取当前是本月的第几天 
  int dayOfMonth = now.getDayofMonth(); 
  //数据写入redis 
  stringRedisTemplate.opsForValue().setBit(key, dayofMonth -1, true); 

  return Boolean.TRUE; 
}

以上是实现用户签到的实现,如果按年去存储一个用户的签到情况,365 天只大约需要46 Byte(365 / 8),那么按照1000W用户量来计算一年的签到情况只需要44MB的空间。

3、实际的应用——数据统计

bitmap在数据统计方面也是非常优秀的,典型的场景有日活跃用户量的统计、最近—周的活跃用户、统计指定用户一个月或者一年之中的登陆天数等等。下面统计用户签到的数量为例。

用户签到数量统计的流程图:

实现的核心代码:

#统计用户连续签到的天数 
public int getSignCount() ( 
  //获取当前的用户 
  Long userId = UserHolder.getUser().getId(); 
  //获取当前时间 
  LocalDateTime now = LocalDateTime.now(); 
  //拼接redis的key 
  keyString keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM")); 
  String key = "USER_SIGN" + userId + keySuffix; 
  //获取当前是本月的第几天 
  int dayOfMonth = now.getDayofMonth(); 
  //获取本月截至今天为止的所有的签到记录,返回的是一个十进制的数字 
  List<Long> result = stringRedisTemplate.opsForValue().bitField(key,BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(dayofMonth)).valueAt(0)); 
  //没有获取到结果 
  if (CollectionUtils.isEmpty(result))  
    return 0; 
  //获取结果 
  Long num = result.get(0); 
  if (num == null || num == 0) 
    return 0; 
  //循环遍历 
  int count = 0 
  while (true) { 
  //让这个数字与1做与运算,得到数字的最后一个bit位 判断这个数字是否为0 
  if ((num & 1) == 0) { 
  //如果为0,则就是签到结束 
    break; 
  } else { 
    count ++; 
  } 
  num >>>= 1; 
  return count; 
 }

4、实际的应用——布隆过滤器

如果用户的请求过来了,先查询Redis缓存,如果Redis无数据再去查询MySQL,当 MySQ L 中也不存在这条数据时,那么每次查询都要访问数据库,这就是缓存穿透。为了解决缓存穿透问题,我们在Redis前面添加一层布隆过滤器,请求先在布隆过滤器中判断,如果布隆过滤器不存在时,直接返回而不做后续的数据库查询操作。我们使用bitmap完成布隆过滤器的功能,其实现的流程图如下:

实现布隆过滤器的核心代码:

/** 
  * 查询布隆过滤器中是否存在 
  */ 
public boolean checkWithBloomFilter(String checkParam,String key) { 
    //使用简单的Java函数实现哈希函数 
    int hashValue = Math.abs(key.hashCode()); 
    long index = (long) (hashValue % Math.pow(2, 32)); 
    return redisTemplate.opsForValue().getBit(checkParam, index); 
 }

总结:通过巧妙的运用bitmap的bytes数组特性,我们总结几种日常项目中常见的bitmap使用的场景以及核心代码的实现。

6