Redis 的bitmap byte转位bit

发布时间 2023-05-24 11:35:02作者: 木马不是马

bitmap

我们知道redis的bitmap本身不是一种数据结构,底层实际上依靠字符串进行存储,可以借助字符串进行位操作,由于redis的字符串最大内存位512MB,所以bitmap的bit位也是有上限的,8 * 1024 * 1024 * 512 = 2^32,由于C语言字符串的末尾都有一位分隔符,所以bitmap存储的实际上限位 2^32 - 1,所以我们可以借助bitmap来实现很多功能,最常用的比如签到功能,比如用一年一个key,所占用存储空间位 365 / 1024 = 0.36kb,假设有10w个用户全部参与签到 0.36 * 100000 / 1024 = 35MB,10w个用户1年才占用35Mb存储空间,就可以签到功能,想比如结构化数据库,所占存储空间可以说是非常小,并且签到数据保留的作用性不是很大,也没有这个必要,所以设置这些key的过期时间为1年

应用

以签到为例,如果需要获取一年当中的某一天是否签到,只需要getbit,对应的offset为dayOfYear,如果需要统计一年当中已经签到了多少天,可以直接使用bitcount进行统计,但是如果想统计已经签到的天数,具体到一年的第几天就需要进一步操作了
由于存储的时候是设置的bit值,所以不能直接用get方法获取值,需要获取当前字符串的byte
如果用jedis更方便获取byte值,这里使用的是stringRedisTemplate,稍微?复杂一点

String redisKey = "";
byte[] bytes = redisTemplate.execute((RedisConnection connection) -> {
           Jedis jedis = (Jedis) connection.getNativeConnection();
    return jedis.get(redisKey.getBytes());
});

得到的结果为byte[],下一步需要将bytes[]转位二进制,也就是还原为redis底层存储的bit位数组结构,一个byte为8个bit,需要先讲byte转为数字,再转为二进制
对数字和字节进行转换。
假设数据存储是以大端模式存储的:

  • byte: 字节类型 占8位二进制 00000000
  • char: 字符类型 占2个字节 16位二进制 byte[0] byte[1]
  • int : 整数类型 占4个字节 32位二进制 byte[0] byte[1] byte[2] byte[3]
  • long: 长整数类型 占8个字节 64位二进制 byte[0] byte[1] byte[2] byte[3] byte[4] byte[5]
  • long: 长整数类型 占8个字节 64位二进制 byte[0] byte[1] byte[2] byte[3] byte[4] byte[5] byte[6] byte[7]
  • float: 浮点数(小数) 占4个字节 32位二进制 byte[0] byte[1] byte[2] byte[3]
  • double: 双精度浮点数(小数) 占8个字节 64位二进制 byte[0] byte[1] byte[2] byte[3] byte[4]byte[5] byte[6] byte[7]
将bytes转位二进制有很多种方法,java总是把byte当做有符号处理,我们可以将byte值与0xFF进行与运算得到它的无符号值,0xFF为11111111即255,进行&就可以得到无符号位
/**
	 * byte转无符号int
	 *
	 * @param byteValue byte值
	 * @return 无符号int值
	 */
	public static int byteToUnsignedInt(byte byteValue) {
		return byteValue & 0xFF;
	}

如Integer.toBinaryString(13) 为1101;因为要还原成redis实际存储的值,所以这里要进行补0,这里也有好几种方法,可以使用StringUtils.leftPad(s, 8, "0")进行补0 ,也可以
Integer.toBinaryString((tByte & 0xFF) + 0x100).substring(1)得到8位二进制数,0x100为100000000=256,,再从第1位进行截取就得到了二进制值,所以可以进行下面这么写

private static final List<String> byteToStr(byte[] bytes) {
		List<String> result = new ArrayList<>();
		StringBuilder stringBuilder = new StringBuilder();
		for (byte aByte : bytes) {
			stringBuilder.append(Integer.toBinaryString((aByte & 0xFF) + 0x100).substring(1));
		}
		String str = stringBuilder.toString();
		for (int i = 0; i < str.length(); i++) {
			if (str.charAt(i) == '1') {
				result.add(String.valueOf(i));
			}
		}
		return result;
	}
byte[] result = {0, 13, -64, -31, 101, 88, 47, -64};
		System.out.println(byteToStr(result));  

最后得到的结果为:

[12, 13, 15, 16, 17, 24, 25, 26, 31, 33, 34, 37, 39, 41, 43, 44, 50, 52, 53, 54, 55, 56, 57]

这样就计算出了一年当中有哪些天进行了签到