什么是限流?
字面的意思就是限制流量,为啥要限流呢?限流在高并发场景中经常用到的自我保护的手段,不如你系统只能维持500并发,突然有一万人的访问,如果不做限制,你的系统就会崩溃,限流是系统健壮性保护的一种手段,也可以防止恶意的攻击。
限流算法
限流的算法有很多,常用的有简单粗暴的计数器、漏斗算法、令牌桶算法。
计数器
所谓计数器就是指单位时间内允许通过的访问量,一般我们会设置1s内允许通过请求量。比如限流qps为100,算法的实现思路就是从第一个请求进来开始计时,在接下去的1s内,每来一个请求,就把计数加1,如果累加的数字达到了100,那么后续的请求就会被全部拒绝。等到1s结束后,把计数恢复成0,重新开始计数。
具体实现:
- 对于每次服务调用,可以通过AtomicLong#incrementAndGet()方法来给计数器加1并返回最新值,通过这个最新值和阈值进行比较。
- 当然也可以用redis来做,使用redis的incr命令实现自增
这种实现方式,相信大家都知道有一个弊端:如果我在单位时间1s内的前10ms,已经通过了100个请求,那后面的990ms,只能眼巴巴的把请求拒绝,这种很浪费啊,也不平滑,不过在秒杀应用的比较多。那么我们能不能用一种比较平滑的方式呢?
漏桶算法
漏桶作为计量工具,类似于生活中的漏斗,水不停的放桶里面倒,但是下面的流出速度是固定的,不管你的倒入流量有多大,对于流出的速度没有影响,满了就会溢出来。
算法中实现一个容器用于放请求,比如服务每隔10ms的时间处理从容器中拿走一个请求来处理,请求突发暴增,容器中满了,新的请求就会被拒绝,直到容器有空间了请求才会再次放入。在算法实现方面,可以准备一个队列,用来保存请求,另外通过一个线程池定期从队列中获取请求并执行,可以一次性获取多个并发执行。
这种算法,在使用过后也存在弊端:无法应对短时间的突发流量。
令牌桶
令牌桶算法,是一个存放固定容量令牌的桶,按照固定速率往桶里添加令牌。令牌桶算法是对漏桶算法的一种改进,漏桶算法能够限制请求流出的速率,而令牌桶算法能够在限制调用的平均速率的同时还允许一定程度的突发调用,
- 有一个固定容量的桶,按照固定的速率向桶中添加令牌。
- 如果桶满了,则新添加的令牌被丢弃。
- 当请求进来时,必须从桶中获取一个令牌才能继续处理,否则拒绝请求,或者暂存到某个缓冲区等待可用令牌。
算法实现上,可以用一个BlockingQueue表示桶,起一个线程以固定的速率向桶中添加令牌。请求到达时需要先从桶中获取令牌,否则拒绝请求或等待。此外,Google开源的guava包也提供了很完善的令牌算法。
应用级限流
限流总并发/连接/请求数
对于一个应用系统来说一定会有极限并发/请求数,即总有一个TPS/QPS阀值,如果超了阀值则系统就会不响应用户请求或响应的非常慢,因此我们最好进行过载保护,防止大量请求涌入击垮系统。
如果你使用过Tomcat,打开/conf/server.xml文件,在Connector之前配置一个线程池:
1 2 3 4 5 | <Executor name= "tomcatThreadPool" namePrefix= "tomcatThreadPool-" maxThreads= "1000" maxIdleTime= "300000" minSpareThreads= "200" /> |
- name: 共享线程池的名字
- maxThreads: 该线程池可以容纳的最大线程数。默认值:200;
- maxIdleTime: 线程存活时间
- minSpareThreads: Tomcat应该始终打开的最小不活跃线程数。默认值:25
其Connector 其中一种配置有如下几个参数:
1 2 3 4 5 6 7 | <Connector executor= "tomcatThreadPool" port= "8080" protocol= "HTTP/1.1" connectionTimeout= "20000" redirectPort= "8443" minProcessors= "5" maxProcessors= "75" acceptCount= "1000" /> |
- acceptCount :如果Tomcat的线程都忙于响应,新来的连接会进入队列排队,如果超出排队大小,则拒绝连接;
- maxConnections : 瞬时最大连接数,超出的会排队等待;
- maxThreads :Tomcat能启动用来处理请求的最大线程数,如果请求处理量一直远远大于最大线程数则可能会僵死。
详细的配置请参考官方文档。另外如Mysql(如max_connections)、Redis(如tcp-backlog)都会有类似的限制连接数的配置。
限流总资源数
如果有的资源是稀缺资源(如数据库连接、线程),而且可能有多个系统都会去使用它,那么需要限制应用;可以使用池化技术来限制总资源数:连接池、线程池。比如分配给每个应用的数据库连接是100,那么本应用最多可以使用100个资源,超出了可以等待或者抛异常。
限流某个接口的总并发/请求数
如果接口可能会有突发访问情况,但又担心访问量太大造成崩溃,如抢购业务;这个时候就需要限制这个接口的总并发/请求数总请求数了;因为粒度比较细,可以为每个接口都设置相应的阀值。可以使用Java中的AtomicLong、semaphore进行限流:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | // AtomicLong实现 try { if (atomic.incrementAndGet() > limit) { // 限流处理 } // 处理请求 } finally { atomic.decrementAndGet(); } // Semaphore实现 private static Semaphore semaphore = new Semaphore( 100 ); boolean allowed = false ; try { allowed = semaphore.tryAcquire(); if (!allowed) { // 限流处理 } // 处理请求 } finally { semaphore.release( 1 ); } |
适合对业务无损的服务或者需要过载保护的服务进行限流,如抢购业务,超出了大小要么让用户排队,要么告诉用户没货了,对用户来说是可以接受的。而一些开放平台也会限制用户调用某个接口的试用请求量,也可以用这种计数器方式实现。这种方式也是简单粗暴的限流,没有平滑处理,需要根据实际情况选择使用;
限流某个接口的时间窗请求数
即一个时间窗口内的请求数,如想限制某个接口/服务每秒/每分钟/每天的请求数/调用量。如一些基础服务会被很多其他系统调用,比如商品详情页服务会调用基础商品服务调用,但是怕因为更新量比较大将基础服务打挂,这时我们要对每秒/每分钟的调用量进行限速;一种实现方式如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | LoadingCache<Long, AtomicLong> counter = CacheBuilder.newBuilder() .expireAfterWrite( 2 , TimeUnit.SECONDS) .build( new CacheLoader<Long, AtomicLong>() { @Override public AtomicLong load(Long seconds) throws Exception { return new AtomicLong( 0 ); } }); long limit = 1000 ; while ( true ) { //得到当前秒 long currentSeconds = System.currentTimeMillis() / 1000 ; if (counter.get(currentSeconds).incrementAndGet() > limit) { System.out.println( "限流了:" + currentSeconds); continue ; } //业务处理 } |
我们使用Guava的Cache来存储计数器,过期时间设置为2秒(保证1秒内的计数器是有的),然后我们获取当前时间戳然后取秒数来作为KEY进行计数统计和限流,这种方式也是简单粗暴,刚才说的场景够用了。
平滑限流某个接口的请求数
之前的限流方式都不能很好地应对突发请求,即瞬间请求可能都被允许从而导致一些问题;因此在一些场景中需要对突发请求进行整形,整形为平均速率请求处理(比如5r/s,则每隔200毫秒处理一个请求,平滑了速率)。这个时候有两种算法满足我们的场景:令牌桶和漏桶算法。Guava框架提供了令牌桶算法实现,可直接拿来使用。
Guava RateLimiter提供了令牌桶算法实现:平滑突发限流(SmoothBursty)和平滑预热限流(SmoothWarmingUp)实现。
SmoothBursty
tryAcquire
返回的是boolean,得到就返回true,否则false
1 2 3 4 5 6 7 | public void testTryAcquire(){ RateLimiter limiter = RateLimiter.create( 10 ); for ( int i = 0 ; i < 10 ; i = i + 1 ) { boolean flag = limiter.tryAcquire(); System.out.println( "cutTime=" + System.currentTimeMillis() + " acq:" + i + " flag:" + flag); } } |
输出
1 2 3 4 5 6 7 8 9 10 | cutTime= 1564401685827 acq: 0 flag: true cutTime= 1564401685833 acq: 1 flag: false cutTime= 1564401685833 acq: 2 flag: false cutTime= 1564401685833 acq: 3 flag: false cutTime= 1564401685833 acq: 4 flag: false cutTime= 1564401685833 acq: 5 flag: false cutTime= 1564401685833 acq: 6 flag: false cutTime= 1564401685833 acq: 7 flag: false cutTime= 1564401685833 acq: 8 flag: false cutTime= 1564401685833 acq: 9 flag: false |
咦。。。怎么只有第一次成功了,因为我们没有设置timeout参数,默认0等待,还没有生成令牌,当然没有获取到了,下面我看看这样:
1 2 3 4 5 | RateLimiter limiter = RateLimiter.create( 10 ); for ( int i = 0 ; i < 10 ; i = i + 1 ) { boolean flag = limiter.tryAcquire( 1 , 100 ,TimeUnit.MILLISECONDS); System.out.println( "cutTime=" + System.currentTimeMillis() + " acq:" + i + " flag:" + flag); } |
输出
1 2 3 4 5 6 7 8 9 10 | cutTime= 1564401572027 acq: 0 flag: true cutTime= 1564401572129 acq: 1 flag: true cutTime= 1564401572228 acq: 2 flag: true cutTime= 1564401572328 acq: 3 flag: true cutTime= 1564401572428 acq: 4 flag: true cutTime= 1564401572528 acq: 5 flag: true cutTime= 1564401572628 acq: 6 flag: true cutTime= 1564401572728 acq: 7 flag: true cutTime= 1564401572828 acq: 8 flag: true cutTime= 1564401572928 acq: 9 flag: true |
设置100ms的等待时间,成功获取了
acquire
1 2 3 4 5 6 7 | RateLimiter limiter = RateLimiter.create( 5 ); System.out.println(limiter.acquire()); System.out.println(limiter.acquire()); System.out.println(limiter.acquire()); System.out.println(limiter.acquire()); System.out.println(limiter.acquire()); System.out.println(limiter.acquire()); |
将得到类似如下的输出:
1 2 3 4 5 6 | 0.0 0.198239 0.196083 0.200609 0.199599 0.19961 |
- RateLimiter.create(5) 表示桶容量为5且每秒新增5个令牌,即每隔200毫秒新增一个令牌;
- limiter.acquire()表示消费一个令牌,如果当前桶中有足够令牌则成功(返回值为0),如果桶中没有令牌则暂停一段时间,比如发令牌间隔是200毫秒,则等待200毫秒后再去消费令牌(如上测试用例返回的为0.198239,差不多等待了200毫秒桶中才有令牌可用),这种实现将突发请求速率平均为了固定请求速率。
再看一个突发示例:
1 2 3 4 | RateLimiter limiter = RateLimiter.create( 5 ); System.out.println(limiter.acquire( 5 )); System.out.println(limiter.acquire( 1 )); System.out.println(limiter.acquire( 1 )) |
将得到类似如下的输出:
1 2 3 4 | 0.0 0.98745 0.183553 0.199909 |
limiter.acquire(5)表示桶的容量为5且每秒新增5个令牌,令牌桶算法允许一定程度的突发,所以可以一次性消费5个令牌,但接下来的limiter.acquire(1)将等待差不多1秒桶中才能有令牌,且接下来的请求也整形为固定速率了。
1 2 3 4 | RateLimiter limiter = RateLimiter.create( 5 ); System.out.println(limiter.acquire( 10 )); System.out.println(limiter.acquire( 1 )); System.out.println(limiter.acquire( 1 )); |
将得到类似如下的输出:
1 2 3 4 | 0.0 1.997428 0.192273 0.200616 |
同上边的例子类似,第一秒突发了10个请求,令牌桶算法也允许了这种突发(允许消费未来的令牌),但接下来的limiter.acquire(1)将等待差不多2秒桶中才能有令牌,且接下来的请求也整形为固定速率了。
接下来再看一个突发的例子:
1 2 3 4 5 6 7 8 9 | RateLimiter limiter = RateLimiter.create( 2 ); System.out.println(limiter.acquire()); Thread.sleep(2000L); System.out.println(limiter.acquire()); System.out.println(limiter.acquire()); System.out.println(limiter.acquire()); System.out.println(limiter.acquire()); System.out.println(limiter.acquire()); |
将得到类似如下的输出:
1 2 3 4 5 6 | 0.0 0.0 0.0 0.0 0.499876 0.495799 |
- 创建了一个桶容量为2且每秒新增2个令牌;
- 首先调用limiter.acquire()消费一个令牌,此时令牌桶可以满足(返回值为0);
- 然后线程暂停2秒,接下来的两个limiter.acquire()都能消费到令牌,第三个limiter.acquire()也同样消费到了令牌,到第四个时就需要等待500毫秒了。
此处可以看到我们设置的桶容量为2(即允许的突发量),这是因为SmoothBursty中有一个参数:最大突发秒数(maxBurstSeconds)默认值是1s,突发量/桶容量=速率*maxBurstSeconds,所以本示例桶容量/突发量为2,例子中前两个是消费了之前积攒的突发量,而第三个开始就是正常计算的了。令牌桶算法允许将一段时间内没有消费的令牌暂存到令牌桶中,留待未来使用,并允许未来请求的这种突发。
SmoothBursty通过平均速率和最后一次新增令牌的时间计算出下次新增令牌的时间的,另外需要一个桶暂存一段时间内没有使用的令牌(即可以突发的令牌数)。另外RateLimiter还提供了tryAcquire方法来进行无阻塞或可超时的令牌消费。
因为SmoothBursty允许一定程度的突发,会有人担心如果允许这种突发,假设突然间来了很大的流量,那么系统很可能扛不住这种突发。因此需要一种平滑速率的限流工具,从而系统冷启动后慢慢的趋于平均固定速率(即刚开始速率小一些,然后慢慢趋于我们设置的固定速率)。Guava也提供了SmoothWarmingUp来实现这种需求,其可以认为是漏桶算法,但是在某些特殊场景又不太一样。
SmoothWarmingUp创建方式:
1 | RateLimiter.create( double permitsPerSecond, long warmupPeriod, TimeUnit unit) |
permitsPerSecond表示每秒新增的令牌数,warmupPeriod表示在从冷启动速率过渡到平均速率的时间间隔。
示例如下:
1 2 3 4 5 6 7 8 | RateLimiter limiter = RateLimiter.create( 5 , 1000 , TimeUnit.MILLISECONDS); for ( int i = 1 ; i < 5 ;i++) { System.out.println(limiter.acquire()); } Thread.sleep(1000L); for ( int i = 1 ; i < 5 ;i++) { System.out.println(limiter.acquire()); } |
将得到类似如下的输出:
1 2 3 4 5 6 7 8 9 10 | 0.0 0.51767 0.357814 0.219992 0.199984 0.0 0.360826 0.220166 0.199723 0.199555 |
速率是梯形上升速率的,也就是说冷启动时会以一个比较大的速率慢慢到平均速率;然后趋于平均速率(梯形下降到平均速率)。可以通过调节warmupPeriod参数实现一开始就是平滑固定速率。
到此应用级限流的一些方法就介绍完了。假设将应用部署到多台机器,应用级限流方式只是单应用内的请求限流,不能进行全局限流。因此我们需要分布式限流和接入层限流来解决这个问题。
分布式限流
分布式限流最关键的是要将限流服务做成原子化,而解决方案可以使使用redis+lua或者nginx+lua技术进行实现,通过这两种技术可以实现的高并发和高性能。
首先我们来使用redis+lua实现时间窗内某个接口的请求数限流,实现了该功能后可以改造为限流总并发/请求数和限制总资源数。Lua本身就是一种编程语言,也可以使用它实现复杂的令牌桶或漏桶算法。
redis
1 2 3 4 5 6 7 8 9 10 | public boolean acquire() throws Exception{ long total = 1L; if (!redisUtil.hasKey(redisTokenKey)){ redisUtil.save(redisTokenKey, "0" ,outTime,TimeUnit.SECONDS); //初始化,并设置过期时间 } else { total = redisUtil.incr(redisTokenKey,1L); } LOG.info( "redis key index : " + total); return maxTotal <= total; } |
上面讲计数器的时候就用redis来实现了,用应用层调用redis接口来实现,但是这样不够优雅,可扩展性比较差,所以我们用lua脚本来实现,大家可以理解为跟存储过程一样的用法。。。
redis+lua实现计数器
lua脚本:
1 2 3 4 5 6 7 8 9 | local key = KEYS[ 1 ] --限流KEY(一秒一个) local limit = tonumber(ARGV[ 1 ]) --限流大小 local current = tonumber(redis.call( "INCRBY" , key, "1" )) --请求数+ 1 if current > limit then --如果超出限流大小 return 0 elseif current == 1 then --只有第一次访问需要设置 2 秒的过期时间 redis.call( "expire" , key, "2" ) end return 1 |
如上操作因是在一个lua脚本中,又因Redis是单线程模型,因此是线程安全的。如上方式有一个缺点就是当达到限流大小后还是会递增的,可以改造成如下方式实现:
1 2 3 4 5 6 7 8 9 10 | local key = KEYS[ 1 ] --限流KEY(一秒一个) local limit = tonumber(ARGV[ 1 ]) --限流大小 local current = tonumber(redis.call( 'get' , key) or "0" ) if current + 1 > limit then --如果超出限流大小 return 0 else --请求数+ 1 ,并设置 2 秒过期 redis.call( "INCRBY" , key, "1" ) redis.call( "expire" , key, "2" ) return 1 end |
如下是Java中判断是否需要限流的代码:
1 2 3 4 5 6 7 | public static boolean acquire() throws Exception { String luaScript = Files.toString( new File( "limit.lua" ), Charset.defaultCharset()); Jedis jedis = new Jedis( "192.168.20.128" , 6379 ); String key = "test_limit:" + System.currentTimeMillis()/ 1000 ; //此处将当前时间戳取秒数 Stringlimit = "5" ; //限流大小 return (Long)jedis.eval(luaScript,Lists.newArrayList(key), Lists.newArrayList(limit)) == 1 ; } |
因为Redis的限制(Lua中有写操作不能使用带随机性质的读操作,如TIME)不能在Redis Lua中使用TIME获取时间戳,因此只好从应用获取然后传入,在某些极端情况下(机器时钟不准的情况下),限流会存在一些小问题。
这样写仅是demo写法,调用每次都从文件中读取性能开销还是很大的,实际项目中应用的时候InitializingBean或者其他形式在项目启动的时候加载进来,当然也可以通过zookeeper加载在系统中用watch监听,变更重新加载。手段很多,这里不多说了。。。
redis令牌桶
大家有没有发现,上面这种写法还是缺陷的,就是所谓的计数器限流,不平滑,没法应对突发的峰值,所以借鉴RateLimiter的做法,用redis做令牌桶限流
lua脚本
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 | --- --- Generated by EmmyLua(https: //github.com/EmmyLua) --- Created by zgl. --- DateTime: 2018 - 10 - 29 16 : 54 --- 返回结果为 0 —未获得令牌, 1 —已获得令牌 --- hmap结构 [lastTimestamp,tokensRemaining] local key = KEYS[ 1 ] -- 限流的redis key值 local currentTimestamp = tonumber(ARGV[ 1 ]) -- 当前时间戳 local limit = tonumber(ARGV[ 2 ]) -- 间隔时间生成令牌的个数 local interval = tonumber(ARGV[ 3 ]) -- 桶的限流间隔 local intervalPerTokenTime = math.floor(interval/limit); -- 生成令牌的间隔时间 local counter = redis.call( 'hgetall' , key) if table.getn(counter) == 0 then -- 如果桶不存在则先设置key,可用令牌数=limit- 1 ,并返回 1 redis.call( 'hmset' , key, 'lastTimestamp' , currentTimestamp, 'tokensRemaining' , limit - 1 ) redis.call( 'pexpire' , key, interval) --设置过期时间 return 1 elseif table.getn(counter) == 4 then -- 如果桶存在则获取值 local lastTimestamp, tokensRemaining = tonumber(counter[ 2 ]), tonumber(counter[ 4 ]) local currentTokens if currentTimestamp > lastTimestamp then -- 校验当前时间是否大于最后一次获取时间 local intervalSinceLastTime = currentTimestamp - lastTimestamp if intervalSinceLastTime > interval then currentTokens = limit redis.call( 'hset' , key, 'lastTimestamp' , currentTimestamp) --更为新当前时间 else local grantedTokens = math.floor(intervalSinceLastTime / intervalPerTokenTime) if grantedTokens > 0 then local padMillis = math.fmod(intervalSinceLastTime, intervalPerTokenTime) redis.call( 'hset' , key, 'lastRefillTime' , currentTimestamp - padMillis) end currentTokens = math.min(grantedTokens + tokensRemaining, limit) end else currentTokens = tokensRemaining end assert (currentTokens >= 0 ) if currentTokens == 0 then -- 不能获取令牌 redis.call( 'hset' , key, 'tokensRemaining' , currentTokens) return 0 else -- 从桶里面拿到一个令牌 redis.call( 'hset' , key, 'tokensRemaining' , currentTokens - 1 ) return 1 end else error( "Size of counter is " .. table.getn(counter) .. ", Should Be 0 or 4." ) end |
java应用直接传key和三个参数
- currentTimestamp:当前获取的时间戳
- limit:间隔时间产生令牌的个数
- interval: 生成令牌的间隔时间,一般默认1000(毫秒)
- 细节上还有写问题,比如精确到毫秒的,当并发大于毫秒的精度时的处理都还没有考虑
- 这样虽然实现了令牌桶的限流,但只是相当于RateLimiter中tryAcquire()方法,没有实现延迟的功能,延迟复杂了点,先这样吧,有机会在优化。。。
public boolean tryAcquire(List<String> keys, List<String> args) { Jedis jedis = new Jedis(ip,port); return (Long)jedis.eval(LuaUtils.getTokenBucketScript(),keys, args) == 1 ; } @RequestMapping ( "/hi" ) public String home( @RequestParam String name) { String limit = "10" ; String currentTimestamp = String.valueOf(System.currentTimeMillis()); String interval = "1000" ; boolean flag = this .limitService.tryAcquire(Lists.newArrayList(name),Lists.newArrayList(currentTimestamp,limit,interval)); if (flag){ return "true" ; } else { return "false" ; } } |
使用Nginx+Lua实现的Lua脚本:
除了redis,在往前推,可以用nginx来实现,把请求挡在代理层
lua脚本
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 | -- 限制接口总并发数/请求数 local limit_count = require "resty.limit.count" -- 这里我们使用jmeter测试,每次访问 50 并发 -- 限制 1s内只能调用 10 次 接口(允许在时间段开始的时候一次性放过 10 个请求) local lim, err = limit_count. new ( "my_limit_count_store" , 10 , 1 ) if not lim then ngx.log(ngx.ERR, "failed to instantiate a resty.limit.count object: " , err) return ngx.exit( 500 ) end -- use the Authorization header as the limiting key local key = ngx.req.get_headers()[ "Authorization" ] or "public" local delay, err = lim:incoming(key, true ) if not delay then if err == "rejected" then ngx.header[ "X-RateLimit-Limit" ] = "5000" ngx.header[ "X-RateLimit-Remaining" ] = 0 return ngx.exit( 503 ) end ngx.log(ngx.ERR, "failed to limit count: " , err) return ngx.exit( 500 ) end local remaining = err ngx.header[ "X-RateLimit-Limit" ] = "5000" ngx.header[ "X-RateLimit-Remaining" ] = remaining |
实现中我们使用那个openresty进行的limit模块进行限流,限制总并发数,单位时间通过的请求数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | http { …… lua_shared_dict my_limit_req_store 100m; lua_shared_dict my_limit_conn_store 100m; lua_shared_dict my_limit_count_store 100m; server { listen 8070 ; server_name localhost; location /test { default_type text/html; access_by_lua_file D:\software\openresty- 1.15 . 8.1 -win64\lua\limit.lua; content_by_lua ' ngx.say( "<p>Hello, World!</p>" ) '; } …… } } |
当然OpenResty的lua-resty-limit-traffic
还可以实现漏桶、令牌桶等算法,这里就不做演示,大家可以github上查看
https://github.com/openresty/lua-resty-limit-traffic
最后
有人会纠结如果应用并发量非常大那么redis或者nginx是不是能抗得住;不过这个问题要从多方面考虑:你的流量是不是真的有这么大,是不是可以通过一致性哈希将分布式限流进行分片,是不是可以当并发量太大降级为应用级限流;redis可以考虑用clustor,nginx的优化手段也很多(比如worker 数量、文件句柄数、keeplive连接数、压缩等等);对策非常多,可以根据实际情况调节;一般流量是没有问题的。