Redis和PHP的Bitmap于二进制串的相互转换
场景
错题集的存储,需要有正确的题号id集合,错误的题号id集合,两者并集后在全量题的集合中取反就是未答题号id
选型
基于场景的数据结构设计,有试过列表等,测试结果:bitmap要比列表方式节省10倍的空间使用;列表可以FIND_IN_SET
查询,但是bitmap必须整存整取后,在应用端计算。各有优缺点,最后还是选择bitmap节省空间。
原理
将id对应二进制串的位数进行存储,有该id,就将位数的值设置为1,反之为0
操作系统中的位运算,64位的,最大仅支持 0 ~63 之间的位移,但是id没有长度限制。
需要定义一个区间长度,进行拼接。如区间长度:8,id:101的存储位置,即:第 ceil(101/8)
区间的第 101%8
位,这里首位永远是 0,id从正整数开始。
实现
PHP中有实现无限整数的位运算扩展:GMP,对于平常的id使用够用了,但是超大量(id位数上千万甚至亿)的运算会比较耗内存。解决方案也是按照区域划分块,参考:PHP实现Bitmap的探索 - GMP扩展使用
class common_BitMap
{// bit映射基数:二进制const BIT_BASE = 2;private static $cacheObj;static function inst(): self{return self::$cacheObj = self::$cacheObj ?? new self();}/*** setBit** @param int|string $num* @param int $bit* @param bool $bitVal* @return string*/public static function setBit($num, int $bit, bool $bitVal = true):string{$gmp = gmp_init($num, self::BIT_BASE);gmp_setbit($gmp, $bit, $bitVal); // index starts at 0$res = gmp_strval($gmp, self::BIT_BASE);return $res;}/*** setBits** @param array $ids* @return string*/public static function setBits(array $ids = []): string{if (empty($ids)) {return '';}$max = '1'.str_repeat(0, max($ids));$maxGmp = gmp_init($max, self::BIT_BASE);sort($ids);foreach ($ids as $id) {gmp_setbit($maxGmp, $id);}$res = gmp_strval($maxGmp, self::BIT_BASE);return $res;}/*** getArrByBitStr (这里按照字符串处理)** @param string $bitStr* @return array*/public static function getArrByBitStr(string $bitStr):array{if (empty($bitStr)) {return [];}$collect = [];$len = strlen($bitStr);$str = strrev($bitStr);for ($i = 1; $i <= $len; $i ++) {if (intval($str{$i}) == 1) {$collect[] = $i;}}return $collect;}}$num = '10010001';
$str = common_BitMap::setBit($num, 3, 1);
var_dump($str);
// string(8) "10011001"$arr = [1, 12, 23];
$str = common_BitMap::setBits($arr);
var_dump($str);
// string(24) "100000000001000000000010"$decArr = common_BitMap::getArrByBitStr($str);
var_dump($decArr);
/*
array(3) {[0]=>int(1)[1]=>int(12)[2]=>int(23)
}
*/
由于大多数都要应用缓存,所以需要存储上兼容redis的bitmap操作,先实现id数组转换
/*** redisSetBit** @param string $key* @return string*/public static function getRedisBitStrByKey(string $key = ''){if (empty($key)) {return '';}$str = of_accy_cache_redis::inst()->get($key); // 此处基于redis的封装类$res = '';for ($i = 0; $i < strlen($str); $i++) {// 获取每个字节的二进制表示,并去掉前缀 '0b'$byteBinary = str_pad(decbin(ord($str[$i])), 8, '0', STR_PAD_LEFT);$res .= $byteBinary;}return $res;}/*** redisSetBits** @param string $key* @param array $ids* @return string*/public static function redisSetBits(string $key = '', array $ids = []){if (empty($key) || empty($ids)) {return '';}$redis = of_accy_cache_redis::inst();foreach ($ids as $id) {$redis->setBit($key, $id, 1);}$str = $redis->get($key);$res = '';for ($i = 0; $i < strlen($str); $i++) {// 获取每个字节的二进制表示,并去掉前缀 '0b'$byteBinary = str_pad(decbin(ord($str[$i])), 8, '0', STR_PAD_LEFT);$res .= $byteBinary;}return $res;}/*** getBitArrByRedisStr** @param string $bitStr* @return array*/public static function getBitArrByRedisStr(string $bitStr = ''){if (empty($bitStr)) {return [];}$collect = [];$len = strlen($bitStr);for ($i = 1; $i <= $len; $i ++) {if (intval($bitStr{$i}) == 1) {$collect[] = $i;}}return $collect;}
$key = 'bit-test';
of_accy_cache_redis::inst()->setBit($key, 12, 1);
$rStr = common_BitMap::getRedisBitStrByKey($key);
var_dump($rStr);
// string(16) "0000000000001000"$arr = [1 ,12 ,23];
$rStr = common_BitMap::redisSetBits($key, $arr);
var_dump($rStr);
// string(24) "010000000000100000000001"$rArr = common_BitMap::getBitArrByRedisStr($rStr);
var_dump($rArr);
/*
array(3) {[0]=>int(1)[1]=>int(12)[2]=>int(23)
}
*/
对比数组转换后的二进制串差异,发现redis的结果是反序的,如此就有了转换方式。
/*** redisStr2BitStr** @param string $str* @return string*/public static function redisStr2BitStr(string $str = ''){return strrev($str);}
$key = 'bit-test';
$arr = [1 ,12 ,23];$str = common_BitMap::setBits($arr);
var_dump($str);
// string(24) "100000000001000000000010"$rStr = common_BitMap::redisSetBits($key, $arr);
var_dump($rStr);
// string(24) "010000000000100000000001"$bitStr = common_BitMap::redisStr2BitStr($rStr);
var_dump($bitStr);
// string(24) "100000000001000000000010"$bitArr = common_BitMap::getArrByBitStr($bitStr);
var_dump($bitArr);
/*
array(3) {[0]=>int(1)[1]=>int(12)[2]=>int(23)
}
*/
欢迎交流!