【PHP】用Redis实现限流的常见方案

更新日期: 2020-11-19 阅读: 3.5k 标签: Redis

限流实现的思路比较多,一般比较常见的思路有 计数器,滑动窗口,令牌桶。

而Redis有着丰富的数据结构以及分布式的支持,使用Redis实现限流的业务还是比较适合的。

并且在Redis 4.0 上可以安装限流模块 redis-cell,其思路也是令牌桶,其提供了限流的原子操作使用起来很方便可靠。


计数器

计数器即在限定的时间内记请求的次数,如果过了这个时间段就重置次数。

这和我们平时参加一些活动很像,比如超市里有时做活动,每天一个人可以领一个鸡蛋,今天领过了就不能再领了但到了明天又可以再领了。

这里可以用 String 来实现,比如要实现一分钟限制100次,只要记下上次请求的是哪一分钟,并记住这一分钟请求的次数即可,利用过期时间可以淘汰不用的数据。

    public function checkRequest($uid)
    {
        $timeKey = self::LIMIT_TIME_KEY . $uid;
        // 得到上次请求的时间
        $lastTime = $this->redis->get($timeKey);
        if ($lastTime) {
            // 看看时间戳是不是已经过了一分钟(自己设定的时间范围)
            $nowTime = time();
            if ($nowTime - $lastTime < self::LIMIT_UNIT) {
                // 还没一分钟
                // 获取下请求了多少次
                $countKey = self::LIMIT_KEY . $lastTime . ‘:‘ . $uid;
                $count = $this->redis->get($countKey);
if ($count >= self::LIMIT_COUNT) { // 超过次数了 return false; } else { // 没超过加一 $this->redis->set($countKey, ++$count, self::TTL); }
} else { // 超过一分钟了可以重置了 $this->initLimitData($uid); } } else { // 都没有记录肯定没请求或好久没请求了 可以重置了 $this->initLimitData($uid); } return true; }

这种实现缺点就是如果请求正好在间隔处密集请求,就大致可以被请求两倍的限额量。


滑动窗口

这种思路是记录下每次请求的时间,然后再统计下在规定范围内(窗口内)被请求了多少次。

这种方式可以通过Redis ZSET 有序集合来实现,通过时间戳作为分数,只要获取相应时间戳范围内的请求次数并加以判断即可。

    public function checkRequest($uid)
    {
        // 是否严重超过请求限制
        $banKey = self::LIMIT_BAN_KEY . $uid;
        $isBan = $this->redis->get($banKey);
        if ($isBan) {
            return false;
        }

        // 得到微秒 粒度太粗不好测
        // startTime 与 nowTime 形成了一个窗口的时间范围
        $nowTime = microtime(true) * 1000;
        $startTime = $nowTime - self::LIMIT_UNIT * 1000;

        $zSetKey = self::LIMIT_KEY . $uid;

        // 获取时间范围内的请求数据
        $requestHistory = $this->redis->zRangeByScore($zSetKey, $startTime, $nowTime);

        $count = count($requestHistory);
        // 首先判断是否严重超过限制
        if ($count > self::LIMIT_BAN_COUNT) {
            // 直接封
            $this->redis->set($banKey, 1, self::BAN_TTL);
            return false;
        }

        // 将下面的REDIS命令打包,使一组命令具有原子性
        $this->redis->multi();
        // 添加
        $options = [];
        $value = $uid . ‘:‘ . $nowTime . rand(0, 999);
        // 多余的数据删除
        $this->redis->zRemRangeByScore($zSetKey, 0, $startTime);
        // 添加数据
        $this->redis->zAdd($zSetKey, $options, $nowTime, $value);
        // 设置过期时间
        $this->redis->expire($zSetKey, self::TTL);
        // 执行这一组命令
        $this->redis->exec();

        if ($count >= self::LIMIT_COUNT) {
            return false;
        } else {
            return true;
        }
    }

要注意的是如果不清理窗口外的数据并有用户一直稳定地请求的话,这个用户的有序集合就会越来越大。

并且删除时也需要注意是否待删除数据会不会很多,因为删除一个BigKey是有可能造成Redis短暂的阻塞。

可以判断下用户在窗口内的请求次数有没有严重超过限制的次数,严重超过后可以考虑停用该用户的服务。


令牌桶

这种限流方式很像小学时候遇到的一种数学题,有个水池如果打开水龙头N小时装满,如果打开下面的水塞M小时放完,现在同时打开水龙头和水塞问啥时候水能放完。

哈哈,如果要把水放完当然要流出的水比流入的水速率高才行。

令牌桶也是一样就是不断往桶里生成令牌,取的太快,取完了就会限流。

用Redis实现只要记录下每次请求的时间,计算这次与上次请求的两次时间内应该生成多少个令牌再减去这次消耗的一个令牌 如果算下来令牌小于0就应该限流了。

    public function checkRequest($uid)
    {
        $key = self::LIMIT_KEY . $uid;
        $data = $this->redis->get($key);
        // 是否有请求的记录数据
        if ($data) {
            $value = json_decode($data, true);

            // 计算一下上个请求到现在为止会生成多少个令牌
            $nowTime = microtime(true) * 1000;
            $speed = self::LIMIT_COUNT / (self::LIMIT_UNIT * 1000);

            // 时间间隔不要太长
            $timeGap = min([$nowTime - $value[‘time‘], self::MAX_GAP * 1000]);
            $tokenCount = $timeGap * $speed;

            // 之前的数量 加上生成的数量 再减掉这次消耗的1个
            $count = $value[‘count‘] + $tokenCount - 1;
            // 记录下令牌的变动
            $this->redis->set($key, json_encode($this->getJsonData($count)), self::TTL);

            if ($count <= 0) {
                return false;
            } else {
                return true;
            }

        } else {
            // 设置初始时间信息
            $initData = $this->getJsonData(1);
            $this->redis->set($key, json_encode($initData), self::TTL);
            return true;
        }
    }

令牌桶是根据速率限流,所以会很敏感,因为生成的速率是平均的,如果一开始流量就突然很大就很容易被限流。当然根据不同的业务场景和需求可以因地制宜的修改与实现。


本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!

链接: https://fly63.com/article/detial/9965

相关推荐

Redis可视化管理工具有哪些?

redis 是当前非常流行的缓存数据库,得益于其简单的 key-value 模式的数据存储和丰富的数据类型与事件机制使得 redis 成为当前后端开发中不可或缺的利器。下面推荐一些好用的 redis 的管理工具

使用 Redis 构建先进先出队列解决秒杀问题

先进先出队列(first in first out queue)是一种非常常见的数据结构, 一般都会包含入队(enqueue)和出队(dequeue)这两个操作, 其中入队操作会将一个元素放入到队列中, 而出队操作则会从队列中移除最先被入队的元素

在使用redis-cluster之前你需要知道这些事

前段时间基础架构组、DBA还有云盘团队一起推广了phpredis的RedisCluster的线上使用,目前线上业务已经稳定,单业务的规模水平是:Qps平均15W,数据量在700G左右。现对这段时间的工作和所遇到的一些常见问题进行简单总结

基于redis实现定时任务

业务中碰到的需求(抽象描述一下):针对不同的用户能够实现不同时间的间隔循环任务。比如在用户注册成功24小时后给用户推送相关短信等类似需求。使用crontab?太重,且基本不现实,不可能给每一个用户在服务器上生成一个定时任务。

docker搭建redis集群

从docker库获取ruby镜像;创建虚拟网卡;查看网卡信息;查看redis-net虚拟网卡网关ip;创建需要挂载的目录以及文件;在/docker/redis目录下创建一个sh脚本文件;执行脚本创建挂载目录以及配置文件

redis用在哪里?redis 应用场景

Redis是一个由Salvatore Sanfilippo写的key-value存储系统。Redis是一个开源的使用ANSI C语言编写、遵守BSD协议、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。

Redis的基本数据结构及基本命令详解

redis是一个key-value存储系统。和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sorted set --有序集合)和hash(哈希类型)

Redis中设置了过期时间的Key,那么你还要知道些什么?

熟悉Redis的同学应该知道,Redis的每个Key都可以设置一个过期时间,当达到过期时间的时候,这个key就会被自动删除。这就是Redis的过期策略。在为key设置过期时间需要注意的事项

Redis 的各项功能解决了哪些问题?

官方简介解释到:Redis是一个基于BSD开源的项目,是一个把结构化的数据放在内存中的一个存储系统,你可以把它作为数据库,缓存和消息中间件来使用。同时支持strings,lists,hashes,sets,sorted sets

Redis分布式锁

redis:Redis是开源,内存存储的数据结构服务器,可用作数据库,高速缓存和消息队列代理.采用单进程单线程模型,并发能力强大,主流的分布式缓存工具。加锁通过setnx向特定的key值写入一个随机值,并且同时设置失效时间,写值成功即加锁成功.

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!