整理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使用的场景以及核心代码的实现。