# 1.计数器的限流器

<?php
class AtomicLimit
{
    public function get_client_ip($type = 0) {
        $type       =  $type ? 1 : 0;
        static $ip  =   NULL;
        if ($ip !== NULL) return $ip[$type];
        if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
            $arr    =   explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
            $pos    =   array_search('unknown',$arr);
            if(false !== $pos) unset($arr[$pos]);
            $ip     =   trim($arr[0]);
        }elseif (isset($_SERVER['HTTP_CLIENT_IP'])) {
            $ip     =   $_SERVER['HTTP_CLIENT_IP'];
        }elseif (isset($_SERVER['REMOTE_ADDR'])) {
            $ip     =   $_SERVER['REMOTE_ADDR'];
        }
        // IP地址合法验证
        $long = ip2long($ip);
        $ip   = $long ? array($ip, $long) : array('0.0.0.0', 0);
        return $ip[$type];
    }

    public function atomic()
    {

        //接口时间限流,这种方式可以防止钻时间漏洞无限的访问接口 比如在59秒的时候访问,就钻了空子
        $redis = new Redis();
        $redis->connect('121.42.136.46', 16379);
        $redis->auth("L520g521");
        $ip = $this->get_client_ip(true);
        $len = $redis->lLen($ip);
        if($len === 0)
        {
            $redis->lPush($ip,time());
            echo "访问1次<br>";
            $redis->expire($ip,10);
        }else{
            //判断有没有超过1分钟
            $max_time = $redis->lRange($ip,0,0);
            //判断最后一次访问的时间比对是否超过了1分钟
            if((time()- $max_time[0]) < 60){
                if($len> 10){

                    echo '访问超过了限制';
                }else{

                    $redis->lPush($ip,time());
                    echo "访问{$len}次<br>";
                }
            }
        }
    }

}

(new AtomicLimit())->atomic();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

# 2.令牌桶算法

<?php
namespace Api\Lib;
 
/**
 * 限流控制
 */
class RateLimit
{
    private $minNum = 60; //单个用户每分访问数
    private $dayNum = 10000; //单个用户每天总的访问量
 
    public function minLimit($uid)
    {
        $minNumKey = $uid . '_minNum';
        $dayNumKey = $uid . '_dayNum';
        $resMin    = $this->getRedis($minNumKey, $this->minNum, 60);
        $resDay    = $this->getRedis($minNumKey, $this->minNum, 86400);
        if (!$resMin['status'] || !$resDay['status']) {
            exit($resMin['msg'] . $resDay['msg']);
        }
    }
 
    public function getRedis($key, $initNum, $expire)
    {
        $nowtime  = time();
        $result   = ['status' => true, 'msg' => ''];
        $redisObj = $this->di->get('redis');
        $redis->watch($key);
        $limitVal = $redis->get($key);
        if ($limitVal) {
            $limitVal = json_decode($limitVal, true);
            $newNum   = min($initNum, ($limitVal['num'] - 1) + (($initNum / $expire) * ($nowtime - $limitVal['time'])));
            if ($newNum > 0) {
                $redisVal = json_encode(['num' => $newNum, 'time' => time()]);
            } else {
                return ['status' => false, 'msg' => '当前时刻令牌消耗完!'];
            }
        } else {
            $redisVal = json_encode(['num' => $initNum, 'time' => time()]);
        }
        $redis->multi();
        $redis->set($key, $redisVal);
        $rob_result = $redis->exec();
        if (!$rob_result) {
            $result = ['status' => false, 'msg' => '访问频次过多!'];
        }
        return $result;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

# 3.滑动窗口限流

/**
* @param string $key      key
* @param num    $limitNum 限制数
* @param int    $ttl      过期时间
* @return mixed|null
*/
public static function apiRedisLimit($key, $limitNum, $ttl = 5)
{
  return RedisPool::invoke(function (\EasySwoole\Redis\Redis $redis) use ($key, $limitNum, $ttl) {
    $score = time();
    $nonce = uniqid() . (10000 * microtime(true));
    $redis->multi();
    $redis->zRemRangeByScore($key, 0, $score - $ttl);
    $redis->zAdd($key, $score, $nonce);
    $redis->expire($key, $ttl);
    $redis->zCount($key, 0, $score);
    $rt = $redis->exec();
    if (empty($rt[3])) {
      return true;
    }
    if ($rt[3] > $limitNum) {
      return false;
    }
    return true;
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# 4.lua限流

--[[ 
			KEYS:
				- redisKey
   	  ARGV: 
				- count 		申请的数量
				- rateCount 限流数量
				- rateTime 	限流时间
				- time		 	当前时间
--]]  
local function addToQueue(x,time)
     local count=0
     for i=1,x,1 do
         redis.call('lpush',KEYS[1],time)
         count=count+1
     end
     return count
 end
 local result=0
 local timeBase = redis.call('lindex',KEYS[1], tonumber(ARGV[2])-tonumber(ARGV[1]))
 if (timeBase == false) or (tonumber(ARGV[4]) - tonumber(timeBase)>tonumber(ARGV[3])) then
   result=result+addToQueue(tonumber(ARGV[1]),tonumber(ARGV[4]))
 end
 if (timeBase~=false) then
    redis.call('ltrim',KEYS[1],0,tonumber(ARGV[2]))
 end
 return result
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* redis+lua限流
*
* @param     $key
* @param     $limitNum
* @param int $ttl
* @return mixed|null
*/     
public static function apiRateLimitByLua($key, $limitNum, $ttl = 5)
{
  return RedisPool::invoke(function (\EasySwoole\Redis\Redis $redis) use ($key, $limitNum, $ttl) {
    $lua   = <<<SCRIPT
            redis.call('zAdd',KEYS[1],tonumber(ARGV[2]),ARGV[3])
            redis.call('zRemRangeByScore',KEYS[1],0,tonumber(ARGV[2])-tonumber(ARGV[1]))
            redis.call('expire',KEYS[1],tonumber(ARGV[1]))
            local num = redis.call('zCount',KEYS[1],0,tonumber(ARGV[2]))
            if num > tonumber(ARGV[4]) then
                return 1
            else
                return 0
            end
SCRIPT;
    $score = time();
    $nonce = uniqid() . (10000 * microtime(true));
    $rt    = $redis->eval($lua, 1, $key, $ttl, $score, $nonce,$limitNum);
    if ($rt === 1) {
      return false;
    }
    return true;
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

关于限流器的话,这是通过应用层去实现的,一般的情况都是通过网关层面去做的,做统一的网关入口,网关去做限流,日志,权限等。如:nginxapisix,阿里网关等。