• 近期将进行后台系统升级,如有访问不畅,请稍后再试!
  • 极客文库-知识库上线!
  • 极客文库小编@勤劳的小蚂蚁,为您推荐每日资讯,欢迎关注!
  • 每日更新优质编程文章!
  • 更多功能模块开发中。。。

如何优雅地使用Redis之位图操作

前言



在进入今天的主题前,先简单地解释下Redis中的位图到底是什么。Redis官方文档对于位图的介绍如下:

位图不是一个真实的数据类型,而是定义在字符串类型上的面向位的操作的集合。由于字符串类型是二进制安全的二进制大对象,并且最大长度是 512MB,适合于设置 2^32 个不同的位。

位操作分为两组:常量时间单个位的操作,像设置一个位为 1 或者 0,或者获取该位的值。对一组位的操作,例如计算指定范围位的置位数量。

位图的最大优势是有时是一种非常显著的节省空间来存储信息的方式。例如,在一个系统中,不同用户由递增的用户 ID 来表示,可以使用 512MB 的内存来表示 400 万用户的单个位信息(例如他们是否需要接收信件)。

简而言之,位图操作是用来操作比特位的,其优点是节省内存空间。为什么可以节省内存空间呢?假如我们需要存储 100 万个用户的登录状态,使用位图的话最少只需要 100 万个比特位(比特位 1 表示登录,比特位 0 表示未登录)就可以存储了。

而如果以字符串的形式存储,比如说以 userId 为 key,是否登录(字符串“1”表示登录,字符串“0”表示未登录)为 value 进行存储的话,就需要存储 100 万个字符串了,相比之下使用位图存储占用的空间要小得多,这就是位图存储的优势。


位图常用操作



位图的常用操作如下:
  • setbit
    设置特定 key 对应的比特位的值。
  • getbit
    获取特定 key 对应的比特位的值。
  • bitcount
    统计给定 key 对应的字符串比特位为 1 的数量。


使用位图存储用户登录状态



位图的常见应用是用来存储状态值,比如存储用户的登录状态。

假设我们现在有一个需求,需要记录用户注册以来每天的登录状态,那么我们就可以以用户 id 为 key,然后以日期或者日期的偏移量作为下标,将登录状态存储到对应的比特位中,这样就可以很方便地获取用户某一天的登录状态了。

接下来看代码:
publicclassUserLoginStatusService{

   privatestaticfinal String host=“111.111.111.111”;

   privatestaticfinalint port=6379;

   privatestaticfinal Jedis jedis=new Jedis(host,port);

   //日期的初始值(也可以理解为用户的注册时间),
   //下文需要使用日期的偏移量作为 redis 位图的 offset,
   //因此需要将要保存登录状态的日期减去该初始日期。
   //这里使用了 Java 8 的新日期 API
   privatestaticfinal LocalDate beginDate=LocalDate.of(2018,1,1);

   static {
       jedis.connect();
   }

   publicvoidsetLoginStatus(String userId, LocalDate date,boolean isLogin){
       long offset = getDateDuration(beginDate, date);
       jedis.setbit(userId,offset,isLogin);
   }

   publicbooleangetLoginStatus(String userId,LocalDate date){
       long offset = getDateDuration(beginDate, date);
       return jedis.getbit(userId,offset);
   }

   privatelonggetDateDuration(LocalDate start ,LocalDate end){
       return start.until(end, ChronoUnit.DAYS);
   }

   publicstaticvoidmain(String[] args){
       UserLoginStatusService userLoginStatusService=new UserLoginStatusService();
       String userId=“user_1”;
       LocalDate today = LocalDate.now();
       userLoginStatusService.setLoginStatus(userId,today,true);
       boolean todayLoginStatus = userLoginStatusService.getLoginStatus(userId, today);
       System.out.println(String.format(“The loginStatus of %s in %s is %s”,userId,today,todayLoginStatus));
       LocalDate yesterday = LocalDate.now().minusDays(1);
       boolean yesterdayLoginStatus = userLoginStatusService.getLoginStatus(userId, yesterday);
       System.out.println(String.format(“The loginStatus of %s in %s is %s”,userId,yesterday,yesterdayLoginStatus));
   }

}

代码不复杂,我们在 main 方法中设置当天的登录状态为 true,然后分别查出当天的登录状态和昨天的登录状态,由于 redis 位图的比特位默认是 0,所以该代码的正确输出应该是今天已登录,昨天未登录,我们运行一次看看结果。
从程序运行结果来看,Redis的位图确实满足了我们的需求,且兼有节省存储空间的优点。


使用位图统计登录天数



接下来我们有一个新需求,就是统计某个用户注册后前 10 天的登录天数,Redis 中有个 bitcount 命令,可以统计某个字符串中的比特位为 1 的数量,其还有 2 个参数 start 和 end,表示要统计的范围。

咋一看好像可以用来实现我们这个需求,但是这里有一个坑需要注意下,bitcount 命令的 start 和 end 参数指的是字节的索引,而不是比特位的索引,而我们如果要使用位图来统计某个用户注册后前 10 天的登录天数的话,需要统计的是比特位索引从 0 到 9 的比特值为 1 的数量,所以直接使用 bitcount 命令显然是无法满足要求的。

那么假如说我们一定要用位图来存储登录状态呢,应该咋办呢?其实办法还是有的。我们可以先拿到比特位索引从 0 到 9 所在的字节数组,再将该字节数组解析成二进制形式,进而统计出比特位索引从 0 到 9 比特值为 1 的数量。

要拿到比特位索引所在的字节在字节数组中的下标比较简单,只要将比特位索引除以 8(一个字节包含 8 个比特位)再向下取整就行了。接下来就是使用 redis 的 getrange 命令来截取字节数组了。

拿到了字节数组,接下来就是解析字节数组,统计其中比特值为 1 的数量了。我们先从最简单的单个字节说起,假设一个字节的各个比特位的值如下:
我们设比特位索引为 index,假如我们要计算比特位为 7 的比特值,只需要将原值直接跟 1 进行与运算就行了。要计算比特位为 6 的比特值,只需要将原值右移 1 位,再跟 1 进行与运算。以此类推,要计算第 index 位的比特值,只需要先右移(7-index)位,再跟 1 进行与运算即可。

只要能够统计出截取出来的的字节数组中比特位的值为 1 的数量,接下来再减去不包含在对应比特索引中的比特值为 1 的数量,即可统计出给定的比特索引范围内比特值为 1 的数量。

这么说有点拗口,我们以上述例子为例进行讲解吧。我们要统计出用户注册后前 10 天的登录天数,如果用位图存储用户登录状态,位图中的索引为注册天数的话,那么我们需要统计比特索引从 0 到 9 的比特值为 1 的数量,才能计算出该用户注册后前 10 天的登录天数。

我们先计算出比特索引从 0 到 9 包含在哪一段字节数组中,前面说了,只需要将对应的索引除以 8,再向下取整就行了。从而可以得知比特位索引从 0 到 9 对应的是下标从 0 到 1 的字节数组。

接下来使用 getrange 命令截取该字节数组,假设其值如下:
假设比特索引 0 到 9 对应的字节数组的比特值情况如上所示,我们需要统计的是第一个字节(下标为 0)中的 0 到 7 位中比特值为 1 的数量,再加上第二个字节(下标为 1)中的第 0 到 1 位中比特值为 1 的数量。加起来刚好 10 位,也就是对应用户注册前 10 天的登录天数。

当然我们也可以统计出这 2 个字节中的比特值为 1 的总数,再减去第二个字节的从 2 到 7 位(上述表格标红的地方)中比特值为 1 的数量,也可统计出该用户注册后前 10 天的登录天数。本文用的是第二种方法。

接下来上代码:
privatestaticfinalint BIT_AMOUNT_IN_ONE_BYTE =8;

   private Jedis jedis;


   publicintbitCountByBitIndex(String key, long startBitIndex, long endBitIndex){
       int startByteIndex = getByteIndexInTheBytes(startBitIndex);
       int endByteIndex = getByteIndexInTheBytes(endBitIndex);
       byte[] bytes = jedis.getrange(key.getBytes(), startByteIndex, endByteIndex);
       int totalBitInBytes = getTotalBitInBytes(bytes);
       int startBitIndexInFirstByte = getBitIndexInTheByte(startBitIndex);
       int endBitIndexInLastByte = getBitIndexInTheByte(endBitIndex);
       byte firstByte = bytes[0];
       byte lastByte = bytes[bytes.length-1];
       for(int i=7;i>(BIT_AMOUNT_IN_ONE_BYTE-1-startBitIndexInFirstByte);i–){
           if(((firstByte>>i)&1)==1){
               totalBitInBytes–;
           }
       }
       for(int i=0;i<(BIT_AMOUNT_IN_ONE_BYTE-1-endBitIndexInLastByte);i++){
           if(((lastByte>>i)&1)==1){
               totalBitInBytes–;
           }
       }

       return totalBitInBytes;
   }

   privateintgetTotalBitInBytes(byte[] bytes){
       int count=0;
       for(byte b:bytes){
           for(int i = 0; i< BIT_AMOUNT_IN_ONE_BYTE; i++){
               if(((b>>i)&1)==1){
                   count++;
               }
           }
       }
       return count;
   }

   privateintgetByteIndexInTheBytes(long offset){
       return (int) offset/ BIT_AMOUNT_IN_ONE_BYTE;
   }

   privateintgetBitIndexInTheByte(long offset){
       return (int)(offset-offset/ BIT_AMOUNT_IN_ONE_BYTE * BIT_AMOUNT_IN_ONE_BYTE);
   }

代码就不注释了,整体思路已经在上面讲解了。

当然要实现本文所述的功能,也不一定非要这么做,还是有其他的方案的。比如:可以将放入位图的 offset 统一乘以 8(一个字节占 8 比特),这样一来就可以直接用 redis 的 bitcount 来统计对应索引范围内的比特值为 1 的数量了。

当然这种方案的缺点也相当明显,就是浪费内存,因为原先只需要 1 比特存储的数据,现在需要 8 比特存储,所以这种方案不能很好地利用位图索引节省存储空间的特点。


丨极客文库, 版权所有丨如未注明 , 均为原创丨
本网站采用知识共享署名-非商业性使用-相同方式共享 3.0 中国大陆许可协议进行授权
转载请注明原文链接:如何优雅地使用 Redis 之位图操作
喜欢 (0)
[247507792@qq.com]
分享 (0)
勤劳的小蚂蚁
关于作者:
温馨提示:本文来源于网络,转载文章皆标明了出处,如果您发现侵权文章,请及时向站长反馈删除。

欢迎 注册账号 登录 发表评论!

  • 精品技术教程
  • 编程资源分享
  • 问答交流社区
  • 极客文库知识库

客服QQ


QQ:2248886839


工作时间:09:00-23:00