当前位置: 首页 > 科技观察

如何优雅的使用Redis的位图操作

时间:2023-03-17 21:47:55 科技观察

在进入今天的话题之前,先简单介绍一下什么是Redis中的位图。Redis官方文档是这样介绍位图的:位图不是真正的数据类型,而是定义在字符串类型上的面向位操作的集合。由于字符串类型是二进制安全的blob,最大长度为512MB,因此适合设置2^32个不同的位。位操作分为两组:对单个位的恒定时间操作,例如将位设置为1或0,或获取该位的值。对一组位的操作,例如计算指定范围内设置位的数量。位图的最大优点是它们有时是一种非常节省空间的信息存储方式。例如,在一个系统中,不同的用户通过递增的用户ID来表示,512MB的内存可以用来表示400万用户的一位信息(例如他们是否需要接收邮件)。简而言之,位图操作就是用来操作位的,它的好处是节省内存空间。为什么可以节省内存空间?如果我们需要存储100万个用户的登录状态,如果我们使用位图,我们只需要至少100万个位(位1表示登录,位0表示未登录)就可以存储,如果在表单中一个字符串的用于存储,比如以userId为key,是否登录(字符串“1”表示登录,字符串“0”表示未登录),如果存储的是value,则1百万个字符串需要存储。相比之下,使用Bitmap存储占用的空间要少得多,这就是位图存储的优势。位图常用操作位图常用操作如下:setbit设置特定key对应的位的值。getbit获取特定键对应的位值。bitcount将给定键对应的字符串位数统计为1。使用位图存储用户登录状态位图的一个常见应用是存储状态值,例如存储用户登录状态。假设我们现在有一个需要记录用户从注册开始每天的登录状态,那么我们可以以用户id为key,然后以日期或者日期偏移量为下标,将登录状态存储在对应的bit中,这样就可以方便的获取用户在某一天的登录状态。接下来看代码:publicclassUserLoginStatusService{privatestaticfinalStringhost="111.111.111.111";privatestaticfinalintport=6379;privatestaticfinalJedisjedis=newJedis(host,port);//日期的初始值(也可以理解为用户的注册时间),//下面需要使用日期的偏移量作为redis位图的偏移量,//所以需要从日期中减去初始日期来保存登录状态。//这里使用了Java8的新日期APIprivatestaticfinalLocalDatebeginDate=LocalDate.of(2018,1,1);static{jedis.connect();}publicvoidsetLoginStatus(StringuserId,LocalDatedate,booleanisLogin){longoffset=getDateDuration(beginDate,date);jedis.setbit(userId,offset,isLogin);}publicbooleangetLoginStatus(StringuserId,LocalDatedate){longoffset=getDateDuration(beginDate,date);returnjedis.getbit(userId,offset);}privatelonggetDateDuration(LocalDatestart,LocalDateend){returnstart.until(end,ChronoUnit.DAYS);}publicstaticvoidmain(String[]args){UserLoginStatusServiceuserLoginStatusService=newUserLoginStatusService();StringuserId="user_1";LocalDatetoday=LocalDate.now();userLoginStatusService.setLoginStatus(userId,today,true);booleantodayLoginStatus=userLoginStatusService.getLoginStatus(userId,today);System.out.println(String.format("TheloginStatusof%sin%sis%s",userId,today,todayLoginStatus));LocalDateyesterday=LocalDate.now().minusDays(1);booleanyesterdayLoginStatus=userLoginStatusService.getLoginStatus(userId,yesterday);System.out.println(String.format("TheloginStatusof%sin%sis%s",userId,yesterday,yesterdayLoginStatus));}}代码并不复杂,我们在main方法将当天的登录状态设置为true,然后分别找出当天的登录状态和昨天的登录状态。由于redis位图的bit默认为0,所以代码的正确输出应该是今天登录,昨天没有登录。我们运行一次看看结果从程序运行的结果来看,Redis的位图确实满足了我们的需求,还有节省存储空间的优点。使用位图统计登录天数接下来我们有一个新的需求,就是统计一个用户在注册后的前10天内的登录天数。Redis中有一个bitcount命令,可以统计一个字符串中有多少位是1,它还有两个参数start和end,表示要统计的范围。乍一看好像可以满足我们的需求,但是这里有一个坑需要注意。bitcount命令的开始和结束参数是指字节。索引,不是位索引,如果要用位图统计用户注册后前10天的登录天数,需要统计0到9位值的个数bitindex为1,所以直接使用bitcount命令显然不能满足要求。那么如果我们一定要用位图来存储登录状态,怎么办呢?其实还是有办法的。我们可以先得到位索引从0到9的字节数组,然后将字节数组解析成二进制形式,然后统计位索引从0到9的位值为1的个数,比较简单要获取字节数组中位索引所在字节的下标,只需将位索引除以8(一个字节包含8位)并向下舍入即可。接下来就是使用redis的getrange命令截取字节数组了。得到字节数组后,接下来就是解析字节数组,统计值为1的位有多少个。我们先从最简单的单字节开始,假设一个字节的每一位的值如下:我们将位索引设置为索引,如果我们要计算第7位的位值,我们只需要直接将原始值转换为与1进行与操作即可。要计算6位的位值,只需移位原值右移1位,再与1进行与运算。以此类推,计算索引位的位值,只需要先右移(7位索引)位,然后与1进行与运算即可。只要取值为1的位数可以统计截取的字节数组中的,然后减去对应的位索引中不包含的值为1的位的个数,可以统计给定的位索引范围内1的个数。这么说有点啰嗦,还是以上面的例子为例吧。我们想统计用户注册后前10天的登录天数。如果用一个位图来存储用户的登录状态,位图中的索引是注册的天数,那么我们需要从0到9统计位值为1的位的个数,来计算用户注册后前10天内的登录天数。我们先计算从0到9的位索引包含在哪个字节数组中。前面说了,我们只需要将对应的索引除以8,然后向下取整即可。由此可知,从0到9的位索引对应下标从0到1的字节数组。接下来使用getrange命令截取字节数组,假设其取值如下:假设位值bitindex0到9对应的byte数组的个数如上图,我们需要统计第一个字节(下标为0),加上第二个字??节(下标为0)的bits0到1中1的位数1).加起来刚好是10位,就是用户注册前10天对应的登录天数。当然,我们也可以统计这2个字节中1的位值总数,然后从2减去第二个字节中1的位值个数到7(图中标红的地方)上表),也可以统计用户注册后前10天的登录天数。本文采用第二种方法。Next上上言:privatestaticfinalintBIT_AMOUNT_IN_ONE_BYTE=8;privateJedisjedis;publicintbitCountByBitIndex(Stringkey,longstartBitIndex,longendBitIndex){intstartByteIndex=getByteIndexInTheBytes(startBitIndex);intendByteIndex=getByteIndexInTheBytes(endBitIndex,get.getByteskey());byte[]byteranges();)),endByteIndex);inttotalBitInBytes=getTotalBitInBytes(字节);intstartBitIndexInFirstByte=getBitIndexInTheByte(startBitIndex);intendBitIndexInLastByte=getBitIndexInTheByte(endBitIndex);bytefirstByte=字节[0];bytelastByte=字节[bytes.length-1];for(;i>(BIT_AMOUNT_IN_ONE_BYTE-1-startBitIndexInFirstByte);i--){if(((firstByte>>i)&1)==1){totalBitInBytes--;}}for(inti=0;i<(BIT_AMOUNT_IN_ONE_BYTE-1-endBitIndexInLastByte);i++){if(((lastByte>>i)&1)==1){totalBitInBytes--;}}returntotalBitInBytes;}privateintgetTotalBitInBytes(byte[]bytes){intcount=0;for(byteb:bytes){for(inti=0;i>i)&1)==1){count++;}}}returncount;}privateintgetByteIndexInTheBytes(longoffset){return(int)offset/BIT_AMOUNT_IN_ONE_BYTE;}privateintgetBitIndexInTheByte(longoffset){return(int)(offset-offset/BIT_AMOUNT_IN_ONE_AYTE}_BIT代码是不做评论,上面已经说明了整体的思路,当然不是一定要实现本文所描述的功能,还有其他的解决方案,比如:放入bitmap的offset可以乘以8(一个字节占用8位),这样就可以直接使用redis的bitcount来统计1对应索引范围内的bit值个数,当然这种方案的缺点也比较明显,就是,很浪费内存,因为原本只需要1bit存储的数据,需要8bit存储,所以这种方案不能很好的利用位图索引的特性来节省存储空间。

最新推荐
猜你喜欢