浅谈API 接口访问频率限制




最近研究对API接口的访问频率进行限制,通过翻阅各种资料,学习到几种常用的限流思路,在此做个简单总结。

限流算法常用的有三种思路:令牌桶、漏桶、计数器。

1.令牌桶限流

令牌桶是一个存放固定容量令牌的桶,按照固定速率往桶里添加令牌,填满了就丢弃令牌,请求是否被处理要看桶中令牌是否足够,当令牌数减为零时则拒绝新的请求。令牌桶允许一定程度突发流量,只要有令牌就可以处理,支持一次拿多个令牌。令牌桶中装的是令牌。

3

2.漏桶限流

漏桶一个固定容量的漏桶,按照固定常量速率流出请求,流入请求速率任意,当流入的请求数累积到漏桶容量时,则新流入的请求被拒绝。漏桶可以看做是一个具有固定容量、固定流出速率的队列,漏桶限制的是请求的流出速率。漏桶中装的是请求。

2

3.计数器限流

有时我们还会使用计数器来进行限流,主要用来限制一定时间内的总并发数,比如数据库连接池、线程池、秒杀的并发数;计数器限流只要一定时间内的总请求数超过设定的阀值则进行限流,是一种简单粗暴的总数量限流,而不是平均速率限流。

1

以上三种限流思想,计数器这个算法虽然简单,但是有一个十分致命的问题,那就是临界问题。假设有一个恶意用户,他在0:59时,瞬间发送了100个请求,并且1:00又瞬间发送了100个请求,那么其实这个用户在 1秒里面,瞬间发送了200个请求。漏桶算法也非常简单,因为流入速度和流出速度一致,天生不会出现临界值问题,但是却没办法满足瞬间大流量需求。而令牌桶算法既能避免出现临界值问题也能满足瞬间大流量需求。接下来我们重点看下令牌桶算法。

在 Wikipedia 上,令牌桶算法是这么描述的:

  1. 每秒会有 r 个令牌放入桶中,或者说,每过 1/r 秒桶中增加一个令牌
  2. 桶中最多存放 b 个令牌,如果桶满了,新放入的令牌会被丢弃
  3. 当一个 n 字节的数据包到达时,消耗 n 个令牌,然后发送该数据包
  4. 如果桶中可用令牌小于 n,则该数据包将被缓存或丢弃

基于PHP+Redis实现的令牌桶算法

实现要点:

按照令牌桶算法的思路来实现,capacity为令牌的桶的最大容量,不管需求是请求次数n/1s或n/1min最终令牌桶的填充宿舍都会转化为每秒速度,其中也用到了redis乐观锁(下面会介绍到),防止key被同时更新,导致令牌数目更新错误。在这里,存储redis的时候给key设置了一个过期时间,尽可能减少无用key对redis存储空间的占用。

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
58
59
60
61
62
63
64
65
66
67
<?php
 
namespace Api\Lib;
 
class TokenBucket
{
 
    public $bucket;
    public $capacity;
    public $unit;
    public $ttl;
    private $redis;
    private static $unitMap = array(
        "second" => 1,
        "minute" => 60,
        "hour"   => 3600,
        "day"    => 86400,
    );
 
    public function __construct($bucket, $capacity, $unit, $redis)
    {
        $this->bucket   = $bucket;
        $this->capacity = $capacity;
        $this->unit     = $unit;
        $this->redis    = $redis;
    }
 
    /**
     * 限流方法
     * 调用:$tokenBucket = new TokenBucket('tokenBucket', 60, 'second', $this->di['redis']);
     *  $res = $tokenBucket->check('tokenBucketTest');
     */
    public function check($identification)
    {
        $period    = self::$unitMap[$this->unit];
        $this->ttl = $period;
        $fillRate  = $this->capacity / $period;
        $key       = $this->bucket . $identification;
        $now       = time();
        $this->redis->watch($key);
        //如果已经存在
        if ($this->redis->exists($key)) {
            $redisArr   = $this->redis->hMGet($key, ['allow', 'time']);
            $bucketTime = $now - $redisArr['time'];
            $allow      = $redisArr['allow'];
            $allow += $bucketTime * $fillRate;
            $allow = min($allow, $this->capacity);
            if ($allow < 1) {
                $this->redis->hMset($key, ['allow' => $allow, 'time' => $now]);
                $this->redis->expire($key, $this->ttl);
                return false;
            } else {
                $this->redis->multi();
                $this->redis->hMset($key, ['num' => $allow - 1, 'time' => $now]);
                $this->redis->expire($key, $this->ttl);
                $res = $this->redis->exec();
                if ($res) {
                    return true;
                }
            }
        } else {
            $this->redis->hMset($key, ['allow' => $this->capacity - 1, 'time' => $now]);
            $this->redis->expire($key, $this->ttl);
            return true;
        }
    }
}

Redis乐观锁

乐观锁
大多数是基于数据版本(version)的记录机制实现的。即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个”version”字段来实现读取出数据时,将此版本号一同读出,之后更新时,对此版本号加1。此时,将提交数据的版本号与数据库表对应记录的当前版本号进行比对,如果提交的数据版本号大于数据库当前版本号,则予以更新,否则认为是过期数据。redis中可以使用watch命令会监视给定的key,当exec时候如果监视的key从调用watch后发生过变化,则整个事务会失败。也可以调用watch多次监视多个key。这样就可以对指定的key加乐观锁了。注意watch的key是对整个连接有效的,事务也一样。如果连接断开,监视和事务都会被自动清除。当然了exec,discard,unwatch命令都会清除连接中的所有监视。

Redis事务
Redis中的事务(transaction)是一组命令的集合。事务同命令一样都是Redis最小的执行单位,一个事务中的命令要么都执行,要么都不执行。Redis事务的实现需要用到 MULTI 和 EXEC 两个命令,事务开始的时候先向Redis服务器发送 MULTI 命令,然后依次发送需要在本次事务中处理的命令,最后再发送 EXEC 命令表示事务命令结束。Redis的事务是下面4个命令来实现

1.multi,开启Redis的事务,置客户端为事务态。
2.exec,提交事务,执行从multi到此命令前的命令队列,置客户端为非事务态。
3.discard,取消事务,置客户端为非事务态。
4.watch,监视键值对,作用时如果事务提交exec时发现监视的监视对发生变化,事务将被取消。

 

发表评论

电子邮件地址不会被公开。 必填项已用*标注