布隆过滤器 php+redis

前言

在实际开发中,我们是否有遇到以下情况。

  • 缓存穿透:某个用户一直在请求一个不存在的商品信息,导致我的数据库宕机了(请求一次获取商品信息数据后台要做的工作比较多,如统计访问量,商品数据的构成由多个表的数据聚合及后台对商品数据的二次换算等。)
  • 某个时间点,我的网站有大量的有效用户在注册或是在活跃。
    分析场景:
    1、正在注册的用户产生了大量的写请求,在注册流程中用户名的唯一性效验是去user表中进行的查询。而当时有大量的查询访问
    2、活跃的用户cookie存储的是用户名,每次刷新一次页面或是跳转都会去user表中效验该用户名对应用户是否存在

基于此,导致数据库宕机了。怎么来处理呢?

答案:做布隆过滤吧

布隆过滤:目标数据晒查出来的结果是:可能存在;一定不存在。

原理

把集合中的每一个值按照提供的Hash算法算出对应的Hash值,然后将Hash值对数组长度取模后得到需要计入数组的索引值,并且将数组这个位置的值从0改成1。在判断一个元素是否存在于这个集合中时,你只需要将这个元素按照相同的算法计算出索引值,如果这个位置的值为1就认为这个元素极大可能在集合中,否则认为绝对不在集合中。
布隆过滤器 php+redis_第1张图片
hash解析映射

布隆过滤器 php+redis_第2张图片
redis bit map 映射

实现

1、先定义出hash算法。用于对数据进行hash计算

class BloomFilterHash
{
    /**
     * 由Justin Sobel编写的按位散列函数
     */
    public function JSHash($string, $len = null)
    {
        $hash = 1315423911;
        $len || $len = strlen($string);
        for ($i=0; $i<$len; $i++) {
            $hash ^= (($hash << 5) + ord($string[$i]) + ($hash >> 2));
        }
        return ($hash % 0xFFFFFFFF) & 0xFFFFFFFF;
    }

    /**
     * 该哈希算法基于AT&T贝尔实验室的Peter J. Weinberger的工作。
     * Aho Sethi和Ulman编写的“编译器(原理,技术和工具)”一书建议使用采用此特定算法中的散列方法的散列函数。
     */
    public function PJWHash($string, $len = null)
    {
        $bitsInUnsignedInt = 4 * 8; //(unsigned int)(sizeof(unsigned int)* 8);
        $threeQuarters = ($bitsInUnsignedInt * 3) / 4;
        $oneEighth = $bitsInUnsignedInt / 8;
        $highBits = 0xFFFFFFFF << (int) ($bitsInUnsignedInt - $oneEighth);
        $hash = 0;
        $test = 0;
        $len || $len = strlen($string);
        for($i=0; $i<$len; $i++) {
            $hash = ($hash << (int) ($oneEighth)) + ord($string[$i]); } $test = $hash & $highBits; if ($test != 0) { $hash = (($hash ^ ($test >> (int)($threeQuarters))) & (~$highBits));
        }
        return ($hash % 0xFFFFFFFF) & 0xFFFFFFFF;
    }

    /**
     * 类似于PJW Hash功能,但针对32位处理器进行了调整。它是基于UNIX的系统上的widley使用哈希函数。
     */
    public function ELFHash($string, $len = null)
    {
        $hash = 0;
        $len || $len = strlen($string);
        for ($i=0; $i<$len; $i++) {
            $hash = ($hash << 4) + ord($string[$i]); $x = $hash & 0xF0000000; if ($x != 0) { $hash ^= ($x >> 24);
            }
            $hash &= ~$x;
        }
        return ($hash % 0xFFFFFFFF) & 0xFFFFFFFF;
    }

    /**
     * 这个哈希函数来自Brian Kernighan和Dennis Ritchie的书“The C Programming Language”。
     * 它是一个简单的哈希函数,使用一组奇怪的可能种子,它们都构成了31 .... 31 ... 31等模式,它似乎与DJB哈希函数非常相似。
     */
    public function BKDRHash($string, $len = null)
    {
        $seed = 131;  # 31 131 1313 13131 131313 etc..
        $hash = 0;
        $len || $len = strlen($string);
        for ($i=0; $i<$len; $i++) {
            $hash = (int) (($hash * $seed) + ord($string[$i]));
        }
        return ($hash % 0xFFFFFFFF) & 0xFFFFFFFF;
    }

    /**
     * 这是在开源SDBM项目中使用的首选算法。
     * 哈希函数似乎对许多不同的数据集具有良好的总体分布。它似乎适用于数据集中元素的MSB存在高差异的情况。
     */
    public function SDBMHash($string, $len = null)
    {
        $hash = 0;
        $len || $len = strlen($string);
        for ($i=0; $i<$len; $i++) {
            $hash = (int) (ord($string[$i]) + ($hash << 6) + ($hash << 16) - $hash);
        }
        return ($hash % 0xFFFFFFFF) & 0xFFFFFFFF;
    }

    /**
     * 由Daniel J. Bernstein教授制作的算法,首先在usenet新闻组comp.lang.c上向世界展示。
     * 它是有史以来发布的最有效的哈希函数之一。
     */
    public function DJBHash($string, $len = null)
    {
        $hash = 5381;
        $len || $len = strlen($string);
        for ($i=0; $i<$len; $i++) {
            $hash = (int) (($hash << 5) + $hash) + ord($string[$i]);
        }
        return ($hash % 0xFFFFFFFF) & 0xFFFFFFFF;
    }

    /**
     * Donald E. Knuth在“计算机编程艺术第3卷”中提出的算法,主题是排序和搜索第6.4章。
     */
    public function DEKHash($string, $len = null)
    {
        $len || $len = strlen($string);
        $hash = $len;
        for ($i=0; $i<$len; $i++) {
            $hash = (($hash << 5) ^ ($hash >> 27)) ^ ord($string[$i]);
        }
        return ($hash % 0xFFFFFFFF) & 0xFFFFFFFF;
    }

    /**
     * 参考 http://www.isthe.com/chongo/tech/comp/fnv/
     */
    public function FNVHash($string, $len = null)
    {
        $prime = 16777619; //32位的prime 2^24 + 2^8 + 0x93 = 16777619
        $hash = 2166136261; //32位的offset
        $len || $len = strlen($string);
        for ($i=0; $i<$len; $i++) {
            $hash = (int) ($hash * $prime) % 0xFFFFFFFF;
            $hash ^= ord($string[$i]);
        }
        return ($hash % 0xFFFFFFFF) & 0xFFFFFFFF;
    }
}

2、抽象布隆过滤操作



namespace App\Components\BloomFilter\Core;

/**
 * 布隆过滤
 */
abstract class BloomFilterCore
{
    /**
     * 增加
     *
     * @param string $bucket
     * @param string $string
     */
    abstract public function add(string $bucket,string $string) : void;

    /**
     * 效验
     *
     * @param string $string
     * @return bool
     */
    abstract public function exists(string $bucket,string $string) : bool;
}

3、布隆过滤封装实现

a、通用版 - redis



namespace App\Components\BloomFilter;

use App\Components\BloomFilter\Core\BloomFilterCore;
use App\Components\BloomFilter\Core\HashFunction;
use App\Components\BloomFilter\Core\HashFunctionType;
use App\Exceptions\AppException;
use Illuminate\Redis\RedisManager;

/**
 * 布隆过滤  通用版
 */
class BloomFilterBase extends BloomFilterCore
{
    /**
     * bucket 名称
     *
     * @var string
     */
    protected string $bucket = 'base';

    /**
     * hash 算法
     *
     * @var array
     */
    protected array $hashFunction = [
        HashFunctionType::BKDRHash,
        HashFunctionType::SDBMHash,
        HashFunctionType::JSHash
    ];

    protected HashFunction $hashFunctionObj;

    protected RedisManager $redis;

    public function __construct()
    {
        if (empty($this->bucket) || empty($this->hashFunction))
        {
            throw new AppException("需要定义bucket和hashFunction");
        }

        $this->hashFunctionObj = new HashFunction();

        $this->redis = app()->get('redis');
    }

    /**
     * 添加到集合中
     */
    public function add(string $bucket,string $string) : void
    {

        /**
         * @var \Redis $pipe
         */
        $pipe = $this->redis->multi();
        foreach ($this->hashFunction as $function)
        {
            $hash = $this->handlerHashNum(
                call_user_func([$this->hashFunctionObj,$function->value],$string)
            );
            $this->redis->setBit($bucket ?? $this->bucket, $hash, 1);
        }
        $pipe->exec();
    }

    public function handlerHashNum($num) : int
    {
        $limit = 11;

        if($num > $limit * 100000000)
        {
            $num = substr($num,0,9);
        }

        return intval($num);
    }

    /**
     * 查询是否存在, 如果曾经写入过,必定回true,如果没写入过,有一定几率会误判为存在
     */
    public function exists(string $bucket,string $string) : bool
    {
        /**
         * @var \Redis $pipe
         */
        $pipe = $this->redis->multi();
        $len = strlen($string);
        foreach ($this->hashFunction as $function)
        {
            $hash = $this->handlerHashNum(
                call_user_func([$this->hashFunctionObj,$function->value],$string)
            );
            $pipe = $pipe->getBit($bucket ?? $this->bucket, $hash);
        }
        $res = $pipe->exec();

        foreach ($res as $bit)
        {
            if ($bit == 0)
            {
                return false;
            }
        }
        return true;
    }
}


b、integer 版 - redis



namespace App\Components\BloomFilter;

use App\Components\BloomFilter\Core\BloomFilterCore;
use App\Exceptions\AppException;
use Illuminate\Redis\RedisManager;

/**
 * 布隆过滤  integer 性能版
 */
class BloomFilterIntegerBase extends BloomFilterCore
{
    /**
     * bucket 名称
     *
     * @var string
     */
    protected string $bucket = 'integer_base';

    protected RedisManager $redis;

    public function __construct()
    {
        if (empty($this->bucket))
        {
            throw new AppException("需要定义bucket和hashFunction");
        }

        $this->redis = app()->get('redis');
    }

    public function add(string $bucket, string $string): void
    {
        $this->redis->setBit($bucket ?? $this->bucket, intval($string), 1);
    }

    public function exists(string $bucket, string $string): bool
    {
        return $this->redis->getBit($bucket ?? $this->bucket, intval($string)) != 0;
    }

}

关于使用


//布隆过滤 - 通用版 - 测试
$key = 'mall_shop_2';

\App\Components\BloomFilter\BloomFilter::add('test1',$key);

$result2 = \App\Components\BloomFilter\BloomFilter::exists('test1',$key);

dd([
    // '$result1'  =>  $result1,
    '$result2'  =>  $result2,
]);


//布隆过滤 - integer版 - 测试

$key = '1111';

\App\Components\BloomFilter\BloomFilterInteger::add('test1',$key);

$result2 = \App\Components\BloomFilter\BloomFilterInteger::exists('test1',$key);

dd([
    // '$result1'  =>  $result1,
    '$result2'  =>  $result2,
]);

应用场景

  • 预防缓存穿透;预防黑客攻击。降低数据库压力
  • 在大量写入场景下(如用户注册,效验用户名是否已存在),解决对数据库写压力的情况。降低数据库压力
  • 在大量请求下,筛出真实有效的访问(踢出无效访问,如用户要访问一个不存在的内容)。降低数据库压力

你可能感兴趣的:(php,redis,开发语言)