avatar
PHP用redis实现计数器功能从而实现限流

admin 47 2019-12-01 16:11:34

                                           
                         <?php
    /**
     * Base on Redis component
     * User: yangzhen
     * Date: 2019/10/30
     * Time: 下午5:01
     */
    
    namespace mysoft\helpers;
    
    use mysoft\base\Exception;
    use yii\helpers\StringHelper;
    
    class Counter
    {
        /**
         * 前缀
         */
        const PREFIX = "cnt";
        
        /**
         * 当前计数器值
         * @var int
         */
        private $currentCount = 0;
        
        /**
         * counter限制
         * @var bool|int
         */
        private $maxLimit = false;
        
        /**
         * 过期时间
         * @var bool|int
         */
        private $expireSeconds = false;
        
        /**
         * 成功回调函数
         * @var bool|callable func
         */
        private $sucFunc = false;
        
        /**
         * 失败回调函数
         * @var bool|callable func
         */
        private $failFunc = false;
        
        /**
         * 延迟过期偏移量
         * @var int
         */
        private $delayOffset = 5;
        
        /**
         * 后缀
         * @var string
         */
        private $suffix = '';
        
        
        public function __construct()
        {
            if (!\Yii::$app->has('redis')) {
                throw new CounterException("component of redis not found for counter");
            }
            
        }
        
        /**
         * 设置过期时间
         * @param $seconds  int 单位秒
         * @return $this
         */
        public function withExpire($seconds)
        {
            $this->expireSeconds = $seconds;
            return $this;
        }
        
        /**
         * 设置最大限制
         * @param $max  int
         * @return $this
         */
        public function withMaxLimit($max)
        {
            $this->maxLimit = (int)$max;
            return $this;
        }
        
        /**
         * 设置未超越限制的回调
         * @param callable $func
         * @return $this
         * @throws \Exception
         */
        public function withUnExceedCalFunc(callable $func)
        {
            if (!is_callable($func)) {
                throw  new CounterException("withUnExceedCalFunc is not callable func");
            }
            $this->sucFunc = $func;
            return $this;
        }
        
        /**
         * 设置超越限制的回调
         * @param callable $func
         * @return $this
         * @throws \Exception
         */
        public function withExceedCalFunc(callable $func)
        {
            if (!is_callable($func)) {
                throw  new \Exception("withExceedCalFunc is not callable func");
            }
            $this->failFunc = $func;
            return $this;
        }
        
        /**
         * 执行计数器
         * @return mixed
         */
        private function doIncr($key)
        {
            
            $this->currentCount = $count = \Yii::$app->redis->incr($key);
            
            
            if ($this->expireSeconds != false && \Yii::$app->redis->ttl($key) <= 0) { //如果设置过期时间则设置
                \Yii::$app->redis->expire($key, $this->expireSeconds);//设置过期时间,便于清理数据
            }
            
            
            if ($this->maxLimit === false) { //没有设置Limit则直接返回当前增长到的count数
                return $count;
            }
            
            
            if ($count <= $this->maxLimit) { //没有超过限制
                
                if ($this->sucFunc != false) {
                    
                    return call_user_func($this->sucFunc, $count); //没有超过限制的回调方法
                }
                
                
            } else {//失败
                
                if ($this->failFunc != false) {
                    return call_user_func($this->failFunc, $count);//超过限制的回调方法
                }
                
            }
            
            return count; //没有设置回调函数统一返回当前count数
        }
        
        /**
         * 构建counter的key
         * @param $key
         * @return string
         * @throws CounterException
         */
        private function buildKey($key)
        {
            if (is_string($key)) {
                if ($this->suffix) $key .= '_' . $this->convertSuffix($this->suffix);
                $key = ctype_alnum($key) && StringHelper::byteLength($key) <= 32 ? $key : md5($key);
            } else {
                if ($this->suffix) $key[] = $this->convertSuffix($this->suffix);
                $key = md5(json_encode($key));
            }
            
            
            return static::PREFIX . ':' . $key;
        }
        
        
        /**
         * suffix的转换方法
         * @param $suffix  string|array
         * string -  普通模式,说明传入为已经定义好的字符串后缀,直接返回
         * array  -  函数模式,第一个元素为方法名,第二个开始就是参数列表值,格式如 [$func,$arg1[,...]],最终返回为String
         * @return  string
         * @throws CounterException
         */
        private function convertSuffix($suffix)
        {
            //处理后缀为函数变量的方法 ,格式为[$func,$arg1,$arg2 ...]
            if (is_array($suffix)) {
                $func = array_shift($suffix);//弹出第一个为命名方法
                $result = call_user_func_array($func, $suffix);
                if (!is_string($result)) {
                    throw new CounterException('suffix must be a string,please check callable func return');
                }
                return $result;
            }
            
            return $suffix;
        }
        
        
        /**
         * 按每秒控制频次,即 'X次/秒'
         * usage:
         *  (new Counter())->withSucessCallBack(callable func)->withFailCallBack(callable func)->limitBySecond()
         *
         * @param $limit
         * @return mixed
         */
        public function limitBySecond()
        {
            $this->suffix = ['date', 'YmdHis'];// date('YmdHis');
            $this->withExpire(2);
            return $this;
            
        }
        
        /**
         * 按每分钟控制频次
         * 用法参照按秒控制
         * @return mixed
         */
        public function limitByMinute()
        {
            $this->suffix = ['date', 'YmdHi'];//date('YmdHi')
            $this->withExpire(60 + $this->delayOffset);
            return $this;
        }
        
        /**
         *  按每小时控制频次
         *  用法参照秒控制
         * @return mixed
         */
        public function limitByHour()
        {
            $this->suffix = ['date', 'YmdH'];// date('YmdH')
            $this->withExpire(3600 + $this->delayOffset);
            return $this;
        }
        
        //用户自定义方式限制
        public function limitByUserDefine(callable $func, array $params, $duration = false)
        {
            //TODO:场景待实现
            return $this;
        }
        
        /**
         * 获取当前的计数器key完整值
         * @return mixed
         * @throws \Exception
         */
        public function getCounterKey($rawKey)
        {
            
            return $this->buildKey($rawKey);
        }
        
        
        /**
         * 获取当前计数器数值,必须先执行Run
         * @return int
         */
        public function getCurrentCount()
        {
            return $this->currentCount;
        }
        
        
        //这里是最终的执行方法
        public function Run($rawKey)
        {
            return $this->doIncr($this->getCounterKey($rawKey));
        }
    }
    
    /**
     * 自定义Counter的异常处理类
     * Class CounterException
     * @package mysoft\helpers
     */
    class CounterException extends Exception
    {
        public function __construct($message, $code = -1)
        {
            parent::__construct($message, $code);
        }
        
        /**
         * @return string the user-friendly name of this exception
         */
        public function getName()
        {
            return 'Counter Exception';
        }
    }
                      
                                       
To share this paste please copy this url and send to your friends
预览

评论

需要身份验证

您必须登录才能发表评论.

登录
    还没有评论.