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)
}
*/
欢迎交流!