Redis中位图Bitmaps的使用(签到功能的实现)

Bitmaps并不属于Redis中数据结构的一种,它其实是使用了字符串类型,是set、get等一系列字符串操作的一种扩展,与其不同的是,它提供的是位级别的操作,从这个角度看,我们也可以把它当成是一种位数组、位向量结构。当我们需要存取一些boolean类型的信息时,Bitmap是一个非常不错的选择,在节省内存的同时也拥有很好的存取速度(getbit/setbit操作时间复杂度为O(1))。
假设现在我们希望记录自己网站上的用户的上线频率,比如说,计算用户A上线了多少天,用户B上
线了多少天
,诸如此类,以此作为数据,从而决定让哪些用户参加beta测试等活动——这个模式可以使
用SETBIT和BITCOUNT来实现。

ATTENTION:    Redis BitMap 的底层数据结构实际上是 String 类型,Redis 对于 String 类型有最大值限制不得超过 512M,即 2^32 次方 byte,因此索引位置不能超过 2^32 次方, 新注册的用户 id是18 位会导致有问题(  https://mp.weixin.qq.com/s/Sx6dgap1jr2nm4q9i_R_Tg )

TODO:  解决index聚散导致value值过大问题

待研究:

             几种类型的优缺点 推荐使用RoaringBitmap???  https://blog.csdn.net/lao000bei/article/details/105725579

             BitMap与RoaringBitmap、JavaEWAH    https://blog.csdn.net/weixin_42142408/article/details/89426499

             Redis 大数据内存优化 (RoaringBitmap) https://www.jianshu.com/p/4da1546ab643

           

  //三种map的初始研究   
  public static void main(String[] args) throws IOException {
        bitMap();
        javaEWAH();
        roadingBitMap();
    }

    /**
     * 普通bitMap使用
     */
    public static void bitMap(){
        ArrayList<String> values = new ArrayList<>();
        for (int i = 0; i < 100000; i++) {
            values.add(getMD5(String.valueOf(i)));
        }

        Jedis jedis = new Jedis("localhost",6379);
        jedis.auth("root");
        try {
            int maxValue=0;
            Pipeline pipeline = jedis.pipelined();
            for (String arg : values) {
                int offset = arg.hashCode() & 0x7FFFFFFF;
                maxValue=offset>maxValue?offset:maxValue;
                pipeline.setbit("audience_id", offset, true);
            }
            System.err.println(maxValue);
            pipeline.sync();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (jedis != null) jedis.close();
        }
        System.out.println("成功 bitmap"); System.out.println("成功");
    }

    /**
     * maven依赖
     *    <dependency>
     *       <groupId>org.roaringbitmap</groupId>
     *       <artifactId>RoaringBitmap</artifactId>
     *       <version>0.8.1</version>
     *    </dependency>
     * roadingBitMap咆哮位图的使用
     *   map之间运算 https://www.liangzl.com/get-article-detail-148556.html
     * @throws IOException
     */
    public static void roadingBitMap() throws IOException {
        int size=1000;
        int[] offsets = new int[size];
        for (int i = 0; i < size; i++) {
            int offset = getMD5(String.valueOf(i)).hashCode() & 0x7FFFFFFF;
            offsets[i] = offset;
        }

        RoaringBitmap roaringBitmap = new RoaringBitmap();
        roaringBitmap.add(offsets);

        // 遍历输出
        roaringBitmap.forEach((IntConsumer) i -> System.out.println(i));

        int roaringMapSize = roaringBitmap.serializedSizeInBytes();
        ByteBuffer buffer = ByteBuffer.allocate(roaringMapSize);
        roaringBitmap.serialize(buffer);
        // 将RoaringBitmap的数据转成字节数组,这样就可以直接存入数据库了,数据库字段类型BLOB
        byte[] bitmapData = buffer.array();

        Jedis jedis = new Jedis("localhost",6379);
        jedis.auth("root");
        // Base64编码后存入Redis
        String encodeString = Base64.getEncoder().encodeToString(bitmapData);
        jedis.set("roaringMap", encodeString);

        String roaringMapStr  = jedis.get("roaringMap");
        //反序列化
        byte[] decode = Base64.getDecoder().decode(roaringMapStr);

        RoaringBitmap deseRoaringBitmap = new RoaringBitmap();
        deseRoaringBitmap.deserialize(new DataInputStream(new ByteArrayInputStream(decode)));
        System.out.println("roaringMap (recovered) : " + deseRoaringBitmap);

        //map 元素大小
        System.out.println(deseRoaringBitmap.getLongCardinality());

        //向rr中添加1、2、3、1000四个数字
        RoaringBitmap rr1 = RoaringBitmap.bitmapOf(1,2,3,1000);
        RoaringBitmap rr2 = RoaringBitmap.bitmapOf(3,4,5,1000);
        //求交集
        RoaringBitmap intersection = RoaringBitmap.and(rr1, rr2);
        System.out.println(intersection);
    }

    /**
     *  谷歌 javaEWAH bitMap
     *  <dependency>
     *      <groupId>com.googlecode.javaewah</groupId>
     *      <artifactId>JavaEWAH</artifactId>
     *      <version>1.1.6</version>
     *  </dependency>
     * todo 如何动态添加元素
     * EWAHCompressedBitmap计算: https://blog.csdn.net/a5678110/article/details/102048463
     *
     */
    public static void javaEWAH(){
        int[] offsets = new int[100000];
        for (int i = 0; i < 100000; i++) {
            int offset = getMD5(String.valueOf(i)).hashCode() & 0x7FFFFFFF;
            offsets[i] = offset;
        }

        EWAHCompressedBitmap ewahBitmap = EWAHCompressedBitmap.bitmapOf(offsets);

        //序列化与反序列化
        try {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ewahBitmap.serialize(new DataOutputStream(bos));
            byte[] bout = bos.toByteArray();

            Jedis jedis = new Jedis("localhost",6379);
            jedis.auth("root");
            //序列化
            String encodeString = Base64.getEncoder().encodeToString(bout);
            jedis.set("ewahBitmap", encodeString);

            String ewahBitmapStr  = jedis.get("ewahBitmap");
            //反序列化
            byte[] decode = Base64.getDecoder().decode(ewahBitmapStr);
            EWAHCompressedBitmap deseEwahBitmap = new EWAHCompressedBitmap();
            deseEwahBitmap.deserialize(new DataInputStream(new ByteArrayInputStream(decode)));
            System.out.println("ewahBitmap (recovered) : " + deseEwahBitmap);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //盐,用于混交md5
    static final String slat = "&%5123***&&%%$$#@";

    /**
     * spring框架生成md5
     * @param str
     * @return
     */
    public static String getMD5(String str) {
        String base = str +"/"+slat;
        String md5 = DigestUtils.md5DigestAsHex(base.getBytes());
        return md5;
    }

  

性能

以上线次数统计例子,即使运行10年,占用的空间也只是每个用户10*365比特位(bit),也即是每个
用户456字节。对于这种大小的数据来说,BITCOUNT的处理速度就像GET和INCR这种O(1)复杂度的
操作一样快。

 

1 常用命令
SETBIT key offset value   可用版本 >= 2.2.0

设置或者清空key的value(字符串)在offset处的bit值。当key不存在的时候,将新建字符串value。参数offset需要大于等于0,并且小于232(限制Bitmap大小为512MB)。没有setbit的位会默认设定为0


GETBIT key offset 可用版本 >= 2.2.0
返回key对应的string在offset处的bit值。当offset超出了字符串长度或key不存在时,返回0。

BITCOUNT key [start end]   可用版本 >= 2.6.0
时间复杂度: O(N)
统计字符串被设置为1的bit数。需要注意的是,这里的start和end并不是位偏移,而是以字节(8位)为单位来偏移的,比如BITCOUNT foo 0 1是统计key为foo的字符串中第一个到第二个字节中bit为1的总数。

BITPOS key bit [start] [end]  可用版本>= 2.8.7
时间复杂度: O(N),其中 N 为位图包含的二进制位数量
返回位图中第一个值为 bit 的二进制位的位置。
在默认情况下, 命令将检测整个位图, 但用户也可以通过可选的 start 参数和 end 参数指定要检测的范围。

BITOP operation destkey key [key ...]
起始版本:2.6.0  复杂度是O(N)

对一个或多个保存二进制位的字符串 key 进行位元操作,并将结果保存到 destkey 上。

BITOP 命令支持 AND 、 OR 、 NOT 、 XOR 这四种操作中的任意一种参数:

  • BITOP AND destkey srckey1 srckey2 srckey3 ... srckeyN ,对一个或多个 key 求逻辑并,并将结果保存到 destkey 。
  • BITOP OR destkey srckey1 srckey2 srckey3 ... srckeyN,对一个或多个 key 求逻辑或,并将结果保存到 destkey 。
  • BITOP XOR destkey srckey1 srckey2 srckey3 ... srckeyN,对一个或多个 key 求逻辑异或,并将结果保存到 destkey 。
  • BITOP NOT destkey srckey,对给定 key 求逻辑非,并将结果保存到 destkey 。

除了 NOT 操作之外,其他操作都可以接受一个或多个 key 作为输入。
执行结果将始终保持到destkey里面。

BITFIELD key [GET type offset] [SET type offset value] [INCRBY type offset increment] [OVERFLOW WRAP|SAT|FAIL]
可用版本: >=3.2.0
时间复杂度: O(1)用于指定的每个子命令

bitfield 有三个子指令,分别是 get/set/incrby,它们都可以对指定位片段进行读写,但是最多只能处理 64 个连续的位,如果超过 64 位,就得使用多个子指令,bitfield 可以一次执行多个子指令。

eg :   命令将位偏移量为100的8位有符号整数加1,并在位偏移量0处获取4位无符号整数的值:
> BITFIELD mykey INCRBY i5 100 1 GET u4 0
1) (integer) 1
2) (integer) 0

 

2 使用Bitmaps实现统计活跃用户数

Bitmap常见的应用场景之一就是用户签到了,在这里,我们以日期作为key,以用户ID作为位偏移(也就是索引),存储用户的签到信息(1为签到,0为未签到)。不过这个方法也是有前置条件的,那就是 userid 是整数连续的,并且活跃占比较高,否则可能得不偿失。

用户签到                                     SETBIT 20190602  userId  1
用户是否签到                              GETBIT 20190602  userId
某天用户的活跃数:                      BITCOUNT  20190602
用户某个时间段签到次数            通过遍历获取GETBIT 20190602  userId值
用户连续签到天数                       get userId   设置过期日期为第二天 
获取当天第一个签到到的用户     BITPOS 20190602 1

统计: 

$redis->bitop('AND', 'threeAnd', 'login:20190311', 'login:20190312', 'login:20190313');
echo "连续三天都签到的用户数量:" . $redis->bitCount('threeAnd');

$redis->bitop('OR', 'threeOr', 'login:20190311', 'login:20190312', 'login:20190313');
echo "三天中签到用户数量(有一天签也算签了):" . $redis->bitCount('threeOr');

$redis->bitop('AND', 'monthActivities'', $redis->keys('login:201903*'));
echo "连续一个月签到用户数量:" . $redis->bitCount('monthActivities');

 

3使用Bitmaps 用户在线状态

考虑到每月初需要重置连续签到次数,最简单的方式是按用户每月存一条签到数据(也可以每年存一条数据)。Key的格式为u:sign:uid:yyyyMM,Value则采用长度为4个字节(32位)的位图(最大月份只有31天)。位图的每一位代表一天的签到,1表示已签,0表示未签。

#用户2月17号签到
SETBIT u:sign:1000:201902 16 1 偏移量是从0开始,所以要把17减1
 
#检查2月17号是否签到
GETBIT u:sign:1000:201902 16   偏移量是从0开始,所以要把17减1
 
#统计2月份的累计签到次数
BITCOUNT u:sign:1000:201902

#统计2月份的连续签到次数(暂定),并设置有效期为第二天
set u:signcount:1000:201902
 
#获取2月份前28天的签到数据
BITFIELD u:sign:1000:201902 get u28 0
 
#获取2月份首次签到的日期
BITPOS u:sign:1000:201902 1  返回的首次签到的偏移量,加上1即为当月的某一天

参考: https://blog.csdn.net/CrazyLai1996/article/details/85220910

         https://www.cnblogs.com/liujiduo/p/10396020.html

         https://blog.csdn.net/HUXU981598436/article/details/88191152

          https://learnku.com/articles/25181

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值