对于一个应用系统来说一定会有极限并发/请求数,即总有一个TPS/QPS阀值,如果超了阀值则系统就会不响应用户请求或响应的非常慢,因此我们最好进行过载保护,防止大量请求涌入击垮系统。如果不知道自己应用TPS/QPS阀值的,可以看下我的另外一篇博文。

那么常用的限流方法有

容器限流

常用的有tomcat容器最大线程数,最大连接数,nginx 限制ip的链接和并发等;

tomcat参数配置

看看tomcat官网上怎么说的:

backlog:当所有可能的请求处理线程都在使用时,队列中排队的请求最大数目。默认为10,当队列已满,任何请求都将被拒绝
maxSpareThread:允许存在空闲线程的最大数目,默认值为50
maxThread:最大线程数,默认值为200
minSpareThreads:设当连接器第一次启动时创建线程的数目,确保至少有这么多的空闲线程可用,默认值为4
port:服务端套接字的TCP端口号,默认值为8089(必须)
topNoDelay:为true时,可以提高性能,默认值为true
soTimeout:超时值

nginx官网我们看下:

HttpLimit zone参数配置

java 对url并发控制 java控制并发访问量_并发

limit_req-zone参数配置

java 对url并发控制 java控制并发访问量_并发_02

java 对url并发控制 java控制并发访问量_默认值_03

这里解释一下 

$binary_remote_addr  表示根据客户端ip限制;
zone=one:10m   生成10m空间 命名为one,用于储存访问频率;
rate=1r/s;的意思是限制相同ip每秒访问频次 这里是1秒1次;
limit_req   zone=one  burst=5;

zone=one表示使用上面的one 10m储存空间数据,计算限制;

burst=5  busrst爆发缓冲区5;超过访问频率的请求放在缓冲区;

限制总的资源数

其实tomcat nginx等容器限流的方式,都是通过限制资源数实现限流的;不过他们是在网络层面就挡了;容器的限流有个不好的地方就是用户体验十分不好;不能做到具体业务优化提示;比如一些异步请求,秒杀场景等;我们有时候可以让这类请求在业务代码层面进行限流,比如秒杀我们可以让请求加入请求队列,然后在慢慢消费,通过限制消费速率来限流;比较经典的有提高mq 异步削峰;这里其实就是漏斗算法的演变。还有比如数据库连接数资源。

限制接口的总并发数/请求数

除了限制总的资源数我们还是可以控制请求数,这点我们12306网站做的炉火纯青;

我们来看下通过验证码挡掉一些请求;

java 对url并发控制 java控制并发访问量_java 对url并发控制_04

除了验证码还有其他的方法比如小库存秒杀场景里,通过随机请求控制并发数。一般这类做法前都会和后台进行一个随机数token的确认,从而避免有人恶意秒。

除了前端控制处理外,也可以在后台控制层加个多线程计数器进行限流;如 AtomicLong;但是并发量大的话 性能不怎么样,小迸发可以;

Guava实现限流

Guava是谷歌开源的Java库,里面对于封装了一些常用的类库,是对JDk的扩展延伸丰富吧。今天我们介绍他里面RateLimiter类。GuavaRateLimiter提供了令牌桶算法的实现:平滑突发限流  和 平滑预热限流 实现。

平滑突发限流:

/**
  * 平滑突发限流(SmoothBursty)
  */
 public class SmoothBurstyRateLimitTest {
     public static void main(String[] args) {
         //QPS = 5,每秒允许5个请求
         RateLimiter limiter = RateLimiter.create(5);
         //limiter.acquire() 返回获取token的耗时,以秒为单位
         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());
     }
 }
  
 0.0
 0.19667
 0.195784
 0.194672
 0.195658
 0.195285/**
  * 平滑突发限流(smoothbursty)
  */
 public class SmoothBurstyRateLimitTest02 {
  
     public static void main(String[] args) {
         //每秒允许5个请求,表示桶容量为5且每秒新增5个令牌,即每隔0.2毫秒新增一个令牌
         RateLimiter limiter = RateLimiter.create(5);
         //一次性消费5个令牌
         System.out.println(limiter.acquire(5));
         //limiter.acquire(1)将等待差不多1秒桶中才能有令牌
         System.out.println(limiter.acquire(1));
         //固定速率
         System.out.println(limiter.acquire(1));
         //固定速率
         System.out.println(limiter.acquire(1));
         //固定速率
         System.out.println(limiter.acquire(1));
     }
 } 
/**
  * 平滑突发限流(smoothbursty)
  */
 public class SmoothBurstyRateLimitTest03 {
  
     public static void main(String[] args) {
         //每秒允许5个请求,表示桶容量为5且每秒新增5个令牌,即每隔0.2毫秒新增一个令牌
         RateLimiter limiter = RateLimiter.create(5);
         //第一秒突发了10个请求
         System.out.println(limiter.acquire(10));
         //limiter.acquire(1)将等待差不多2秒桶中才能有令牌
         System.out.println(limiter.acquire(1));
         //固定速率
         System.out.println(limiter.acquire(1));
         //固定速率
         System.out.println(limiter.acquire(1));
         //固定速率
         System.out.println(limiter.acquire(1));
     }
 }平滑预热限流:
/**
  * 平滑预热限流(SmoothWarmingUp)
  */
 public class SmoothWarmingUp {
     public static void main(String[] args) {
         //permitsPerSecond:每秒新增的令牌数  warmupPeriod:从冷启动速率过渡到平均速率的时间间隔
         //系统冷启动后慢慢的趋于平均固定速率(即刚开始速率慢一些,然后慢慢趋于我们设置的固定速率)
         RateLimiter limiter = RateLimiter.create(10, 1000, TimeUnit.MILLISECONDS);
         for(int i = 0; i < 10;i++) {
             //获取一个令牌
             System.out.println(limiter.acquire(1));
         }
     }
 }
guava 秒杀场景的实现
public class MiaoShaTest {

    public static void main(String[] args) throws InterruptedException {
        //限流,每秒允许10个请求进入秒杀
        RateLimiter limiter = RateLimiter.create(10);
        for (int i = 0; i < 100; i++) {
            //100个线程同时抢购
            new Thread(() -> {
                //每个秒杀请求如果100ms以内得到令牌,就算是秒杀成功,否则就返回秒杀失败
                if (limiter.tryAcquire(100, TimeUnit.MILLISECONDS)) {
                    System.out.println("恭喜您,秒杀成功");
                } else {
                    System.out.println("秒杀失败,请继续努力");
                }
            }).start();
            //等待新的令牌生成
            Thread.sleep(10);
        }
    }
}

运行结果:恭喜您,秒杀成功
秒杀失败,请继续努力
秒杀失败,请继续努力
秒杀失败,请继续努力
秒杀失败,请继续努力
恭喜您,秒杀成功
秒杀失败,请继续努力
秒杀失败,请继续努力
秒杀失败,请继续努力
秒杀失败,请继续努力
秒杀失败,请继续努力
秒杀失败,请继续努力
秒杀失败,请继续努力
恭喜您,秒杀成功
秒杀失败,请继续努力
秒杀失败,请继续努力
秒杀失败,请继续努力
秒杀失败,请继续努力
秒杀失败,请继续努力
秒杀失败,请继续努力
秒杀失败,请继续努力
秒杀失败,请继续努力
恭喜您,秒杀成功

    

最后附上俩在限流算法:

漏桶算法 VS 令牌桶算法

漏桶算法(Leaky Bucket)

漏桶算法(Leaky Bucket):水(请求)先进入到漏桶里,漏桶以一定的速度出水(接口有响应速率),当水流入速度过大会直接溢出(访问频率超过接口响应速率),然后就拒绝请求,可以看出漏桶算法能强行限制数据的传输速率.示意图如下:

可见这里有两个变量,一个是桶的大小,支持流量突发增多时可以存多少的水(burst),另一个是水桶漏洞的大小(rate)。

令牌桶算法(Token Bucket)

令牌桶算法(Token Bucket)和 Leaky Bucket 效果一样但方向相反的算法,更加容易理解.随着时间流逝,系统会按恒定1/QPS时间间隔(如果QPS=100,则间隔是10ms)往桶里加入Token(想象和漏洞漏水相反,有个水龙头在不断的加水),如果桶已经满了就不再加了.新请求来临时,会各自拿走一个Token,如果没有Token可拿了就阻塞或者拒绝服务.

令牌桶的另外一个好处是可以方便的改变速度. 一旦需要提高速率,则按需提高放入桶中的令牌的速率. 一般会定时(比如100毫秒)往桶中增加一定数量的令牌, 有些变种算法则实时的计算应该增加的令牌的数量.

漏桶算法VS令牌桶算法

令牌桶是按照固定速率往桶中添加令牌,请求是否被处理需要看桶中令牌是否足够,当令牌数减为零时则拒绝新的请求;

漏桶则是按照常量固定速率流出请求,流入请求速率任意,当流入的请求数累积到漏桶容量时,则新流入的请求被拒绝;

令牌桶限制的是平均流入速率(允许突发请求,只要有令牌就可以处理,支持一次拿3个令牌,4个令牌),并允许一定程度突发流量;

漏桶限制的是常量流出速率(即流出速率是一个固定常量值,比如都是1的速率流出,而不能一次是1,下次又是2),从而平滑突发流入速率;

令牌桶允许一定程度的突发,而漏桶主要目的是平滑流入速率;

两个算法实现可以一样,但是方向是相反的,对于相同的参数得到的限流效果是一样的。