Redis之BitMap

Redis的BitMap

前言

  • 通过一个bit位来表示某个元素对应的值或者状态,其中的key就是对应元素本身
  • 版本:redis 2.2.0
  • 新增 setbit,getbit,bitcount几个bitmap相关命令

setbit命令说明

指令 setbit key offset value

  • 复杂度o(1)
  • 设置或者清空key的取决于value(字符串)在offset处的bit值(只能只0或者1)
  • 字符串会进行伸展(grown)以确保它可以将value 保存在指定的偏移量上。当字符串值进行伸展时,空白位置以 0 填充。
  • offset参数必须大于或等于0,小于2^32
  • 返回值 字符串值指定偏移量上原来存储的位(bit)

getbit命令说明

指令getbit key offest

  • key 所储存的字符串值,获取指定偏移量上的位(bit)。
  • offset 比字符串值的长度大,或者 key 不存在时,返回 0
  • 返回值 字符串指定偏移量上的位

bitcount命令说明

指令bitcount keybitcount key start end

  • 计算给定字符串中,被设置为1的比特位的数量
  • 一般情况下,给定的整个字符串都会被进行计数,通过指定额外的 startend 参数,可以让计数只在特定的位上进行
  • startend 参数的设置和 GETRANGE 命令类似,都可以使用负数值: 比如 -1 表示最后一个字节, -2表示倒数第二个字节,以此类推
  • 不存在的 key 被当成是空字符串来处理,因此对一个不存在的 key 进行 BITCOUNT 操作,结果为 0。
  • 返回值 被设置为1的位的数量

使用场景

1. 用户签到
Jedis redis = new Jedis("192.168.31.89",6379,100000);
//用户uid
String uid = "1";
String cacheKey = "sign_"+Integer.valueOf(uid);
//记录有uid的key
// $cacheKey = sprintf("sign_%d", $uid);

//开始有签到功能的日期
String startDate = "2017-01-01";

//今天的日期
String todayDate = "2017-01-21";

//计算offset(时间戳)
long startTime = dateParase(startDate,"yyyy-MM-dd").getTime();
long todayTime = dateParase(todayDate,"yyyy-MM-dd").getTime();
long offset = (long) Math.floor((todayTime - startTime) / 86400);

System.out.println("今天是第"+offset+"天");

//签到
//一年一个用户会占用多少空间呢?大约365/8=45.625个字节,好小,有木有被惊呆?
redis.setbit(cacheKey,offset,"1");

//查询签到情况
boolean bitStatus = redis.getbit(cacheKey, offset);
//判断是否已经签到
//计算总签到次数
long qdCount = redis.bitcount(cacheKey);
2. 统计活跃用户

使用时间作为cacheKey,然后用户ID为offset,如果当日活跃过就设置为1 那么我该如果计算某几天/月/年的活跃用户呢(暂且约定,统计时间内只有有一天在线就称为活跃)

一个新的命令 BITOP operation destkey key [key ...]

说明

  1. 对一个或多个保存二进制位的字符串 key 进行位元操作,并将结果保存到 destkey 上
  2. BITOP 命令支持 AND 、 OR 、 NOT 、 XOR 这四种操作中的任意一种参数
Map<String,List<Integer>>dateActiveuser = new HashMap<>();
Jedis redis = new Jedis("192.168.31.89",6379,100000);
Integer[] temp01 = {1,2,3,4,5,6,7,8,9,10};
List<Integer>temp01List = new ArrayList<>();
Collections.addAll(temp01List,temp01);
dateActiveuser.put("2017-01-10",temp01List);


Integer[] temp02 = {1,2,3,4,5,6,7,8};
List<Integer>temp02List = new ArrayList<>();
Collections.addAll(temp02List,temp02);
dateActiveuser.put("2017-01-11",temp02List);

Integer[] temp03 = {1,2,3,4,5,6};
List<Integer>temp03List = new ArrayList<>();
Collections.addAll(temp03List,temp03);
dateActiveuser.put("2017-01-12",temp03List);

Integer[] temp04 = {1,4,5,6};
List<Integer>temp04List = new ArrayList<>();
Collections.addAll(temp04List,temp04);
dateActiveuser.put("2017-01-13",temp04List);

Integer[] temp05 = {1,4,5,6};
List<Integer>temp05List = new ArrayList<>();
Collections.addAll(temp05List,temp05);
dateActiveuser.put("2017-01-14",temp05List);

String date[] = {"2017-01-10","2017-01-11","2017-01-12","2017-01-13","2017-01-14"};

//测试数据放入redis中
for (int i=0;i<date.length;i++){
    for (int j=0;j<dateActiveuser.get(date[i]).size();j++){
        redis.setbit(date[i], dateActiveuser.get(date[i]).get(j), "1");
    }
}

//bitOp
redis.bitop(BitOP.AND, "stat", "stat_2017-01-10", "stat_2017-01-11","stat_2017-01-12");

System.out.println("总活跃用户:"+redis.bitcount("stat"));

redis.bitop(BitOP.AND, "stat1", "stat_2017-01-10", "stat_2017-01-11","stat_2017-01-14");
System.out.println("总活跃用户:"+redis.bitcount("stat1"));

redis.bitop(BitOP.AND, "stat2", "stat_2017-01-10", "stat_2017-01-11");
System.out.println("总活跃用户:"+redis.bitcount("stat2"));

假设当前站点有5000W用户,那么一天的数据大约为50000000/8/1024/1024=6MB

3. 用户在线状态

前段时间开发一个项目,对方给我提供了一个查询当前用户是否在线的接口。不了解对方是怎么做的,自己考虑了一下,使用bitmap是一个节约空间效率又高的一种方法,只需要一个key,然后用户ID为offset,如果在线就设置为1,不在线就设置为0,和上面的场景一样,5000W用户只需要6MB的空间。

//批量设置在线状态
$uids = range(1, 500000);
foreach($uids as $uid) {
$redis->setBit('online', $uid, $uid % 2);
}
//一个一个获取状态
$uids = range(1, 500000);
$startTime = microtime(true);
foreach($uids as $uid) {
echo $redis->getBit('online', $uid) . PHP_EOL;
}
$endTime = microtime(true);
//在我的电脑上,获取50W个用户的状态需要25秒
echo "total:" . ($endTime - $startTime) . "s";
/**
* 对于批量的获取,上面是一种效率低的办法,实际可以通过get获取到value,然后自己计算

* 具体计算方法改天再写吧,之前写的代码找不见了。。。
*/